From 3ab8e3b2111cf3f39e1945e9bede663b71540b1a Mon Sep 17 00:00:00 2001 From: l30062829 Date: Sat, 17 May 2025 11:10:23 +0800 Subject: [PATCH 01/30] =?UTF-8?q?=E3=80=90Mindearth=E3=80=91=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E8=B4=A1=E7=8C=AE=E8=80=85=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applications/earthquake/G-TEAM/README.md | 2 +- .../earthquake/G-TEAM/README_CN.md | 2 +- .../earthquake/G-TEAM/images/image.png | Bin 188617 -> 208953 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MindEarth/applications/earthquake/G-TEAM/README.md b/MindEarth/applications/earthquake/G-TEAM/README.md index 4ca498841..fde491dff 100644 --- a/MindEarth/applications/earthquake/G-TEAM/README.md +++ b/MindEarth/applications/earthquake/G-TEAM/README.md @@ -75,6 +75,6 @@ Scatter plot compares predicted vs actual PGA values (x-axis vs y-axis). Closer ## Contributors -gitee id: funfunplus +gitee id: chengjie, longjundong, xujiabao, dinghongyang, funfunplus email: funniless@163.com \ No newline at end of file diff --git a/MindEarth/applications/earthquake/G-TEAM/README_CN.md b/MindEarth/applications/earthquake/G-TEAM/README_CN.md index 4fc45eae1..5bb7eff96 100644 --- a/MindEarth/applications/earthquake/G-TEAM/README_CN.md +++ b/MindEarth/applications/earthquake/G-TEAM/README_CN.md @@ -56,6 +56,6 @@ python main.py --cfg_path ./config/config.yaml --device_id 0 --device_target Asc ## 贡献者 -gitee id: funfunplus +gitee id: chengjie, longjundong, xujiabao, dinghongyang, funfunplus email: funniless@163.com \ No newline at end of file diff --git a/MindEarth/applications/earthquake/G-TEAM/images/image.png b/MindEarth/applications/earthquake/G-TEAM/images/image.png index 7c638885b15de261c21b664bd576b55ec169dd99..455f89f7b4e7d98a145f983df99d13bdad6e8c31 100644 GIT binary patch literal 208953 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%0AMod%WG?Ed*g3&VujPFl`uQ( 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 -- Gitee From 4fe133066a9adc4acf80b7c0bac23405b28dcf5d Mon Sep 17 00:00:00 2001 From: brian Date: Mon, 26 May 2025 20:13:03 +0800 Subject: [PATCH 02/30] add flash attention --- MindFlow/mindflow/cell/__init__.py | 8 +- MindFlow/mindflow/cell/attention.py | 236 ++++++++++++------ .../mindflow/cell/diffusion_transformer.py | 7 +- MindFlow/mindflow/cell/vit.py | 6 +- .../mindflow/cell/attention/test_attention.py | 127 ++++++---- tests/st/mindflow/cell/test_optimizers.py | 17 +- 6 files changed, 258 insertions(+), 143 deletions(-) diff --git a/MindFlow/mindflow/cell/__init__.py b/MindFlow/mindflow/cell/__init__.py index 59bbc9634..d71670098 100644 --- a/MindFlow/mindflow/cell/__init__.py +++ b/MindFlow/mindflow/cell/__init__.py @@ -16,7 +16,7 @@ from .activation import get_activation from .basic_block import LinearBlock, ResBlock, InputScale, FCSequential, MultiScaleFCSequential, DropPath from .neural_operators import FNO1D, FNO2D, FNO3D, KNO1D, KNO2D, PDENet, PeRCNN, SNO, SNO1D, SNO2D, SNO3D -from .attention import Attention, MultiHeadAttention, AttentionBlock +from .attention import Attention, MultiHeadAttention, TransformerBlock from .vit import ViT from .unet2d import UNet2D from .sno_utils import poly_data, get_poly_transform, interpolate_1d_dataset, interpolate_2d_dataset @@ -24,8 +24,8 @@ from .diffusion import DiffusionScheduler, DiffusionTrainer, DDPMScheduler, DDIM from .diffusion_transformer import DiffusionTransformer, ConditionDiffusionTransformer __all__ = ["get_activation", "FNO1D", "FNO2D", "FNO3D", "KNO1D", "KNO2D", "PDENet", "UNet2D", "PeRCNN", - "SNO", "SNO1D", "SNO2D", "SNO3D", "Attention", "MultiHeadAttention", "AttentionBlock", "ViT", "DDPMPipeline", - "DDIMPipeline", "DiffusionTrainer", "DiffusionScheduler", "DDPMScheduler", "DDIMScheduler", - "DiffusionTransformer", "ConditionDiffusionTransformer"] + "SNO", "SNO1D", "SNO2D", "SNO3D", "Attention", "MultiHeadAttention", "TransformerBlock", + "ViT", "DDPMPipeline", "DDIMPipeline", "DiffusionTrainer", "DiffusionScheduler", "DDPMScheduler", + "DDIMScheduler", "DiffusionTransformer", "ConditionDiffusionTransformer"] __all__.extend(basic_block.__all__) __all__.extend(sno_utils.__all__) diff --git a/MindFlow/mindflow/cell/attention.py b/MindFlow/mindflow/cell/attention.py index b100dd8cb..ce4460809 100644 --- a/MindFlow/mindflow/cell/attention.py +++ b/MindFlow/mindflow/cell/attention.py @@ -13,7 +13,7 @@ # limitations under the License. # ============================================================================ """Attention module""" - +from typing import Optional from mindspore import ops, nn, Tensor import mindspore.common.dtype as mstype @@ -30,11 +30,10 @@ class Attention(nn.Cell): Inputs: - **x** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. - - **attn_mask** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, sequence\_len)` or - :math:`(sequence\_len, sequence\_len)` or :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)`. - - **key_padding_mask** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len)` or - :math:`(batch\_size, sequence\_len, sequence\_len)` or - :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)`. + - **attn_mask** (Tensor, optional) - Tensor with shape :math:`(sequence\_len, sequence\_len)` or + or :math:`(batch\_size, 1, sequence\_len, sequence\_len)`. Default: ``None``. + - **key_padding_mask** (Tensor, optional) - Tensor with shape :math:`(batch\_size, sequence\_len)`. + Default: ``None``. Outputs: - **output** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. @@ -52,50 +51,48 @@ class Attention(nn.Cell): (2, 4, 32, 128) """ - def __init__(self, in_channels, num_heads, compute_dtype=mstype.float32): + def __init__(self, in_channels: int, num_heads: int, compute_dtype: mstype = mstype.float32): super().__init__() self.num_heads = num_heads self.compute_dtype = compute_dtype - self.softmax_func = nn.Softmax(axis=-1) - self.matmul = ops.BatchMatMul() self.qkv = nn.Dense( in_channels, in_channels * 3, weight_init="XavierUniform" ).to_float(compute_dtype) - def softmax(self, scores, compute_dtype=mstype.float32): - if scores.dtype != mstype.float32: - scores = scores.astype(mstype.float32) - attn = self.softmax_func(scores) - if compute_dtype == mstype.float32: - return attn - return attn.astype(compute_dtype) - - def _mask_scores(self, scores, attn_mask=None, key_padding_mask=None): - """mask attention scores""" - batch, _, _, node = scores.shape - mask = ops.zeros_like(scores) + @staticmethod + def merge_mask(attn_mask: Optional[Tensor] = None, key_padding_mask: Optional[Tensor] = None) -> Tensor: + """merge mask""" + if attn_mask is None and key_padding_mask is None: + return None + mask = Tensor(0, dtype=mstype.uint8) if attn_mask is not None: - attn_mask = attn_mask.astype(scores.dtype) + node = attn_mask.shape[-1] if len(attn_mask.shape) == 2: attn_mask = attn_mask.reshape(1, 1, node, node) - elif len(attn_mask.shape) == 3: - attn_mask = attn_mask.unsqueeze(1) - else: + elif len(attn_mask.shape) == 4: pass - mask += attn_mask + else: + raise Exception(f'attn_mask shape {attn_mask.shape} not support') + mask = mask + attn_mask.astype(mstype.uint8) if key_padding_mask is not None: - key_padding_mask = key_padding_mask.astype(scores.dtype) + batch, node = key_padding_mask.shape[0], key_padding_mask.shape[-1] if len(key_padding_mask.shape) == 2: key_padding_mask = ops.broadcast_to(key_padding_mask.unsqueeze(1), (batch, node, node)).unsqueeze(1) - elif len(key_padding_mask.shape) == 3: - key_padding_mask = key_padding_mask.unsqueeze(1) else: - pass - mask += key_padding_mask + raise Exception(f'key_padding_mask shape {attn_mask.shape} not support') + mask = mask + key_padding_mask.astype(mstype.uint8) + return mask + + @staticmethod + def mask_scores(scores: Tensor, mask: Optional[Tensor] = None) -> Tensor: + """mask attention scores""" + if mask is None: + return scores scores += mask * Tensor(-1e10, scores.dtype) return scores - def get_qkv(self, x): + def get_qkv(self, x: Tensor) -> tuple[Tensor]: + """get qkv value""" b, n, _ = x.shape qkv = ( self.qkv(x).reshape(b, n, 3, self.num_heads, - @@ -103,32 +100,103 @@ class Attention(nn.Cell): ) return qkv[0], qkv[1], qkv[2] - def _reshape_output(self, x): + def _reshape_output(self, x: Tensor) -> Tensor: b, _, n, _ = x.shape return x.transpose(0, 2, 1, 3).reshape(b, n, -1) - def construct(self, x, attn_mask=None, key_padding_mask=None): + def construct(self, x: Tensor, attn_mask: Optional[Tensor] = None, key_padding_mask: Optional[Tensor] = None): """Attention network construction.""" raise NotImplementedError +class ScaledDot(nn.Cell): + """Scaled dot attention""" + + def __init__(self, scale): + super().__init__() + self.scale = scale + + def construct(self, query: Tensor, key: Tensor, value: Tensor, mask: Optional[Tensor] = None): + scores = ops.matmul(query, key.swapaxes(-1, -2)) * self.scale + scores = Attention.mask_scores(scores, mask) + scores = scores.astype(mstype.float32) + attn = ops.softmax(scores, axis=-1) + attn = attn.astype(value.dtype) + output = ops.matmul(attn, value) + return output + + +class FlashAttn(nn.Cell): + r"""FlashAttention proposed in `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_. + + Args: + num_heads (int): The number of attention heads. + scale (float): The attention scale. + fa_dtype (mindspore.dtype, optional): FlashAttention compute dtype. Choose from `mstype.bfloat16`, + `mstype.float16`. Default: ``mstype.bfloat16``, indicates ``mindspore.bfloat16``. + + Inputs: + - **query** (Tensor) - Tensor with shape :math:`(batch\_size, num\_heads, sequence\_len, in\_channels)`. + - **key** (Tensor) - Tensor with shape :math:`(batch\_size, num\_heads, sequence\_len, in\_channels)`. + - **value** (Tensor) - Tensor with shape :math:`(batch\_size, num\_heads, sequence\_len, in\_channels)`. + - **mask** (Tensor, optional) - Tensor with shape :math:`(sequence\_len, sequence\_len)` or + or :math:`(batch\_size, 1, sequence\_len, sequence\_len)`. Default: ``None``. + + Outputs: + - **output** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import FlashAttn + >>> model = FlashAttn(num_heads=4, scale=0.25) + >>> in_shape = (2, 16, 32, 16) + >>> q, k, v = ops.rand(in_shape), ops.rand(in_shape), ops.rand(in_shape) + >>> mask_shape = (32, 32) + >>> mask = ops.randint(0, 2, mask_shape) + >>> output = model(q, k, v, mask) + >>> print(output.shape) + (2, 16, 32, 16) + """ + + def __init__(self, num_heads: int, scale: float, fa_dtype=mstype.bfloat16): + super().__init__() + self.fa_dtype = fa_dtype + self.num_heads = num_heads + self.scale = scale + + def construct(self, query: Tensor, key: Tensor, value: Tensor, mask: Optional[Tensor] = None): + query, key, value = query.astype(self.fa_dtype), key.astype(self.fa_dtype), value.astype(self.fa_dtype) + if mask is not None: + mask = mask.astype(mstype.uint8) + scores = ops.flash_attention_score(query, key, value, input_layout='BNSD', head_num=self.num_heads, + attn_mask=mask, scalar_value=self.scale) + return scores + + class MultiHeadAttention(Attention): r"""Multi Head Attention proposed in `Attention Is All You Need `_. Args: in_channels (int): The input channels. num_heads (int): The number of attention heads. + enable_flash_attn (bool): Whether use flash attention. FlashAttention only supports Ascend backend. + FlashAttention proposed in `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_. + Default: ``False``. + fa_dtype (mindspore.dtype): FlashAttention compute dtype. Choose from `mstype.bfloat16`, `mstype.float16`. + Default: ``mstype.bfloat16``, indicates ``mindspore.bfloat16``. drop_mode (str): Dropout method, ``dropout`` or ``droppath``. Default: ``dropout``. dropout_rate (float): The drop rate of dropout layer, greater than 0 and less equal than 1. Default: ``0.0``. compute_dtype (mindspore.dtype): Compute dtype. Default: ``mstype.float32``, indicates ``mindspore.float32``. Inputs: - **x** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. - - **attn_mask** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, sequence\_len)` or - :math:`(sequence\_len, sequence\_len)` or :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)`. - - **key_padding_mask** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len)` or - :math:`(batch\_size, sequence\_len, sequence\_len)` or - :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)`. + - **attn_mask** (Tensor, optional) - Tensor with shape :math:`(sequence\_len, sequence\_len)` or + or :math:`(batch\_size, 1, sequence\_len, sequence\_len)`. Default: ``None``. + - **key_padding_mask** (Tensor, optional) - Tensor with shape :math:`(batch\_size, sequence\_len)`. + Default: ``None``. Outputs: - **output** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. @@ -141,65 +209,59 @@ class MultiHeadAttention(Attention): >>> from mindflow.cell import MultiHeadAttention >>> model = MultiHeadAttention(in_channels=512, num_heads=4) >>> x = ops.rand((2, 32, 512)) - >>> mask_shape = (2, 4, 32, 32) + >>> mask_shape = (32, 32) >>> mask = ops.ones(mask_shape) >>> output = model(x, mask) >>> print(output.shape) (2, 32, 512) """ - def __init__(self, in_channels, - num_heads, - drop_mode="dropout", - dropout_rate=0.0, - compute_dtype=mstype.float32, + def __init__(self, in_channels: int, + num_heads: int, + enable_flash_attn: bool = False, + fa_dtype: mstype = mstype.bfloat16, + drop_mode: str = "dropout", + dropout_rate: float = 0.0, + compute_dtype: mstype = mstype.float32, ): super().__init__(in_channels, num_heads, compute_dtype) assert ( in_channels % num_heads == 0 ), "hidden channels must be divisible by number of heads" - self.scale = (in_channels // num_heads) ** -0.5 - self.proj = nn.Dense( - in_channels, in_channels, weight_init="XavierUniform" - ).to_float(compute_dtype) - + scale = (in_channels // num_heads) ** -0.5 + self.proj = nn.Dense(in_channels, in_channels).to_float(compute_dtype) + if enable_flash_attn: + print('use flash attention') + self.attn = FlashAttn(num_heads=num_heads, scale=scale, fa_dtype=fa_dtype) + else: + self.attn = ScaledDot(scale=scale) if drop_mode == "dropout": self.drop = nn.Dropout(p=dropout_rate) - self.attn_drop = nn.Dropout(p=dropout_rate) else: self.drop = DropPath(dropout_rate=dropout_rate) - self.attn_drop = DropPath(dropout_rate=dropout_rate) - def construct(self, x, attn_mask=None, key_padding_mask=None): + def construct(self, x: Tensor, attn_mask: Optional[Tensor] = None, key_padding_mask: Optional[Tensor] = None): """construct""" query, key, value = self.get_qkv(x) - scores = self.matmul(query, key.swapaxes(-1, -2)) * self.scale - scores = self._mask_scores(scores, attn_mask, key_padding_mask) - attn = self.softmax(scores, self.compute_dtype) - attn = self.attn_drop(attn) - output = self.matmul(attn, value) + mask = self.merge_mask(attn_mask, key_padding_mask) + output = self.attn(query, key, value, mask) + output = output.astype(mstype.float32) output = self._reshape_output(output) - output = self.proj(output) output = self.drop(output) return output -class Mlp(nn.Cell): - """Mlp""" - +class FeedForward(nn.Cell): + """FeedForward""" def __init__(self, in_channels, dropout_rate=0.0, compute_dtype=mstype.float16): super().__init__() - self.fc1 = nn.Dense( - in_channels, in_channels * 4, weight_init="XavierUniform" - ).to_float(compute_dtype) - self.fc2 = nn.Dense( - in_channels * 4, in_channels, weight_init="XavierUniform" - ).to_float(compute_dtype) + self.fc1 = nn.Dense(in_channels, in_channels * 4).to_float(compute_dtype) + self.fc2 = nn.Dense(in_channels * 4, in_channels).to_float(compute_dtype) self.act_fn = nn.GELU() self.dropout = nn.Dropout(p=dropout_rate) - def construct(self, x): + def construct(self, x: Tensor): """construct""" x = self.fc1(x) x = self.act_fn(x) @@ -209,21 +271,25 @@ class Mlp(nn.Cell): return x -class AttentionBlock(nn.Cell): - r""" - `AttentionBlock` comprises an `MultiHeadAttention` and an `MLP` layer. +class TransformerBlock(nn.Cell): + r""" `TransformerBlock` comprises an `MultiHeadAttention` and an `FeedForward` layer. Args: in_channels (int): The input channels. num_heads (int): The number of attention heads. + enable_flash_attn (bool): Whether use flash attention. FlashAttention only supports Ascend backend. + FlashAttention proposed in `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_. + Default: ``False``. + fa_dtype (mindspore.dtype): FlashAttention compute dtype. Choose from `mstype.bfloat16`, `mstype.float16`. + Default: ``mstype.bfloat16``, indicates ``mindspore.bfloat16``. drop_mode (str): Dropout method. Default: ``dropout``. Support ``dropout`` or ``droppath``. dropout_rate (float): The drop rate of dropout layer, greater than 0 and less equal than 1. Default: ``0.0``. compute_dtype (mindspore.dtype): Compute dtype. Default: ``mstype.float32``, indicates ``mindspore.float32``. Inputs: - **x** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. - - **mask** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, sequence\_len)` or - :math:`(sequence\_len, sequence\_len)` or :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)`. + - **mask** (Tensor, optional) - Tensor with shape :math:`(sequence\_len, sequence\_len)` or + :math:`(batch\_size, 1, sequence\_len, sequence\_len)`. Default: ``None``. Outputs: - **output** (Tensor) - Tensor with shape :math:`(batch\_size, sequence\_len, in\_channels)`. @@ -233,8 +299,8 @@ class AttentionBlock(nn.Cell): Examples: >>> from mindspore import ops - >>> from mindflow.cell import AttentionBlock - >>> model = AttentionBlock(in_channels=256, num_heads=4) + >>> from mindflow.cell import TransformerBlock + >>> model = TransformerBlock(in_channels=256, num_heads=4) >>> x = ops.rand((4, 100, 256)) >>> output = model(x) >>> print(output.shape) @@ -242,11 +308,13 @@ class AttentionBlock(nn.Cell): """ def __init__(self, - in_channels, - num_heads, - drop_mode="dropout", - dropout_rate=0.0, - compute_dtype=mstype.float32, + in_channels: int, + num_heads: int, + enable_flash_attn: bool = False, + fa_dtype: mstype = mstype.bfloat16, + drop_mode: str = "dropout", + dropout_rate: float = 0.0, + compute_dtype: mstype = mstype.float32, ): super().__init__() self.compute_dtype = compute_dtype @@ -256,7 +324,7 @@ class AttentionBlock(nn.Cell): self.ffn_norm = nn.LayerNorm([in_channels], epsilon=1e-6).to_float( mstype.float32 ) - self.ffn = Mlp( + self.ffn = FeedForward( in_channels=in_channels, dropout_rate=dropout_rate, compute_dtype=compute_dtype, @@ -264,12 +332,14 @@ class AttentionBlock(nn.Cell): self.attention = MultiHeadAttention( in_channels=in_channels, num_heads=num_heads, + enable_flash_attn=enable_flash_attn, + fa_dtype=fa_dtype, drop_mode=drop_mode, dropout_rate=dropout_rate, compute_dtype=compute_dtype, ) - def construct(self, x, mask=None): + def construct(self, x: Tensor, mask: Optional[Tensor] = None): """construct""" h = x x = self.attention_norm(x) diff --git a/MindFlow/mindflow/cell/diffusion_transformer.py b/MindFlow/mindflow/cell/diffusion_transformer.py index 98e94bd49..cdb08ffe4 100644 --- a/MindFlow/mindflow/cell/diffusion_transformer.py +++ b/MindFlow/mindflow/cell/diffusion_transformer.py @@ -19,11 +19,12 @@ import math import numpy as np from mindspore import nn, ops, Tensor from mindspore import dtype as mstype -from mindflow.cell import AttentionBlock +from mindflow.cell import TransformerBlock class Mlp(nn.Cell): """MLP""" + def __init__(self, in_channels, out_channels, dropout=0., compute_dtype=mstype.float32): super().__init__() self.fc1 = nn.Dense( @@ -44,6 +45,7 @@ class Mlp(nn.Cell): class SinusoidalPosEmb(nn.Cell): """sinusoidal embedding model""" + def __init__(self, dim, max_period=10000, compute_dtype=mstype.float32): super().__init__() half_dim = dim // 2 @@ -62,12 +64,13 @@ class SinusoidalPosEmb(nn.Cell): class Transformer(nn.Cell): """Transformer backbone model""" + def __init__(self, hidden_channels, layers, heads, compute_dtype=mstype.float32): super().__init__() self.hidden_channels = hidden_channels self.layers = layers self.blocks = nn.CellList([ - AttentionBlock( + TransformerBlock( in_channels=hidden_channels, num_heads=heads, drop_mode="dropout", diff --git a/MindFlow/mindflow/cell/vit.py b/MindFlow/mindflow/cell/vit.py index 5fcf6fa6e..f7c8e1181 100644 --- a/MindFlow/mindflow/cell/vit.py +++ b/MindFlow/mindflow/cell/vit.py @@ -22,7 +22,7 @@ from mindspore.common.initializer import initializer, XavierUniform import mindspore.common.dtype as mstype from .utils import to_2tuple, get_2d_sin_cos_pos_embed -from .attention import AttentionBlock +from .attention import TransformerBlock class PatchEmbedding(nn.Cell): @@ -132,7 +132,7 @@ class VitEncoder(nn.Cell): mstype.float32 ) for _ in range(depths): - layer = AttentionBlock( + layer = TransformerBlock( in_channels=hidden_channels, num_heads=num_heads, dropout_rate=dropout_rate, @@ -212,7 +212,7 @@ class VitDecoder(nn.Cell): mstype.float32 ) for _ in range(depths): - layer = AttentionBlock( + layer = TransformerBlock( in_channels=hidden_channels, num_heads=num_heads, dropout_rate=dropout_rate, diff --git a/tests/st/mindflow/cell/attention/test_attention.py b/tests/st/mindflow/cell/attention/test_attention.py index aceb878ba..7db0b58bf 100644 --- a/tests/st/mindflow/cell/attention/test_attention.py +++ b/tests/st/mindflow/cell/attention/test_attention.py @@ -21,7 +21,7 @@ import numpy as np from mindspore import Tensor, ops, load_checkpoint, load_param_into_net, jit_class, context from mindspore import dtype as mstype -from mindflow.cell import Attention, MultiHeadAttention, AttentionBlock, DropPath, ViT +from mindflow.cell import Attention, MultiHeadAttention, TransformerBlock, DropPath, ViT from mindflow.core import RelativeRMSELoss PROJECT_ROOT = os.path.abspath(os.path.join( @@ -44,48 +44,90 @@ def load_inputs(): @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -def test_attention_softmax_dtype(mode): +@pytest.mark.parametrize('compute_dtype', [mstype.float16, mstype.float32]) +def test_attention_qkv(mode, compute_dtype): """ - Feature: attention softmax - Description: test forward result dtype + Feature: attention + Description: test qkv dtype and shape Expectation: success """ context.set_context(mode=mode) - net = Attention(IN_CHANNELS, NUM_HEADS, compute_dtype=mstype.float32) - x, _ = load_inputs() - net_scores_32 = net.softmax(x, mstype.float32) - net_scores_16 = net.softmax(x, mstype.float16) - assert net_scores_16.dtype == mstype.float16 - compare_output(net_scores_32.numpy(), - net_scores_16.numpy(), FP32_RTOL, FP32_ATOL) + net = Attention(IN_CHANNELS, NUM_HEADS, compute_dtype=compute_dtype) + x = ops.randn((BATCH_SIZE, SEQ_LEN, IN_CHANNELS)) + qkv = net.get_qkv(x) + for tensor in qkv: + assert tensor.dtype == compute_dtype + assert tensor.shape == (BATCH_SIZE, NUM_HEADS, SEQ_LEN, IN_CHANNELS//NUM_HEADS) @pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -def test_attention_dtype(mode): +@pytest.mark.parametrize('fa_dtype', [mstype.float16, mstype.bfloat16]) +def test_flash_attn(mode, fa_dtype): """ - Feature: attention - Description: test forward result dtype + Feature: FlashAttn + Description: test forward result Expectation: success """ context.set_context(mode=mode) - net = Attention(IN_CHANNELS, NUM_HEADS, compute_dtype=mstype.float16) - x, _ = load_inputs() - q, k, v = net.get_qkv(x) - assert q.dtype == mstype.float16 - assert k.dtype == mstype.float16 - assert v.dtype == mstype.float16 + in_shape = (BATCH_SIZE, NUM_HEADS, SEQ_LEN, IN_CHANNELS//NUM_HEADS) + query, key, value = ops.randn(in_shape), ops.randn(in_shape), ops.randn(in_shape) + mask = ops.randint(0, 2, (SEQ_LEN, SEQ_LEN)) + net = MultiHeadAttention(IN_CHANNELS, NUM_HEADS, enable_flash_attn=True, fa_dtype=fa_dtype) + output = net.attn(query, key, value, mask) + assert output.dtype == fa_dtype + assert output.shape == in_shape -# pylint: disable=W0212 @pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -@pytest.mark.parametrize('compute_dtype', [mstype.float16, mstype.float32]) -def test_attention_mask1(mode, compute_dtype): +@pytest.mark.parametrize('fa_dtype', [mstype.float16, mstype.bfloat16]) +def test_multihead_fa(mode, fa_dtype): + """ + Feature: FlashAttention + Description: test forward result + Expectation: success + """ + context.set_context(mode=mode) + net = MultiHeadAttention(IN_CHANNELS, NUM_HEADS, enable_flash_attn=True, fa_dtype=fa_dtype) + in_shape = (BATCH_SIZE, SEQ_LEN, IN_CHANNELS) + x = ops.randn(in_shape) + mask = ops.randint(0, 2, (BATCH_SIZE, 1, SEQ_LEN, SEQ_LEN)) + output = net(x, mask) + assert output.dtype == mstype.float32 + assert output.shape == in_shape + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) +@pytest.mark.parametrize('fa_dtype', [mstype.float16, mstype.bfloat16]) +def test_fa_forward(mode, fa_dtype): + """ + Feature: FlashAttention + Description: test FlashAttention forward result + Expectation: success + """ + context.set_context(mode=mode) + net = MultiHeadAttention(IN_CHANNELS, NUM_HEADS, enable_flash_attn=False) + fa_net = MultiHeadAttention(IN_CHANNELS, NUM_HEADS, enable_flash_attn=True, fa_dtype=fa_dtype) + batch_size, seq_len = 256, 512 + in_shape = (batch_size, seq_len, IN_CHANNELS) + x = ops.randn(in_shape) + mask = ops.randint(0, 2, (batch_size, 1, seq_len, seq_len)) + validate_checkpoint(net, fa_net, (x, mask), FP32_RTOL, FP32_ATOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) +def test_attention_mask1(mode): """ Feature: attention Description: test attention mask function @@ -93,41 +135,35 @@ def test_attention_mask1(mode, compute_dtype): """ context.set_context(mode=mode) net = Attention(IN_CHANNELS, NUM_HEADS, compute_dtype=mstype.float16) - scores = ops.randn([BATCH_SIZE, NUM_HEADS, SEQ_LEN, SEQ_LEN], dtype=compute_dtype) - attn_mask = ops.randint(0, 2, (BATCH_SIZE, SEQ_LEN, SEQ_LEN)) + attn_mask = ops.randint(0, 2, (SEQ_LEN, SEQ_LEN)) key_padding_mask = ops.randint(0, 2, (BATCH_SIZE, SEQ_LEN)) - y = net._mask_scores(scores, attn_mask, key_padding_mask) - assert y.shape == (BATCH_SIZE, NUM_HEADS, SEQ_LEN, SEQ_LEN) - assert y.dtype == compute_dtype + mask = net.merge_mask(attn_mask, key_padding_mask) + assert mask.shape == (BATCH_SIZE, 1, SEQ_LEN, SEQ_LEN) -# pylint: disable=W0212 @pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -@pytest.mark.parametrize('compute_dtype', [mstype.float16, mstype.float32]) -def test_attention_mask2(mode, compute_dtype): +def test_attention_mask2(mode): """ Feature: attention Description: test attention mask function Expectation: success """ context.set_context(mode=mode) - net = Attention(IN_CHANNELS, NUM_HEADS, compute_dtype=mstype.float16) - scores = ops.randn([BATCH_SIZE, NUM_HEADS, SEQ_LEN, SEQ_LEN], dtype=compute_dtype) - attn_mask = ops.randint(0, 2, (SEQ_LEN, SEQ_LEN)) - key_padding_mask = ops.randint(0, 2, (BATCH_SIZE, SEQ_LEN, SEQ_LEN)) - y = net._mask_scores(scores, attn_mask, key_padding_mask) - assert y.shape == (BATCH_SIZE, NUM_HEADS, SEQ_LEN, SEQ_LEN) - assert y.dtype == compute_dtype + net = Attention(IN_CHANNELS, NUM_HEADS) + attn_mask = ops.randint(0, 2, (BATCH_SIZE, 1, SEQ_LEN, SEQ_LEN)) + key_padding_mask = ops.randint(0, 2, (BATCH_SIZE, SEQ_LEN)) + mask = net.merge_mask(attn_mask, key_padding_mask) + assert mask.shape == (BATCH_SIZE, 1, SEQ_LEN, SEQ_LEN) @pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -def test_multihead_attention_multi_dtype(mode): +def test_multihead_attention_forward(mode): """ Feature: MultiHeadAttention Description: test result dtype @@ -163,17 +199,18 @@ def test_multihead_attention(mode): @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) -def test_multihead_attention_dtype(mode): +@pytest.mark.parametrize('compute_dtype', [mstype.float16, mstype.bfloat16]) +def test_multihead_attention_dtype(mode, compute_dtype): """ Feature: MultiHeadAttention Description: test forward result dtype Expectation: success """ context.set_context(mode=mode) - net_16 = MultiHeadAttention( - in_channels=IN_CHANNELS, num_heads=NUM_HEADS, compute_dtype=mstype.float16) + net = MultiHeadAttention( + in_channels=IN_CHANNELS, num_heads=NUM_HEADS, compute_dtype=compute_dtype) x, mask = load_inputs() - validate_output_dtype(net_16, (x, mask), mstype.float16) + validate_output_dtype(net, (x, mask), compute_dtype) @pytest.mark.level0 @@ -182,12 +219,12 @@ def test_multihead_attention_dtype(mode): @pytest.mark.parametrize('mode', [context.GRAPH_MODE, context.PYNATIVE_MODE]) def test_attn_block(mode): """ - Feature: AttentionBlock + Feature: TransformerBlock Description: test forward result Expectation: success """ context.set_context(mode=mode) - net = AttentionBlock(in_channels=IN_CHANNELS, num_heads=NUM_HEADS) + net = TransformerBlock(in_channels=IN_CHANNELS, num_heads=NUM_HEADS) x, mask = load_inputs() validate_model_infer(net, (x, mask), './attention_block.ckpt', './attention_block_output.npy', FP32_RTOL, FP32_ATOL) diff --git a/tests/st/mindflow/cell/test_optimizers.py b/tests/st/mindflow/cell/test_optimizers.py index 545f31da7..1882e36df 100644 --- a/tests/st/mindflow/cell/test_optimizers.py +++ b/tests/st/mindflow/cell/test_optimizers.py @@ -24,8 +24,8 @@ import numpy as np import mindspore as ms from mindspore import ops, set_seed, nn from mindspore import dtype as mstype -from mindflow import UNet2D, AttentionBlock, AdaHessian -from mindflow.cell.attention import Mlp +from mindflow import UNet2D, TransformerBlock, AdaHessian +from mindflow.cell.attention import FeedForward from mindflow.cell.unet2d import Down PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) @@ -44,6 +44,7 @@ random.seed(0) class TestAdaHessianAccuracy(AdaHessian): ''' Child class for testing the accuracy of AdaHessian optimizer ''' + def gen_rand_vecs(self, grads): ''' generate certain vector for accuracy test ''' return [ms.Tensor(np.arange(p.size).reshape(p.shape) - p.size // 2, dtype=ms.float32) for p in grads] @@ -51,6 +52,7 @@ class TestAdaHessianAccuracy(AdaHessian): class TestUNet2D(UNet2D): ''' Child class for testing optimizing UNet with AdaHessian ''' + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -70,16 +72,17 @@ class TestUNet2D(UNet2D): activation=self.activation, enable_bn=self.enable_bn)) -class TestAttentionBlock(AttentionBlock): +class TestAttentionBlock(TransformerBlock): ''' Child class for testing optimizing Attention with AdaHessian ''' + def __init__(self, in_channels, num_heads, drop_mode="dropout", dropout_rate=0.0, compute_dtype=mstype.float16): super().__init__( in_channels, num_heads, drop_mode=drop_mode, dropout_rate=dropout_rate, compute_dtype=compute_dtype) - class TestMlp(Mlp): + class TestMlp(FeedForward): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.act_fn = nn.ReLU() # replace `gelu` with `relu` to avoid `vjp` problem + self.act_fn = nn.ReLU() # replace `gelu` with `relu` to avoid `vjp` problem self.ffn = TestMlp(in_channels=in_channels, dropout_rate=dropout_rate, compute_dtype=compute_dtype) @@ -125,6 +128,7 @@ def test_adahessian_accuracy(mode): relative_error = np.max(np.abs(outputs - outputs_ref)) / np.max(np.abs(outputs_ref)) assert relative_error < FP32_RTOL, "The verification of adahessian accuracy is not successful." + @pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @@ -174,6 +178,7 @@ def test_adahessian_st(mode, model_option): assert ops.isfinite(loss) + @pytest.mark.level1 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @@ -181,7 +186,7 @@ def test_adahessian_compare(): """ Feature: AdaHessian compare with Adam Description: Compare the algorithm results of the AdaHessian optimizer with Adam. - The code runs in PYNATIVE_MODE and the network under comparison is AttentionBlock. + The code runs in PYNATIVE_MODE and the network under comparison is TransformerBlock. The optimization runs 100 rounds to demonstrate an essential loss decrease. Expectation: The loss of AdaHessian outperforms Adam by 20% under the same configuration on an Attention network. """ -- Gitee From 044be6515030ed9a3c385ffea42f4ded2fac08e6 Mon Sep 17 00:00:00 2001 From: brian Date: Thu, 29 May 2025 17:08:36 +0800 Subject: [PATCH 03/30] mod docs --- .../cell/mindflow.cell.MultiHeadAttention.rst | 12 +++++++----- ...lock.rst => mindflow.cell.TransformerBlock.rst} | 14 ++++++++------ docs/api_python/mindflow/mindflow.cell.rst | 2 +- docs/api_python_en/mindflow/mindflow.cell.rst | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) rename docs/api_python/mindflow/cell/{mindflow.cell.AttentionBlock.rst => mindflow.cell.TransformerBlock.rst} (43%) diff --git a/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst b/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst index 145c1e24c..419893510 100644 --- a/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst +++ b/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst @@ -1,23 +1,25 @@ mindflow.cell.MultiHeadAttention ================================= -.. py:class:: mindflow.cell.MultiHeadAttention(in_channels, num_heads, drop_mode='dropout', dropout_rate=0.0, compute_dtype=mstype.float32) +.. py:class:: mindflow.cell.MultiHeadAttention(in_channels, num_heads, enable_flash_attn=False, fa_dtype=mstype.bfloat16, drop_mode='dropout', dropout_rate=0.0, compute_dtype=mstype.float32) 多头注意力机制,具体细节可以参见 `Attention Is All You Need `_ 。 参数: - **in_channels** (int) - 输入的输入特征维度。 - **num_heads** (int) - 输出的输出特征维度。 + - **enable_flash_attn** (bool) - 是否使能FlashAttention。FlashAttention只支持 `Ascend` 后端。具体细节参见 `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_ 。 + 默认值: ``False`` 。 + - **fa_dtype** (mindspore.dtype): FlashAttention计算类型。支持以下类型: `mstype.bfloat16`、 `mstype.float16`。默认值: ``mstype.bfloat16`` ,表示 ``mindspore.bfloat16`` 。 - **drop_mode** (str) - dropout方式。默认值: ``dropout`` 。支持以下类型: ``dropout`` 和 ``droppath`` 。 - **dropout_rate** (float) - dropout层丢弃的比率。取值在 `[0, 1]` 。默认值: ``0.0`` 。 - **compute_dtype** (mindspore.dtype) - 网络层的数据类型。默认值: ``mstype.float32`` ,表示 ``mindspore.float32`` 。 输入: - **x** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, in\_channels)` 的Tensor。 - - **attn_mask** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, sequence\_len)` 或 - :math:`(sequence\_len, sequence\_len)` or :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)` 的Tensor. - - **key_padding_mask** (Tensor) - shape为 :math:`(batch\_size, sequence\_len)` 或 - :math:`(batch\_size, sequence\_len, sequence\_len)` 或 :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)` 的Tensor. + - **attn_mask** (Tensor,可选) - shape为 :math:`(sequence\_len, sequence\_len)` 或 + :math:`(batch\_size, 1, sequence\_len, sequence\_len)` 的Tensor。默认值: ``None`` 。 + - **key_padding_mask** (Tensor,可选) - shape为 :math:`(batch\_size, sequence\_len)` 的Tensor。默认值: ``None`` 。 输出: - **output** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, in\_channels)` 的Tensor。 diff --git a/docs/api_python/mindflow/cell/mindflow.cell.AttentionBlock.rst b/docs/api_python/mindflow/cell/mindflow.cell.TransformerBlock.rst similarity index 43% rename from docs/api_python/mindflow/cell/mindflow.cell.AttentionBlock.rst rename to docs/api_python/mindflow/cell/mindflow.cell.TransformerBlock.rst index 5791d69b5..574b01e5d 100644 --- a/docs/api_python/mindflow/cell/mindflow.cell.AttentionBlock.rst +++ b/docs/api_python/mindflow/cell/mindflow.cell.TransformerBlock.rst @@ -1,21 +1,23 @@ -mindflow.cell.AttentionBlock -============================ +mindflow.cell.TransformerBlock +====================================== -.. py:class:: mindflow.cell.AttentionBlock(in_channels, num_heads, drop_mode='dropout', dropout_rate=0.0, compute_dtype=mstype.float32) +.. py:class:: mindflow.cell.TransformerBlock(in_channels, num_heads, enable_flash_attn=False, fa_dtype=mstype.bfloat16, drop_mode='dropout', dropout_rate=0.0, compute_dtype=mstype.float32) - `AttentionBlock` 包含 `MultiHeadAttention` 和 `MLP` 网络堆叠而成。 + `TransformerBlock` 包含 `MultiHeadAttention` 和 `FeedForward` 网络堆叠而成。 参数: - **in_channels** (int) - 输入的输入特征维度。 - **num_heads** (int) - 输出的输出特征维度。 + - **enable_flash_attn** (bool) - 是否使能FlashAttention。FlashAttention只支持 `Ascend` 后端。具体细节参见 `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_ 。 + 默认值: ``False`` 。 + - **fa_dtype** (mindspore.dtype): FlashAttention计算类型。支持以下类型: `mstype.bfloat16`、 `mstype.float16`。默认值: ``mstype.bfloat16`` ,表示 ``mindspore.bfloat16`` 。 - **drop_mode** (str) - dropout方式。默认值: ``dropout`` 。支持以下类型: ``dropout`` 和 ``droppath`` 。 - **dropout_rate** (float) - dropout层丢弃的比率,在 ``[0, 1]`` 范围。默认值: ``0.0`` 。 - **compute_dtype** (mindspore.dtype) - 网络层的数据类型。默认值: ``mstype.float32`` ,表示 ``mindspore.float32`` 。 输入: - **x** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, in\_channels)` 的Tensor。 - - **mask** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, sequence\_len)` 或 - :math:`(sequence\_len, sequence\_len)` 或 :math:`(batch\_size, num_heads, sequence\_len, sequence\_len)` 的Tensor. + - **mask** (Tensor) - shape为 :math:`(sequence\_len, sequence\_len)` 或 :math:`(batch\_size, 1, sequence\_len, sequence\_len)` 的Tensor. 输出: - **output** (Tensor) - shape为 :math:`(batch\_size, sequence\_len, in\_channels)` 的Tensor。 diff --git a/docs/api_python/mindflow/mindflow.cell.rst b/docs/api_python/mindflow/mindflow.cell.rst index eb677b002..c52ce4ceb 100644 --- a/docs/api_python/mindflow/mindflow.cell.rst +++ b/docs/api_python/mindflow/mindflow.cell.rst @@ -6,7 +6,6 @@ mindflow.cell :nosignatures: :template: classtemplate.rst - mindflow.cell.AttentionBlock mindflow.cell.ConditionDiffusionTransformer mindflow.cell.DiffusionTrainer mindflow.cell.DiffusionTransformer @@ -29,6 +28,7 @@ mindflow.cell mindflow.cell.SNO1D mindflow.cell.SNO2D mindflow.cell.SNO3D + mindflow.cell.TransformerBlock mindflow.cell.UNet2D mindflow.cell.ViT mindflow.cell.get_activation diff --git a/docs/api_python_en/mindflow/mindflow.cell.rst b/docs/api_python_en/mindflow/mindflow.cell.rst index f4136a8e7..76791f5cf 100644 --- a/docs/api_python_en/mindflow/mindflow.cell.rst +++ b/docs/api_python_en/mindflow/mindflow.cell.rst @@ -6,7 +6,6 @@ mindflow.cell :nosignatures: :template: classtemplate.rst - mindflow.cell.AttentionBlock mindflow.cell.ConditionDiffusionTransformer mindflow.cell.DiffusionTrainer mindflow.cell.DiffusionTransformer @@ -29,6 +28,7 @@ mindflow.cell mindflow.cell.SNO1D mindflow.cell.SNO2D mindflow.cell.SNO3D + mindflow.cell.TransformerBlock mindflow.cell.UNet2D mindflow.cell.ViT mindflow.cell.get_activation -- Gitee From 435ce7964167343e53301ea3b41a92c3c90c5100 Mon Sep 17 00:00:00 2001 From: wangqc <1160619743@qq.com> Date: Tue, 3 Jun 2025 13:36:00 +0800 Subject: [PATCH 04/30] fix README --- MindChemistry/applications/crystalflow/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/MindChemistry/applications/crystalflow/README.md b/MindChemistry/applications/crystalflow/README.md index bead72bb0..afdea75c1 100644 --- a/MindChemistry/applications/crystalflow/README.md +++ b/MindChemistry/applications/crystalflow/README.md @@ -15,7 +15,7 @@ ## 快速入门 > 1. 将Mindchemistry/mindchemistry文件包下载到当前目录 -> 2. 在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/diffcsp/)下载相应的数据集 +> 2. 在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/diffcsp/dataset/)下载相应的数据集 > 3. 安装依赖包:`pip install -r requirement.txt` > 4. 训练命令: `python train.py` > 5. 预测命令: `python evaluate.py` @@ -54,7 +54,7 @@ applications ## 下载数据集 -在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/diffcsp/)中下载相应的数据集文件夹和dataset_prop.txt数据集属性文件放置于当前路径的dataset文件夹下(如果没有需要自己手动创建),文件路径参考: +在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/diffcsp/dataset/)中下载相应的数据集文件夹和dataset_prop.txt数据集属性文件放置于当前路径的dataset文件夹下(如果没有需要自己手动创建),文件路径参考: ```txt crystalflow @@ -87,8 +87,6 @@ python train.py ### 推理 -将权重的path写入config文件的checkpoint.last_path中。预训练模型可以从[预训练模型链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/diffcsp/pre-train)中获取。 - 更改config文件中的test字段来更改推理参数,特别是test.num_eval,它**决定了对于每个组分生成多少个样本**,对于后续的评估阶段很重要。 ```bash -- Gitee From 721c557fd269bea55928d258e5824fa0ec544cec Mon Sep 17 00:00:00 2001 From: wangqc <1160619743@qq.com> Date: Thu, 12 Jun 2025 14:09:54 +0800 Subject: [PATCH 05/30] feat: add the link of ckpt in test --- .../applications/crystalflow/test_crystalflow.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/MindChemistry/applications/crystalflow/test_crystalflow.py b/MindChemistry/applications/crystalflow/test_crystalflow.py index ffce4881f..0f9531b13 100644 --- a/MindChemistry/applications/crystalflow/test_crystalflow.py +++ b/MindChemistry/applications/crystalflow/test_crystalflow.py @@ -1,6 +1,7 @@ """model test""" import math import os +import urllib.request import mindspore as ms import mindspore.numpy as mnp @@ -33,6 +34,10 @@ class SinusoidalTimeEmbeddings(nn.Cell): (ops.Sin()(embeddings), ops.Cos()(embeddings))) return embeddings +def download_file(url, filename): + urllib.request.urlretrieve(url, filename) + print(f"File downloaded successfully: {filename}") + def test_cspnet(): """test cspnet.py""" ms.set_seed(1234) @@ -153,7 +158,8 @@ def test_loss(): cspnet = CSPNet(num_layers=6, hidden_dim=512, num_freqs=256) cspflow = CSPFlow(cspnet) - mindspore_ckpt = load_checkpoint("./torch2ms_ckpt/ms_flow.ckpt") + download_file('https://download-mindspore.osinfra.cn/mindscience/mindchemistry/crystalflow/ms_flow.ckpt', 'ms_flow.ckpt') + mindspore_ckpt = load_checkpoint("ms_flow.ckpt") load_param_into_net(cspflow, mindspore_ckpt) loss_func_mse = L2LossMask(reduction='mean') -- Gitee From 5adc891e1c0b31b1a46580c973889c950d57360f Mon Sep 17 00:00:00 2001 From: wangbo Date: Thu, 19 Jun 2025 16:02:50 +0800 Subject: [PATCH 06/30] ufold_debug --- MindSPONGE/src/mindsponge/pipeline/models/ufold/ufold.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MindSPONGE/src/mindsponge/pipeline/models/ufold/ufold.py b/MindSPONGE/src/mindsponge/pipeline/models/ufold/ufold.py index 5e45acd64..742e61dbf 100644 --- a/MindSPONGE/src/mindsponge/pipeline/models/ufold/ufold.py +++ b/MindSPONGE/src/mindsponge/pipeline/models/ufold/ufold.py @@ -143,7 +143,7 @@ class UFold(Model): seq_embedding_batch = Tensor(ops.Cast()(seq_embeddings, mstype.float32)) pred_contacts = self.network(seq_embedding_batch) contact_masks = ops.ZerosLike()(pred_contacts) - contact_masks[:, :seq_lens.item(0), :seq_lens.item(0)] = 1 + contact_masks[:, :seq_lens[0].item(), :seq_lens[0].item()] = 1 contact_masks = contact_masks.astype(ms.float32) feat = [seq_embedding_batch, contact_masks, contacts_batch] feat = mutable(feat) -- Gitee From 47c582852a5a4f142446a3a9a0660329d323514c Mon Sep 17 00:00:00 2001 From: wangbo Date: Thu, 19 Jun 2025 15:51:15 +0800 Subject: [PATCH 07/30] esmif1_debug --- .../src/mindsponge/pipeline/models/esm_if1/module/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MindSPONGE/src/mindsponge/pipeline/models/esm_if1/module/features.py b/MindSPONGE/src/mindsponge/pipeline/models/esm_if1/module/features.py index a164be032..9fb79ddd1 100644 --- a/MindSPONGE/src/mindsponge/pipeline/models/esm_if1/module/features.py +++ b/MindSPONGE/src/mindsponge/pipeline/models/esm_if1/module/features.py @@ -147,7 +147,7 @@ class GVPInputFeaturizer(nn.Cell): d_neighbors, e_idx = ops.Sort(axis=-1, descending=True)(d_adjust) else: d_neighbors, e_idx = ops.TopK(sorted=True)(d_adjust, d_adjust.shape[-1]) - d_neighbors, e_idx = d_neighbors[..., ::-1], e_idx[..., ::-1] + d_neighbors, e_idx = ms.mint.flip(d_neighbors, [-1]), ms.mint.flip(e_idx, [-1]) d_neighbors, e_idx = d_neighbors[:, :, 0:int(min(top_k_neighbors, x.shape[1]))], \ e_idx[:, :, 0:int(min(top_k_neighbors, x.shape[1]))] d_neighbors = ms.Tensor(d_neighbors, ms.float32)*1e4 -- Gitee From 1a48820c47e30dd4949c8d9d7be24ed6dcb93101 Mon Sep 17 00:00:00 2001 From: l30062829 Date: Thu, 12 Jun 2025 15:12:24 +0800 Subject: [PATCH 08/30] add GTEAM trainpart --- .../applications/earthquake/G-TEAM/README.md | 18 +- .../earthquake/G-TEAM/README_CN.md | 19 +- .../earthquake/G-TEAM/config/GTEAM.yaml | 40 +- .../earthquake/G-TEAM/images/train_loss.png | Bin 0 -> 43090 bytes .../applications/earthquake/G-TEAM/main.py | 22 +- .../earthquake/G-TEAM/src/data.py | 842 +++++++++--------- .../earthquake/G-TEAM/src/forcast.py | 380 +++++++- .../earthquake/G-TEAM/src/models.py | 95 +- .../earthquake/G-TEAM/src/utils.py | 89 +- .../earthquake/G-TEAM/src/visual.py | 1 - 10 files changed, 1005 insertions(+), 501 deletions(-) create mode 100644 MindEarth/applications/earthquake/G-TEAM/images/train_loss.png diff --git a/MindEarth/applications/earthquake/G-TEAM/README.md b/MindEarth/applications/earthquake/G-TEAM/README.md index fde491dff..fc37db4f3 100644 --- a/MindEarth/applications/earthquake/G-TEAM/README.md +++ b/MindEarth/applications/earthquake/G-TEAM/README.md @@ -32,7 +32,7 @@ The model is trained using the [Diting Dataset 2.0 - Multifunctional Large AI Tr - Only retains initial P-wave and S-wave phases - Includes events recorded by ≥3 stations for reliability -The inference module has been open-sourced and supports prediction using provided checkpoint files (.ckpt). +This model has fully open-sourced both the inference and training modules. For the inference part, the provided [ckpt](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/) is used for inference, while the training part utilizes the provided [hdf5](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/) and [pkl](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/) files for training. ## Quick Start @@ -41,6 +41,9 @@ You can download the required data and ckpt files for training and inference at ### Execution Run via command line using the `main` script: +It is necessary to configure the istraining parameter in the config.yaml file in advance to set up inference or training: +istraining: false -- Inference +istraining: true -- Training ```python python main.py --cfg_path ./config/config.yaml --device_id 0 --device_target Ascend @@ -52,13 +55,15 @@ Parameters: --device_target: Hardware type (default: Ascend) --device_id: Device ID (default: 0) +### Inference + ### Visualization ![](./images/pga.png) Scatter plot compares predicted vs actual PGA values (x-axis vs y-axis). Closer alignment to y=x line indicates higher accuracy. -### 结果展示 +### Results Presentation | Parameter | NPU | |:----------------------:|:--------------------------:| @@ -73,8 +78,15 @@ Scatter plot compares predicted vs actual PGA values (x-axis vs y-axis). Closer | Inference Resource | 1NPU | | Inference Speed(ms/step) | 556 | +### Training + +### Results Presentation + +![](./images/train_loss.png) +Under normal circumstances, the Average Training Loss should continue to converge. + ## Contributors -gitee id: chengjie, longjundong, xujiabao, dinghongyang, funfunplus +gitee id: xujiabao, longjundong, dinghongyang, chengjie email: funniless@163.com \ No newline at end of file diff --git a/MindEarth/applications/earthquake/G-TEAM/README_CN.md b/MindEarth/applications/earthquake/G-TEAM/README_CN.md index 5bb7eff96..3475aa849 100644 --- a/MindEarth/applications/earthquake/G-TEAM/README_CN.md +++ b/MindEarth/applications/earthquake/G-TEAM/README_CN.md @@ -15,7 +15,7 @@ 本模型的训练数据来源于[谛听数据集2.0 -中国地震台网多功能大型人工智能训练数据集](http://www.esdc.ac.cn/article/137),该数据集汇集了中国大陆及其邻近地区(15°-50°N,65°-140°E)1177 个中国地震台网固定台站的波形记录,覆盖时间范围为 2020 年 3 月至 2023 年 2 月。数据集包含研究区域内所有震级大于 0 的地方震事件,共计 264,298 个。我们在训练过程中仅选取了初至 P 波和 S 波震相,并且只保留至少被三个台站记录到的地震事件,以确保数据的可靠性和稳定性。 -目前本模型已开源推理部分,可使用提供的[ckpt](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/)进行推理。 +本模型已全部开源推理和训练模块,其中推理部分使用提供的[ckpt](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/)进行推理,训练部分使用提供的[hdf5](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/)和[pkl](https://download-mindspore.osinfra.cn/mindscience/mindearth/dataset/G-TEAM/)进行训练。 ## 快速开始 @@ -23,7 +23,9 @@ ### 运行方式: 在命令行调用`main`脚本 -### 推理 +需提前在config.yaml中配置istraining参数设定推理/训练 +istraining: false -- 推理 +istraining: true -- 训练 ```python @@ -33,7 +35,9 @@ python main.py --cfg_path ./config/config.yaml --device_id 0 --device_target Asc 其中, --cfg_path表示配置文件路径,默认值"./config/config.yaml" --device_target 表示设备类型,默认Ascend。 --device_id 表示运行设备的编号,默认值0。 -### 结果可视化 +### 推理 + +### 可视化结果 ![](./images/pga.png) @@ -54,8 +58,15 @@ python main.py --cfg_path ./config/config.yaml --device_id 0 --device_target Asc | 推理资源 | 1NPU | | 推理速度(ms/step) | 556 | +### 训练 + +### 结果展示 + +![](./images/train_loss.png) +正常情况Average Training Loss会持续收敛。 + ## 贡献者 -gitee id: chengjie, longjundong, xujiabao, dinghongyang, funfunplus +gitee id: xujiabao, longjundong, dinghongyang, chengjie email: funniless@163.com \ No newline at end of file diff --git a/MindEarth/applications/earthquake/G-TEAM/config/GTEAM.yaml b/MindEarth/applications/earthquake/G-TEAM/config/GTEAM.yaml index 68c066929..0faf89c09 100644 --- a/MindEarth/applications/earthquake/G-TEAM/config/GTEAM.yaml +++ b/MindEarth/applications/earthquake/G-TEAM/config/GTEAM.yaml @@ -1,4 +1,6 @@ -model: +model: + istraining: false + use_mlp: False hidden_dim: 1000 hidden_dropout: 0.0 n_heads: 10 @@ -13,6 +15,7 @@ model: pga: true mode: test no_event_token : False + max_stations: 5 data: root_dir: "./dataset" batch_size: 64 @@ -35,6 +38,39 @@ data: waveform_shape: [3000, 3] overwrite_sampling_rate: None noise_seconds: 5 +training_params: + seed: 42 + clipnorm: 1.0 + data_path: ./diting2_2020-2022_sc_abridged.hdf5 + ensemble_rotation: true + epochs_full_model: 100 + epochs_single_station: 5 + filter_single_station_by_pick: true + generator_params: + - batch_size: 1 + cutout_end: 25 + cutout_start: -1 + disable_station_foreshadowing: true + key: Mag + magnitude_resampling: 1.5 + min_upsample_magnitude: 4 + pga_from_inactive: true + pga_key: pga + pga_selection_skew: 1000 + pos_offset: [30,102] + scale_metadata: false + selection_skew: 1000 + shuffle_train_dev: true + transform_target_only: false + translate: false + trigger_based: true + upsample_high_station_events: 10 + loss_weights: + location: 1 + magnitude: 0.3 + pga: 1 + lr: 1e-5 + workers: 1 summary: summary_dir: "./summary" - ckpt_path: "./dataset/ckpt/g_team.ckpt" + ckpt_path: "./dataset/ckpt/g_team.ckpt" \ No newline at end of file diff --git a/MindEarth/applications/earthquake/G-TEAM/images/train_loss.png b/MindEarth/applications/earthquake/G-TEAM/images/train_loss.png new file mode 100644 index 0000000000000000000000000000000000000000..77ee77fa11f4db7eec0504a01a2e0acbac987697 GIT binary patch literal 43090 zcmeFa2UwHYwlEwO8v;s4q-cZyp-7h^FiJx2N$5!L9U>|SC<>U+dlQjPLhk_tL_j){ zP^4EWp*dmzrT&?@B0C0l*12~=lC<4w?-TXU?8IbB^*7CFS|^lngW%sV*@yu&^*QFfp;R zb91s@xz5JKbXDN$br269nC~*Dps*mXFgGtB?=MJBoIQJ%@*E{SB_%yCD-$d4Km9p= z1E4*3^3tiJQzw9cle8yJ(VjSN1F(~aN*?T~Uw-}eA&>GD#krFw&Xd80mjEYDQk*

az?-_3mFGkyJ(2gG#AM#_ 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 4) * np.random.randn(y.shape[0]).reshape(y.shape) * (y - 4) * 0.05 + + return (ms.tensor(x, dtype=ms.float32), + ms.tensor(np.expand_dims(np.expand_dims(y, axis=1), axis=2), dtype=ms.float32)) + class EarthquakeDataset(Dataset): """ Dataset class for loading and processing seismic event data. @@ -105,7 +204,7 @@ class EarthquakeDataset(Dataset): **kwargs, ): - super(EarthquakeDataset, self).__init__() + super().__init__() self.data_path = data_path self.event_key = event_key @@ -199,421 +298,343 @@ class EarthquakeDataset(Dataset): if self.shuffle: np.random.shuffle(self.indexes) - -class DataProcessor: +class PreloadedEventGenerator(Dataset): """ - A data processor for seismic event analysis that handles waveform preprocessing, - station selection, and target preparation for machine learning models. - Key functionalities: - Batch processing of seismic waveforms and metadata - Station selection strategies for efficient processing - Multiple preprocessing techniques (cutout, integration, etc.) - Coordinate transformations and target preparations - PGA (Peak Ground Acceleration) target handling - Data augmentation techniques (label smoothing, station blinding) + A custom dataset generator for preloading seismic events. """ + def __init__(self, data_path, event_key, data, event_metadata, waveform_shape=(3000, 6), key='MA', batch_size=32, + cutout=None, sliding_window=False, windowlen=3000, shuffle=True, coords_target=True, oversample=1, + pos_offset=(-21, -69), label_smoothing=False, station_blinding=False, magnitude_resampling=3, + pga_targets=None, adjust_mean=True, transform_target_only=False, max_stations=None, trigger_based=None, + min_upsample_magnitude=2, disable_station_foreshadowing=False, selection_skew=None, + pga_from_inactive=False, integrate=False, differentiate=False, sampling_rate=100., select_first=False, + fake_borehole=False, scale_metadata=True, pga_key='pga', pga_mode=False, p_pick_limit=5000, + coord_keys=None, upsample_high_station_events=None, no_event_token=False, pga_selection_skew=None, + **kwargs): + ''' + Initializes the PreloadedEventGenerator. - def __init__( - self, - waveform_shape=(3000, 6), - max_stations=None, - cutout=None, - sliding_window=False, - windowlen=3000, - coords_target=True, - pos_offset=(-21, -69), - label_smoothing=False, - station_blinding=False, - pga_targets=None, - adjust_mean=True, - transform_target_only=False, - trigger_based=None, - disable_station_foreshadowing=False, - selection_skew=None, - pga_from_inactive=False, - integrate=False, - sampling_rate=100.0, - select_first=False, - scale_metadata=True, - p_pick_limit=5000, - pga_mode=False, - no_event_token=False, - pga_selection_skew=None, - **kwargs, - ): + Args: + data_path: Path to the HDF5 file containing waveform data. + event_key: The key in the event metadata DataFrame identifying each event. + data: Dictionary containing 'coords' and 'pga' keys for metadata and PGA values. + event_metadata: Pandas DataFrame with event metadata. + waveform_shape: Shape of each waveform (number of samples, number of channels). + key: The key in event metadata to use for magnitude. + batch_size: Number of events per batch. + cutout: Tuple specifying the range for random cutout in the waveform. + sliding_window: Whether to use a sliding window for cutout. + windowlen: Length of the sliding window. + shuffle: Whether to shuffle the events at the end of each epoch. + coords_target: Whether to include event coordinates as targets. + oversample: Factor by which to oversample the events. + pos_offset: Offset to apply to event coordinates. + label_smoothing: Whether to apply label smoothing to magnitudes. + station_blinding: Whether to randomly blind stations in the waveforms. + magnitude_resampling: Factor by which to resample events based on their magnitude. + pga_targets: Number of PGA targets to sample per event. + adjust_mean: Whether to adjust the mean of the waveforms. + transform_target_only: Whether to apply transformations only to the target coordinates. + max_stations: Maximum number of stations to include per event. + trigger_based: Whether to zero out waveforms before the P-wave trigger. + min_upsample_magnitude: Minimum magnitude for upsampling. + disable_station_foreshadowing: Whether to disable station foreshadowing. + selection_skew: Skew parameter for selecting stations when max_stations is reached. + pga_from_inactive: Whether to sample PGA from inactive stations. + integrate: Whether to integrate the waveforms. + differentiate: Whether to differentiate the waveforms. + sampling_rate: Sampling rate of the waveforms. + select_first: Whether to select the first stations when max_stations is reached. + fake_borehole: Whether to add fake borehole channels to the waveforms. + scale_metadata: Whether to scale the metadata coordinates. + pga_key: Key in the data dictionary for PGA values. + pga_mode: Whether to operate in PGA mode. + p_pick_limit: Limit for P-wave picks. + coord_keys: Keys in the event metadata for coordinates. + upsample_high_station_events: Whether to upsample events with high station counts. + no_event_token: Whether to include an event token in the outputs. + pga_selection_skew: Skew parameter for selecting PGA targets. + **kwargs: + ''' + super().__init__() + if kwargs: + print(f'Unused parameters: {", ".join(kwargs.keys())}') + self.data_path = data_path + self.event_key = event_key + self.batch_size = batch_size + self.shuffle = shuffle self.waveform_shape = waveform_shape - self.max_stations = max_stations + self.metadata = data['coords'] + self.event_metadata = event_metadata + self.pga = data[pga_key] + self.key = key self.cutout = cutout self.sliding_window = sliding_window self.windowlen = windowlen self.coords_target = coords_target + self.oversample = oversample self.pos_offset = pos_offset self.label_smoothing = label_smoothing self.station_blinding = station_blinding + self.magnitude_resampling = magnitude_resampling self.pga_targets = pga_targets self.adjust_mean = adjust_mean self.transform_target_only = transform_target_only + self.max_stations = max_stations self.trigger_based = trigger_based self.disable_station_foreshadowing = disable_station_foreshadowing self.selection_skew = selection_skew self.pga_from_inactive = pga_from_inactive + self.pga_selection_skew = pga_selection_skew self.integrate = integrate + self.differentiate = differentiate self.sampling_rate = sampling_rate self.select_first = select_first + self.fake_borehole = fake_borehole self.scale_metadata = scale_metadata - self.p_pick_limit = p_pick_limit - self.pga_mode = pga_mode + self.upsample_high_station_events = upsample_high_station_events self.no_event_token = no_event_token - self.pga_selection_skew = pga_selection_skew - self.key = kwargs["key"] - - def process_batch(self, batch_data): - """Main method to process a batch of data, now decomposed into smaller functions.""" - ( - indexes, - waveforms_list, - metadata_list, - pga_list, - p_picks_list, - event_info_list, - pga_indexes, - ) = self._extract_batch_data(batch_data) + self.triggers = data['p_picks'] + self.pga_mode = pga_mode + self.p_pick_limit = p_pick_limit + self.base_indexes = np.arange(self.event_metadata.shape[0]) + self.reverse_index = None + if magnitude_resampling > 1: + magnitude = self.event_metadata[key].values + for i in np.arange(min_upsample_magnitude, 9): + ind = np.where(np.logical_and(i < magnitude, magnitude <= i + 1))[0] + self.base_indexes = np.concatenate( + (self.base_indexes, np.repeat(ind, int(magnitude_resampling ** (i - 1) - 1)))) + if pga_mode: + new_base_indexes = [] + self.reverse_index = [] + c = 0 + for idx in self.base_indexes: + num_samples = (len(self.pga[idx]) - 1) // pga_targets + 1 + new_base_indexes += [(idx, i) for i in range(num_samples)] + self.reverse_index += [c] + c += num_samples + self.reverse_index += [c] + self.base_indexes = new_base_indexes + self.indexes = np.arange(len(self.event_metadata)) + if coord_keys is None: + self.coord_keys = detect_location_keys(event_metadata.columns) + else: + self.coord_keys = coord_keys + self.on_epoch_end() - true_batch_size = len(indexes) - true_max_stations_in_batch = self._get_max_stations_in_batch(metadata_list) - waveforms, metadata, pga, full_p_picks, p_picks, reverse_selections = ( - self._initialize_arrays( - true_batch_size, true_max_stations_in_batch, metadata_list - ) - ) - waveforms, metadata, pga, p_picks, full_p_picks, reverse_selections = ( - self._process_stations( - waveforms_list, - metadata_list, - pga_list, - p_picks_list, - waveforms, - metadata, - pga, - p_picks, - full_p_picks, - ) - ) - magnitude, target = self._process_magnitude_and_targets(event_info_list) - org_waveform_length = waveforms.shape[2] - waveforms, _ = self._process_waveforms(waveforms, org_waveform_length, p_picks) - metadata, target = self._transform_locations(metadata, target) - magnitude = self._apply_label_smoothing(magnitude) - metadata, pga = self._adjust_metadata_and_pga(metadata, pga) - pga_values, pga_targets_data = self._process_pga_targets( - true_batch_size, - pga, - metadata, - pga_indexes, - reverse_selections, - full_p_picks, - indexes, - ) - waveforms, metadata = self._apply_station_blinding(waveforms, metadata) - waveforms, metadata = self._handle_stations_without_trigger(waveforms, metadata) - waveforms, metadata = self._ensure_no_empty_arrays(waveforms, metadata) - inputs, outputs = self._prepare_model_io( - waveforms, metadata, magnitude, target, pga_targets_data, pga_values - ) + def __len__(self): + """ + Returns the number of batches in the dataset. + """ + return int(np.ceil(len(self.indexes) / self.batch_size)) - return inputs, outputs + def __getitem__(self, index): + """ + Retrieves a batch of events from the dataset. + """ + indexes = self.indexes[index * self.batch_size:(index + 1) * self.batch_size] + true_batch_size = len(indexes) + if self.pga_mode: + self.pga_indexes = [x[1] for x in indexes] + indexes = [x[0] for x in indexes] - def _extract_batch_data(self, batch_data): - """Extract data from the batch dictionary.""" - indexes = batch_data["indexes"] - waveforms_list = batch_data["waveforms"] - metadata_list = batch_data["metadata"] - pga_list = batch_data["pga"] - p_picks_list = batch_data["p_picks"] - event_info_list = batch_data["event_info"] - pga_indexes = batch_data.get("pga_indexes", None) - - return ( - indexes, - waveforms_list, - metadata_list, - pga_list, - p_picks_list, - event_info_list, - pga_indexes, - ) - - def _get_max_stations_in_batch(self, metadata_list): - """Calculate the maximum number of stations in the batch.""" - return max( - [len(m) for m in metadata_list if m is not None] + [self.max_stations] - ) - - def _initialize_arrays( - self, true_batch_size, true_max_stations_in_batch, metadata_list - ): - """Initialize arrays for batch processing.""" waveforms = np.zeros([true_batch_size, self.max_stations] + self.waveform_shape) - metadata = np.zeros( - (true_batch_size, true_max_stations_in_batch) + metadata_list[0].shape[1:] - ) + true_max_stations_in_batch = max(max([self.metadata[idx].shape[0] for idx in indexes]), self.max_stations) + metadata = np.zeros((true_batch_size, true_max_stations_in_batch) + self.metadata[0].shape[1:]) pga = np.zeros((true_batch_size, true_max_stations_in_batch)) full_p_picks = np.zeros((true_batch_size, true_max_stations_in_batch)) p_picks = np.zeros((true_batch_size, self.max_stations)) reverse_selections = [] - return waveforms, metadata, pga, full_p_picks, p_picks, reverse_selections + waveforms, metadata, pga, p_picks, reverse_selections, full_p_picks = ( + self.htpyfile_process(indexes, waveforms, metadata, pga, + p_picks, reverse_selections, full_p_picks)) - def _process_stations( - self, - waveforms_list, - metadata_list, - pga_list, - p_picks_list, - waveforms, - metadata, - pga, - p_picks, - full_p_picks, - ): - """Process stations and waveforms for each item in the batch.""" - reverse_selections = [] + magnitude = self.event_metadata.iloc[indexes][self.key].values.copy() + magnitude = magnitude.astype(np.float32) - for i, (waveform_data, meta, pga_data, p_pick_data) in enumerate( - zip(waveforms_list, metadata_list, pga_list, p_picks_list) - ): - if waveform_data is None: - continue - - num_stations = waveform_data.shape[0] - - if num_stations <= self.max_stations: - waveforms[i, :num_stations] = waveform_data - metadata[i, : len(meta)] = meta - pga[i, : len(pga_data)] = pga_data - p_picks[i, : len(p_pick_data)] = p_pick_data - reverse_selections += [[]] - else: - selection = self._select_stations(num_stations, p_pick_data) + target, waveforms, magnitude, metadata, pga_values, pga_targets = ( + self.data_preprocessing(indexes, waveforms, p_picks, magnitude, metadata, + pga, true_batch_size, reverse_selections, full_p_picks)) - metadata[i, : len(selection)] = meta[selection] - pga[i, : len(selection)] = pga_data[selection] - full_p_picks[i, : len(selection)] = p_pick_data[selection] + waveforms, metadata = self.data_processing(waveforms, metadata) - tmp_reverse_selection = [0 for _ in selection] - for j, s in enumerate(selection): - tmp_reverse_selection[s] = j - reverse_selections += [tmp_reverse_selection] + return self.get_result(waveforms, metadata, magnitude, target, pga_targets, pga_values) - selection = selection[: self.max_stations] - waveforms[i] = waveform_data[selection] - p_picks[i] = p_pick_data[selection] - - return waveforms, metadata, pga, p_picks, full_p_picks, reverse_selections - - def _select_stations(self, num_stations, p_pick_data): - """Select stations based on configured strategy.""" - if self.selection_skew is None: - selection = np.arange(0, num_stations) - np.random.shuffle(selection) + def htpyfile_process(self, indexes, waveforms, metadata, pga, + p_picks, reverse_selections, full_p_picks): + """ + Processes the HDF5 file to retrieve waveform data for a batch of events. + """ + with h5py.File(self.data_path, 'r') as f: + for i, idx in enumerate(indexes): + event = self.event_metadata.iloc[idx] + event_name = str(event[self.event_key]) + if event_name not in f['data']: + continue + g_event = f['data'][event_name] + waveform_data = g_event['waveforms'][:, :, :] + + num_stations = waveform_data.shape[0] + + if num_stations <= self.max_stations: + waveforms[i, :num_stations] = waveform_data + metadata[i, :len(self.metadata[idx])] = self.metadata[idx] + pga[i, :len(self.pga[idx])] = self.pga[idx] + p_picks[i, :len(self.triggers[idx])] = self.triggers[idx] + reverse_selections += [[]] + else: + if self.selection_skew is None: + selection = np.arange(0, num_stations) + np.random.shuffle(selection) + else: + tmp_p_picks = self.triggers[idx].copy() + mask = np.logical_and(tmp_p_picks <= 0, tmp_p_picks > self.p_pick_limit) + tmp_p_picks[mask] = min(np.max(tmp_p_picks), self.p_pick_limit) + coeffs = np.exp(-tmp_p_picks / self.selection_skew) + coeffs *= np.random.random(coeffs.shape) + coeffs[self.triggers[idx] == 0] = 0 + coeffs[self.triggers[idx] > self.waveform_shape[0]] = 0 + selection = np.argsort(-coeffs) + + if self.select_first: + selection = np.argsort(self.triggers[idx]) + + metadata[i, :len(selection)] = self.metadata[idx][selection] + pga[i, :len(selection)] = self.pga[idx][selection] + full_p_picks[i, :len(selection)] = self.triggers[idx][selection] + + tmp_reverse_selection = [0 for _ in selection] + for j, s in enumerate(selection): + tmp_reverse_selection[s] = j + reverse_selections += [tmp_reverse_selection] + + selection = selection[:self.max_stations] + waveforms[i] = waveform_data[selection] + p_picks[i] = self.triggers[idx][selection] + return waveforms, metadata, pga, p_picks, reverse_selections, full_p_picks + + def pga_mode_process(self, waveforms, reverse_selections, metadata, + pga_values, pga_targets, pga, indexes, full_p_picks): + """ + Processes the data in PGA mode. + """ + if self.pga_mode: + for i in range(waveforms.shape[0]): + pga_index = self.pga_indexes[i] + if reverse_selections[i]: + sorted_pga = pga[i, reverse_selections[i]] + sorted_metadata = metadata[i, reverse_selections[i]] + else: + sorted_pga = pga[i] + sorted_metadata = metadata[i] + pga_values_pre = sorted_pga[pga_index * self.pga_targets:(pga_index + 1) * self.pga_targets] + pga_values[i, :len(pga_values_pre)] = pga_values_pre + pga_targets_pre = sorted_metadata[pga_index * self.pga_targets:(pga_index + 1) * self.pga_targets, :] + if pga_targets_pre.shape[-1] == 4: + pga_targets_pre = pga_targets_pre[:, (0, 1, 3)] + pga_targets[i, :len(pga_targets_pre), :] = pga_targets_pre else: - tmp_p_picks = p_pick_data.copy() - mask = np.logical_and(tmp_p_picks <= 0, tmp_p_picks > self.p_pick_limit) - tmp_p_picks[mask] = min(np.max(tmp_p_picks), self.p_pick_limit) - coeffs = np.exp(-tmp_p_picks / self.selection_skew) - coeffs *= np.random.random(coeffs.shape) - coeffs[p_pick_data == 0] = 0 - coeffs[p_pick_data > self.waveform_shape[0]] = 0 - selection = np.argsort(-coeffs) - - if self.select_first: - selection = np.argsort(p_pick_data) - - return selection - - def _process_magnitude_and_targets(self, event_info_list): - """Process magnitude and coordinate targets.""" - magnitude = np.array([e[self.key] for e in event_info_list], dtype=np.float32) + pga[np.logical_or(np.isnan(pga), np.isinf(pga))] = 0 + for i in range(waveforms.shape[0]): + active = np.where(pga[i] != 0)[0] + l = len(active) + if l == 0: + raise ValueError(f'Found event without PGA idx={indexes[i]}') + while len(active) < self.pga_targets: + active = np.repeat(active, 2) + if self.pga_selection_skew is not None: + active_p_picks = full_p_picks[i, active] + mask = np.logical_and(active_p_picks <= 0, active_p_picks > self.p_pick_limit) + active_p_picks[mask] = min(np.max(active_p_picks), self.p_pick_limit) + coeffs = np.exp(-active_p_picks / self.pga_selection_skew) + coeffs *= np.random.random(coeffs.shape) + active = active[np.argsort(-coeffs)] + else: + np.random.shuffle(active) + + samples = active[:self.pga_targets] + if metadata.shape[-1] == 3: + pga_targets[i] = metadata[i, samples, :] + else: + full_targets = metadata[i, samples] + pga_targets[i] = full_targets[:, (0, 1, 3)] + pga_values[i] = pga[i, samples] + return pga_values, pga_targets + + def data_preprocessing(self, indexes, waveforms, p_picks, magnitude, metadata, + pga, true_batch_size, reverse_selections, full_p_picks): + """ + Data preprocessing. + """ target = None - if self.coords_target: - coord_keys = detect_location_keys( - [col for e in event_info_list for col in e.index] - ) - target = np.array( - [[e[k] for k in coord_keys] for e in event_info_list], dtype=np.float32 - ) - - magnitude = np.expand_dims(np.expand_dims(magnitude, axis=-1), axis=-1) - return magnitude, target - - def _process_waveforms(self, waveforms, org_waveform_length, p_picks): - """Apply cutout, sliding window, trigger-based, and integration transformations to waveforms.""" - cutout = org_waveform_length - + target = self.event_metadata.iloc[indexes][self.coord_keys].values + target = target.astype(np.float32) + org_waveform_length = waveforms.shape[2] if self.cutout: if self.sliding_window: windowlen = self.windowlen - window_end = np.random.randint( - max(windowlen, self.cutout[0]), - min(waveforms.shape[2], self.cutout[1]) + 1, - ) - waveforms = waveforms[:, :, window_end - windowlen : window_end] - + window_end = np.random.randint(max(windowlen, self.cutout[0]), + min(waveforms.shape[2], self.cutout[1]) + 1) + waveforms = waveforms[:, :, window_end - windowlen: window_end] cutout = window_end if self.adjust_mean: waveforms -= np.mean(waveforms, axis=2, keepdims=True) else: cutout = np.random.randint(*self.cutout) if self.adjust_mean: - waveforms -= np.mean( - waveforms[:, :, : cutout + 1], axis=2, keepdims=True - ) + waveforms -= np.mean(waveforms[:, :, :cutout + 1], axis=2, keepdims=True) waveforms[:, :, cutout:] = 0 + else: + cutout = waveforms.shape[2] if self.trigger_based: p_picks[p_picks <= 0] = org_waveform_length waveforms[cutout < p_picks, :, :] = 0 - if self.integrate: waveforms = np.cumsum(waveforms, axis=2) / self.sampling_rate + if self.differentiate: + waveforms = np.diff(waveforms, axis=2) - return waveforms, cutout - - def _transform_locations(self, metadata, target): - """Transform locations using the location_transformation method.""" + magnitude = np.expand_dims(np.expand_dims(magnitude, axis=-1), axis=-1) if self.coords_target: metadata, target = self.location_transformation(metadata, target) else: metadata = self.location_transformation(metadata) - return metadata, target - def _apply_label_smoothing(self, magnitude): - """Apply label smoothing to magnitude if enabled.""" if self.label_smoothing: - magnitude += ( - (magnitude > 4) - * np.random.randn(magnitude.shape[0]).reshape(magnitude.shape) - * (magnitude - 4) - * 0.05 - ) - return magnitude - - def _adjust_metadata_and_pga(self, metadata, pga): - """Adjust metadata and PGA arrays based on configuration.""" + magnitude += (magnitude > 4) * np.random.randn(magnitude.shape[0]).reshape(magnitude.shape) * ( + magnitude - 4) * 0.05 if not self.pga_from_inactive and not self.pga_mode: - metadata = metadata[:, : self.max_stations] - pga = pga[:, : self.max_stations] - return metadata, pga - - def _process_pga_targets( - self, - true_batch_size, - pga, - metadata, - pga_indexes, - reverse_selections, - full_p_picks, - indexes, - ): - """Process PGA targets if enabled.""" - pga_values = None - pga_targets_data = None - + metadata = metadata[:, :self.max_stations] + pga = pga[:, :self.max_stations] + pga_values = () + pga_targets = () if self.pga_targets: pga_values = np.zeros((true_batch_size, self.pga_targets)) - pga_targets_data = np.zeros((true_batch_size, self.pga_targets, 3)) - - if self.pga_mode and pga_indexes is not None: - self._process_pga_mode( - pga_values, - pga_targets_data, - pga, - metadata, - pga_indexes, - reverse_selections, - ) - else: - self._process_pga_normal( - pga_values, pga_targets_data, pga, metadata, full_p_picks, indexes - ) + pga_targets = np.zeros((true_batch_size, self.pga_targets, 3)) + + pga_values, pga_targets = self.pga_mode_process(waveforms, reverse_selections, metadata, + pga_values, pga_targets, pga, indexes, full_p_picks) pga_values = pga_values.reshape((true_batch_size, self.pga_targets, 1, 1)) - return pga_values, pga_targets_data + return target, waveforms, magnitude, metadata, pga_values, pga_targets - def _process_pga_mode( - self, - pga_values, - pga_targets_data, - pga, - metadata, - pga_indexes, - reverse_selections, - ): - """Process PGA in PGA mode.""" - for i in range(len(pga_values)): - pga_index = pga_indexes[i] - if reverse_selections[i]: - sorted_pga = pga[i, reverse_selections[i]] - sorted_metadata = metadata[i, reverse_selections[i]] - else: - sorted_pga = pga[i] - sorted_metadata = metadata[i] - pga_values_pre = sorted_pga[ - pga_index * self.pga_targets : (pga_index + 1) * self.pga_targets - ] - pga_values[i, : len(pga_values_pre)] = pga_values_pre - pga_targets_pre = sorted_metadata[ - pga_index * self.pga_targets : (pga_index + 1) * self.pga_targets, - :, - ] - if pga_targets_pre.shape[-1] == 4: - pga_targets_pre = pga_targets_pre[:, (0, 1, 3)] - pga_targets_data[i, : len(pga_targets_pre), :] = pga_targets_pre - - def _process_pga_normal( - self, pga_values, pga_targets_data, pga, metadata, full_p_picks, indexes - ): - """Process PGA in normal mode.""" - pga[np.logical_or(np.isnan(pga), np.isinf(pga))] = 0 - for i in range(pga_values.shape[0]): - active = np.where(pga[i] != 0)[0] - if not active: - raise ValueError(f"Found event without PGA idx={indexes[i]}") - while len(active) < self.pga_targets: - active = np.repeat(active, 2) - - if self.pga_selection_skew is not None: - active = self._select_pga_with_skew(active, full_p_picks[i]) - else: - np.random.shuffle(active) - - samples = active[: self.pga_targets] - if metadata.shape[-1] == 3: - pga_targets_data[i] = metadata[i, samples, :] - else: - full_targets = metadata[i, samples] - pga_targets_data[i] = full_targets[:, (0, 1, 3)] - pga_values[i] = pga[i, samples] - - def _select_pga_with_skew(self, active, full_p_picks): - """Select PGA with skew-based selection.""" - active_p_picks = full_p_picks[active] - mask = np.logical_and(active_p_picks <= 0, active_p_picks > self.p_pick_limit) - active_p_picks[mask] = min(np.max(active_p_picks), self.p_pick_limit) - coeffs = np.exp(-active_p_picks / self.pga_selection_skew) - coeffs *= np.random.random(coeffs.shape) - return active[np.argsort(-coeffs)] - - def _apply_station_blinding(self, waveforms, metadata): - """Apply station blinding if enabled.""" + def data_processing(self, waveforms, metadata): + """ + Data process. + """ + metadata = metadata[:, :self.max_stations] if self.station_blinding: mask = np.zeros(waveforms.shape[:2], dtype=bool) for i in range(waveforms.shape[0]): active = np.where((waveforms[i] != 0).any(axis=(1, 2)))[0] - if not active == 0: + l = len(active) + if l == 0: active = np.zeros(1, dtype=int) blind_length = np.random.randint(0, len(active)) np.random.shuffle(active) @@ -623,58 +644,48 @@ class DataProcessor: waveforms[mask] = 0 metadata[mask] = 0 - return waveforms, metadata - - def _handle_stations_without_trigger(self, waveforms, metadata): - """Handle stations without trigger signal.""" - stations_without_trigger = (metadata != 0).any(axis=2) & (waveforms == 0).all( - axis=(2, 3) - ) - + stations_without_trigger = (metadata != 0).any(axis=2) & (waveforms == 0).all(axis=(2, 3)) if self.disable_station_foreshadowing: metadata[stations_without_trigger] = 0 else: waveforms[stations_without_trigger, 0, 0] += 1e-9 - return waveforms, metadata - - def _ensure_no_empty_arrays(self, waveforms, metadata): - """Ensure there are no empty arrays in the batch.""" - mask = np.logical_and( - (metadata == 0).all(axis=(1, 2)), (waveforms == 0).all(axis=(1, 2, 3)) - ) + mask = np.logical_and((metadata == 0).all(axis=(1, 2)), (waveforms == 0).all(axis=(1, 2, 3))) waveforms[mask, 0, 0, 0] = 1e-9 metadata[mask, 0, 0] = 1e-9 return waveforms, metadata - def _prepare_model_io( - self, waveforms, metadata, magnitude, target, pga_targets_data, pga_values - ): - """Prepare model inputs and outputs.""" - inputs = [ - mindspore.tensor(waveforms, dtype=mindspore.float32), - mindspore.tensor(metadata, dtype=mindspore.float32), - ] + def get_result(self, waveforms, metadata, magnitude, target, pga_targets, pga_values): + """ + get result. + """ + inputs = [ms.tensor(waveforms, dtype=ms.float32), ms.tensor(metadata, dtype=ms.float32)] outputs = [] - if not self.no_event_token: - outputs += [mindspore.tensor(magnitude, dtype=mindspore.float32)] + outputs += [ms.tensor(magnitude, dtype=ms.float32)] if self.coords_target: target = np.expand_dims(target, axis=-1) - outputs += [mindspore.tensor(target, dtype=mindspore.float32)] + outputs += [ms.tensor(target, dtype=ms.float32)] - if self.pga_targets and pga_values is not None and pga_targets_data is not None: - inputs += [mindspore.tensor(pga_targets_data, dtype=mindspore.float32)] - outputs += [mindspore.tensor(pga_values, dtype=mindspore.float32)] + if self.pga_targets: + inputs += [ms.tensor(pga_targets, dtype=ms.float32)] + outputs += [ms.tensor(pga_values, dtype=ms.float32)] return inputs, outputs + def on_epoch_end(self): + """ + Resets the indexes for a new epoch, optionally with oversampling and shuffling. + """ + self.indexes = np.repeat(self.base_indexes.copy(), self.oversample, axis=0) + if self.shuffle: + np.random.shuffle(self.indexes) + def location_transformation(self, metadata, target=None): """ - Apply transformations to the metadata and optionally to the target. - Adjusts positions based on a positional offset and scales the data if required. + Transforms the event coordinates and optionally the target coordinates. """ transform_target_only = self.transform_target_only metadata = metadata.copy() @@ -682,86 +693,32 @@ class DataProcessor: metadata_old = metadata metadata = metadata.copy() mask = (metadata == 0).all(axis=2) - if target is not None: target[:, 0] -= self.pos_offset[0] target[:, 1] -= self.pos_offset[1] - metadata[:, :, 0] -= self.pos_offset[0] metadata[:, :, 1] -= self.pos_offset[1] + + # Coordinates to kilometers (assuming a flat earth, which is okay close to equator) if self.scale_metadata: metadata[:, :, :2] *= D2KM if target is not None: target[:, :2] *= D2KM + metadata[mask] = 0 + if self.scale_metadata: metadata /= 100 if target is not None: target /= 100 + if transform_target_only: metadata = metadata_old + if target is None: return metadata - return metadata, target - - -class PreloadedEventGenerator(Dataset): - """ - A custom PyTorch Dataset class designed to generate preloaded event data for training or evaluation. - This class wraps an `EarthquakeDataset` and a `DataProcessor` to provide processed input-output pairs. - Attributes: - dataset (EarthquakeDataset): An instance of the EarthquakeDataset class, responsible for loading - raw earthquake-related data. - processor (DataProcessor): An instance of the DataProcessor class, responsible for processing - the raw data into model-ready inputs and outputs. - """ - - def __init__(self, data_path, event_key, data, event_metadata, **kwargs): - """ - Initializes the PreloadedEventGenerator. - Args: - data_path (str): The file path or directory where the dataset is stored. - event_key (str): A key used to identify specific events within the dataset. - data (dict or array-like): Raw data associated with the events. - event_metadata (dict or DataFrame): Metadata describing the events in the dataset. - **kwargs: Additional keyword arguments passed to both EarthquakeDataset and DataProcessor. - """ - super(PreloadedEventGenerator, self).__init__() - self.dataset = EarthquakeDataset( - data_path=data_path, - event_key=event_key, - data=data, - event_metadata=event_metadata, - **kwargs, - ) - self.processor = DataProcessor(**kwargs) - - def __len__(self): - """ - Returns the total number of samples in the dataset. - - Returns: - int: The length of the underlying EarthquakeDataset. - """ - return len(self.dataset) - - def __getitem__(self, index): - """ - Retrieves and processes a single batch of data at the given index. - - Args: - index (int): The index of the data sample to retrieve. - - Returns: - tuple: A tuple containing two elements: - inputs: Processed input data ready for model consumption. - outputs: Corresponding target outputs for the model. - """ - batch_data = self.dataset[index] - inputs, outputs = self.processor.process_batch(batch_data) - - return inputs, outputs + return metadata, target def generator_from_config( config, @@ -796,4 +753,5 @@ def generator_from_config( pga_mode=pga, **generator_params, ) + return generator diff --git a/MindEarth/applications/earthquake/G-TEAM/src/forcast.py b/MindEarth/applications/earthquake/G-TEAM/src/forcast.py index 2c67ab33b..b1752b8e4 100644 --- a/MindEarth/applications/earthquake/G-TEAM/src/forcast.py +++ b/MindEarth/applications/earthquake/G-TEAM/src/forcast.py @@ -12,19 +12,73 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"GTeam inference" +"GTeam forcast" +import os +from tqdm import tqdm import numpy as np +import mindspore as ms +import mindspore.nn as nn +import mindspore.ops as ops + from src.utils import ( predict_at_time, calc_mag_stats, calc_loc_stats, calc_pga_stats, ) -from src.data import load_data +from src.data import DataGenerator, PreloadedEventGenerator, load_pickle_data, load_data +from src.models import SingleStationModel +from src.utils import evaluation, seed_np_tf from src.visual import generate_true_pred_plot +class CustomWithLossCell(nn.Cell): + """ + A neural network cell that wraps a main network and loss function together, + allowing the entire forward pass including loss computation to be treated as a single cell. + + This class combines a neural network model and a loss function into a single computation unit, + which is useful for training loops and model encapsulation in deep learning frameworks. + + Attributes: + net (nn.Cell): The main neural network model whose output will be used in loss computation. + loss_fn (nn.Cell): The loss function cell that computes the difference between predictions + and true labels. + """ + + def __init__(self, net, loss_fn): + """ + Initializes the CustomWithLossCell with a network model and loss function. + + Args: + net (nn.Cell): The neural network model whose output will be used for loss calculation. + loss_fn (nn.Cell): The loss computation function that takes (true_labels, predictions) + and returns a scalar loss value. + """ + super().__init__() + self.net = net + self.loss_fn = loss_fn + + def construct(self, x, y): + ''' + Computes the loss by first passing input data through the network and then applying the loss function. + + Args: + X (Tensor): Input data tensor containing features. + y (Tensor): Ground truth labels tensor. + + Returns: + Tensor: Computed loss value. + + Note: + The input labels 'y' are squeezed along dimension 2 to match the output shape from the network. + This ensures the loss function receives inputs of the expected shape. + ''' + outputs = self.net(x) + return self.loss_fn(y.squeeze(2), outputs) + + class GTeamInference: """ Initialize the GTeamInference class. @@ -143,3 +197,325 @@ class GTeamInference: ) self._save_results() print("Inference completed and results saved") + +class GTeamTrain: + """ + A class to handle the training of a full model for earthquake detection and localization. + It manages data loading, training of single-station models, and full-model training. + """ + def __init__(self, model_ins, cfg, output_dir, logger): + """ + Initialize the GTeamTrain class with model, configuration, output directory, and logger. + Args: + model_ins (nn.Cell): The full model instance to be trained. + cfg (dict): Configuration dictionary containing training parameters and paths. + output_dir (str): Directory to save checkpoints and outputs. + logger (logging.Logger): Logger instance for logging messages. + """ + self.full_model = model_ins + self.cfg = cfg + self.output_dir = output_dir + self.logger = logger + self.waveform_shape = [3000, 3] + self.training_params = self.cfg['training_params'] + self.generator_params = self.training_params.get('generator_params', [self.training_params.copy()]) + self.file_basename = os.path.basename(self.training_params['data_path']).split('.')[0] + + def load_train_data(self): + """ + Load training data from a pickle file. + Returns: + Data structure: The loaded training data. + """ + data_path = self.cfg['data']["root_dir"] + filename_train = os.path.join(data_path, f"{self.file_basename}_train.pkl") + return load_pickle_data(filename_train) + + def load_val_data(self): + """ + Load validation data from a pickle file. + Returns: + Data structure: The loaded validation data. + """ + data_path = self.cfg['data']["root_dir"] + filename_val = os.path.join(data_path, f"{self.file_basename}_val.pkl") + return load_pickle_data(filename_val) + + def init_single_generator(self, sampling_rate, event_metadata_index_train, event_key_train, + event_metadata_index_val, event_key_val, decimate_train): + """ + Initialize the single-station model and its data generators for training and validation. + Args: + sampling_rate (float): Sampling rate of the seismic data. + event_metadata_index_train (list): Indices for training events in the metadata. + event_key_train (str): Key for selecting the training event data. + event_metadata_index_val (list): Indices for validation events in the metadata. + event_key_val (str): Key for selecting the validation event data. + decimate_train (bool): Whether to decimate the training data. + """ + self.single_station_model = SingleStationModel(output_mlp_dims=self.cfg['model']['output_mlp_dims'], + use_mlp=self.cfg['model']['use_mlp']) + noise_seconds = self.generator_params[0].get('noise_seconds', 5) + cutout = (sampling_rate * (noise_seconds + self.generator_params[0]['cutout_start']), + sampling_rate * (noise_seconds + self.generator_params[0]['cutout_end'])) + self.single_train_generator = DataGenerator(self.training_params['data_path'], + event_metadata_index_train, event_key_train, + mag_key=self.generator_params[0]['key'], + batch_size=self.generator_params[0]['batch_size'], + cutout=cutout, + label_smoothing=True, + sliding_window=self.generator_params[0].get('sliding_window', + False), + decimate=decimate_train) + self.single_validation_generator = DataGenerator(self.training_params['data_path'], + event_metadata_index_val, event_key_val, + mag_key=self.generator_params[0]['key'], + batch_size=self.generator_params[0]['batch_size'], + cutout=cutout, + label_smoothing=True, + sliding_window=self.generator_params[0].get('sliding_window', + False), + decimate=decimate_train) + optimizer_single = nn.Adam(self.single_station_model.trainable_params(), learning_rate=1e-4) + self.criterion_single_mse = nn.MSELoss() + + loss_net = CustomWithLossCell(self.single_station_model, self.criterion_single_mse) + self.single_train_network = nn.TrainOneStepCell(loss_net, optimizer_single) + + self.single_station_model.set_train(True) + + def single_station_train(self, sampling_rate, event_metadata_index_train, event_key_train, + event_metadata_index_val, event_key_val, decimate_train): + """ + Train the single-station model. Loads a pre-trained model if specified, otherwise + initializes the generator and trains from scratch. + Args: + sampling_rate (float): Sampling rate of the seismic data. + event_metadata_index_train (list): Indices for training events in the metadata. + event_key_train (str): Key for selecting the training event data. + event_metadata_index_val (list): Indices for validation events in the metadata. + event_key_val (str): Key for selecting the validation event data. + decimate_train (bool): Whether to decimate the training data. + """ + if 'single_station_model_path' in self.training_params: + print('Loading single station model') + param_dict = ms.load_checkpoint(self.training_params['single_station_model_path']) + ms.load_param_into_net(self.single_station_model, param_dict) + elif 'transfer_model_path' not in self.training_params: + self.init_single_generator(sampling_rate, event_metadata_index_train, event_key_train, + event_metadata_index_val, event_key_val, decimate_train) + + for epoch in tqdm(range(self.training_params['epochs_single_station']), + desc='training single station model'): + train_loss = 0.0 + + for i in range(len(self.single_train_generator)): + x, y = self.single_train_generator[i] + loss = self.single_train_network(x, y) + train_loss += loss.asnumpy() + + train_loss /= len(self.single_train_generator) + + val_loss = 0.0 + for i in range(len(self.single_validation_generator)): + x, y = self.single_validation_generator[i] + outputs = self.single_station_model(x) + loss = self.criterion_single_mse(y.squeeze(2), outputs) + val_loss += loss.item() + + val_loss /= len(self.single_validation_generator) + + print(f'Epoch {epoch + 1}/{self.training_params["epochs_single_station"]}, ' + f'Training Loss: {train_loss}, Validation Loss: {val_loss}') + + ms.save_checkpoint(self.single_station_model, + os.path.join(self.output_dir, f'single-station-{epoch + 1}')) + + def init_full_generator(self, sampling_rate, event_key_train, data_train, event_metadata_train, + max_stations, event_key_val, data_val, event_metadata_val): + """ + Initialize the full model's data generators and optimizer. + Args: + sampling_rate (float): Sampling rate of the seismic data. + event_key_train (str): Key for selecting the training event data. + data_train: Training data. + event_metadata_train: Metadata for training events. + max_stations (int): Maximum number of stations to consider. + event_key_val (str): Key for selecting the validation event data. + data_val: Validation data. + event_metadata_val: Metadata for validation events. + """ + if 'load_model_path' in self.training_params: + print('Loading full model') + param_dict = ms.load_checkpoint(self.training_params['load_model_path']) + ms.load_param_into_net(self.full_model, param_dict) + + n_pga_targets = self.cfg['model'].get('n_pga_targets', 0) + no_event_token = self.cfg['model'].get('no_event_token', False) + + self.optimizer_full = nn.Adam(self.full_model.trainable_params(), learning_rate=1e-4) + self.losses_full_mse = {'magnitude': nn.MSELoss(), 'location': nn.MSELoss(), 'pga': nn.MSELoss()} + + generator_param_set = self.generator_params[0] + noise_seconds = generator_param_set.get('noise_seconds', 5) + cutout = (sampling_rate * (noise_seconds + generator_param_set['cutout_start']), + sampling_rate * (noise_seconds + generator_param_set['cutout_end'])) + + generator_param_set['transform_target_only'] = generator_param_set.get('transform_target_only', True) + + if 'data_path' in generator_param_set: + del generator_param_set['data_path'] + + self.full_train_generator = PreloadedEventGenerator(self.training_params['data_path'], + event_key_train, + data_train, + event_metadata_train, + waveform_shape=self.waveform_shape, + coords_target=True, + label_smoothing=True, + station_blinding=True, + cutout=cutout, + pga_targets=n_pga_targets, + max_stations=max_stations, + sampling_rate=sampling_rate, + no_event_token=no_event_token, + **generator_param_set) + + old_oversample = generator_param_set.get('oversample', 1) + generator_param_set['oversample'] = 4 + + self.full_validation_generator = PreloadedEventGenerator(self.training_params['data_path'], + event_key_val, + data_val, + event_metadata_val, + waveform_shape=self.waveform_shape, + coords_target=True, + station_blinding=True, + cutout=cutout, + pga_targets=n_pga_targets, + max_stations=max_stations, + sampling_rate=sampling_rate, + no_event_token=no_event_token, + **generator_param_set) + + generator_param_set['oversample'] = old_oversample + print('len(full_train_generator)', len(self.full_train_generator)) + + self.loss_weights = self.training_params['loss_weights'] + print(f'The total number of parameters: {sum(p.numel() for p in self.full_model.trainable_params())}') + + def full_station_train(self, sampling_rate, event_key_train, data_train, event_metadata_train, + max_stations, event_key_val, data_val, event_metadata_val): + """ + Train the full station model using the initialized generators and optimizer. + + Args: + sampling_rate (float): Sampling rate of the seismic data + event_key_train (str): Key for selecting training event data + data_train: Training data + event_metadata_train: Training event metadata + max_stations (int): Maximum number of stations to consider + event_key_val (str): Key for selecting validation event data + data_val: Validation data + event_metadata_val: Validation event metadata + """ + self.init_full_generator(sampling_rate, event_key_train, data_train, event_metadata_train, + max_stations, event_key_val, data_val, event_metadata_val) + def calculate_total_loss(network, x, y): + train_mag_loss = 0 + train_loc_loss = 0 + train_pga_loss = 0 + outputs = network(x[0], x[1], x[2]) + total_loss = 0 + for k, loss_fn in self.losses_full_mse.items(): + if k == 'magnitude': + mag_pre = outputs[0] + mag_target = y[0] + mag_loss = loss_fn(mag_target.squeeze(2), mag_pre) * self.loss_weights[k] + train_mag_loss += mag_loss + total_loss += mag_loss + elif k == 'location': + loc_pre = outputs[1] + loc_target = y[1] + loc_loss = loss_fn(loc_target.squeeze(2), loc_pre) * self.loss_weights[k] + train_loc_loss += loc_loss + total_loss += loc_loss + elif k == 'pga': + pga_pre = outputs[2] + if 'italy' in self.file_basename: + pga_target = y[2] + else: + pga_target = ops.log(ops.abs(y[2])) + pga_loss = loss_fn(pga_target.squeeze(3), pga_pre) * self.loss_weights[k] + train_pga_loss += pga_loss + total_loss += pga_loss + return total_loss + + self.full_model.set_train() + grad_fn = ms.value_and_grad( + fn=calculate_total_loss, + grad_position=None, + weights=self.full_model.trainable_params(), + has_aux=False + ) + for epoch in tqdm(range(self.training_params['epochs_full_model']), desc='training full model'): + train_loss = 0 + + for i in range(len(self.full_train_generator)): + x, y = self.full_train_generator[i] + + total_loss, grads = grad_fn(self.full_model, x, y) + self.optimizer_full(grads) + + train_loss += total_loss.item() + avg_train_loss = train_loss / len(self.full_train_generator) + + avg_val_loss = evaluation(self.full_model, self.full_validation_generator, + self.losses_full_mse, self.loss_weights) + + print(f'Epoch {epoch + 1}/{self.training_params["epochs_full_model"]}', + f'Average Training Loss: {avg_train_loss}', f'Average val Loss: {avg_val_loss}') + + ms.save_checkpoint(self.full_model, os.path.join(self.output_dir, f'event-{epoch + 1}')) + + print('Training complete, and loss history saved.') + + def train(self): + """ + Train the full model for earthquake detection and localization. + + This method orchestrates the training process by: + 1. Setting the random seed for reproducibility. + 2. Loading training and validation datasets. + 3. Extracting key parameters like sampling rate and event metadata. + 4. Training single-station models for each station in the dataset. + 5. Training the full multi-station model using the pre-trained single-station models. + + Steps: + - Initialize random seed from configuration (default: 42) + - Load training data and extract metadata + - Load validation data + - Extract sampling rate and remove 'max_stations' from model config + - Train single-station models using training and validation data + - Train full model using combined data from all stations + + Note: This method assumes that the `single_station_train` and `full_station_train` methods are implemented. + """ + seed_np_tf(self.cfg['training_params'].get('seed', 42)) + + print('Loading data') + (event_metadata_index_train, event_metadata_train, metadata_train, + data_train, event_key_train, decimate_train) = self.load_train_data() + (event_metadata_index_val, event_metadata_val, _, + data_val, event_key_val, _) = self.load_val_data() + + sampling_rate = metadata_train['sampling_rate'] + max_stations = self.cfg['model']['max_stations'] + del self.cfg['model']['max_stations'] + + print('training') + self.single_station_train(sampling_rate, event_metadata_index_train, event_key_train, + event_metadata_index_val, event_key_val, decimate_train) + + self.full_station_train(sampling_rate, event_key_train, data_train, event_metadata_train, + max_stations, event_key_val, data_val, event_metadata_val) diff --git a/MindEarth/applications/earthquake/G-TEAM/src/models.py b/MindEarth/applications/earthquake/G-TEAM/src/models.py index d7a650c9d..1c91673a6 100644 --- a/MindEarth/applications/earthquake/G-TEAM/src/models.py +++ b/MindEarth/applications/earthquake/G-TEAM/src/models.py @@ -29,20 +29,31 @@ class MLP(nn.Cell): final_activation: The activation function for the final layer. Default is nn.ReLU. """ - def __init__(self, input_shape, dims=(100, 50), final_activation=nn.ReLU()): + def __init__(self, input_shape, dims=(100, 50), final_activation=nn.ReLU(), is_mlp=False): super().__init__() layers = [] in_dim = input_shape[0] - - for dim in dims[:-1]: - layers.append(nn.Dense(in_dim, dim)) - layers.append(nn.ReLU()) - in_dim = dim - layers.append(nn.Dense(in_dim, dims[-1])) - - if final_activation: - layers.append(final_activation) - self.model = nn.SequentialCell(*layers) + if is_mlp: + for dim in dims[:-1]: + layers.append(nn.Dense(in_dim, dim)) + layers.append(nn.LayerNorm((dim,))) + layers.append(nn.ReLU()) + in_dim = dim + layers.append(nn.Dense(in_dim, dims[-1])) + + if final_activation: + layers.append(final_activation) + self.model = nn.SequentialCell(*layers) + else: + for dim in dims[:-1]: + layers.append(nn.Dense(in_dim, dim)) + layers.append(nn.ReLU()) + in_dim = dim + layers.append(nn.Dense(in_dim, dims[-1])) + + if final_activation: + layers.append(final_activation) + self.model = nn.SequentialCell(*layers) def construct(self, x): """ @@ -61,7 +72,7 @@ class NormalizedScaleEmbedding(nn.Cell): convolutional and pooling layers, and processes the features through a multi-layer perceptron (MLP). """ - def __init__(self, downsample=5, mlp_dims=(500, 300, 200, 150), eps=1e-8): + def __init__(self, downsample=5, mlp_dims=(500, 300, 200, 150), eps=1e-8, use_mlp=False): """ Initialize the module with given parameters. Parameters: @@ -98,8 +109,20 @@ class NormalizedScaleEmbedding(nn.Cell): self.conv1d_5 = nn.Conv1d(32, 16, kernel_size=4, has_bias=True, pad_mode="pad") self.flatten = nn.Flatten() - self.mlp = MLP((865,), dims=self.mlp_dims) + self.mlp = MLP((865,), dims=self.mlp_dims, is_mlp=use_mlp) self.leaky_relu = nn.LeakyReLU(alpha=0.01) + self._initialize_weights() + + def _initialize_weights(self): + self.conv2d_1.bias.set_data(ms.numpy.zeros_like(self.conv2d_1.bias)) + self.conv2d_2.bias.set_data(ms.numpy.zeros_like(self.conv2d_2.bias)) + + # For Conv1d layers + self.conv1d_1.bias.set_data(ms.numpy.zeros_like(self.conv1d_1.bias)) + self.conv1d_2.bias.set_data(ms.numpy.zeros_like(self.conv1d_2.bias)) + self.conv1d_3.bias.set_data(ms.numpy.zeros_like(self.conv1d_3.bias)) + self.conv1d_4.bias.set_data(ms.numpy.zeros_like(self.conv1d_4.bias)) + self.conv1d_5.bias.set_data(ms.numpy.zeros_like(self.conv1d_5.bias)) def construct(self, x): """ @@ -213,6 +236,7 @@ class PositionEmbedding(nn.Cell): min_lat, max_lat = wavelengths[0] min_lon, max_lon = wavelengths[1] min_depth, max_depth = wavelengths[2] + assert emb_dim % 10 == 0 lat_dim = emb_dim // 5 lon_dim = emb_dim // 5 depth_dim = emb_dim // 10 @@ -307,7 +331,43 @@ class AddEventToken(nn.Cell): return x +class SingleStationModel(nn.Cell): + """ + A neural network model for processing seismic waveforms from a single station. + This class implements a two-stage processing pipeline: waveform embedding followed by feature extraction. + """ + def __init__(self, waveform_model_dims=(500, 500, 500), + output_mlp_dims=(150, 100, 50, 30, 10), downsample=5, use_mlp=False): + """ + Initialize the SingleStationModel. + + Args: + waveform_model_dims (tuple): Dimensions of the MLP in the waveform embedding module. + Format: (input_dim, hidden_dim1, hidden_dim2, ...) + output_mlp_dims (tuple): Dimensions of the final MLP for feature extraction. + Format: (input_dim, hidden_dim1, hidden_dim2, ...) + downsample (int): Factor by which to downsample the input waveform data. + """ + super().__init__() + + self.waveform_model = NormalizedScaleEmbedding(downsample=downsample, mlp_dims=waveform_model_dims, + use_mlp=use_mlp) + self.mlp_mag_single_station = MLP((self.waveform_model.mlp_dims[-1],), output_mlp_dims) + + def construct(self, x): + """ + Forward pass of the SingleStationModel. + + Args: + x (Tensor): Input waveform data with shape (batch_size, time_steps, features) + + Returns: + Tensor: Extracted features with shape (batch_size, output_features) + """ + emb = self.waveform_model(x) + emb_mlp = self.mlp_mag_single_station(emb) + return emb_mlp def _init_pad_mask(waveforms, pga_targets): """ _init_pad_mask function, used to initialize the padding mask. @@ -344,10 +404,11 @@ class WaveformFullmodel(nn.Cell): hidden_dropout=0.0, n_pga_targets=0, downsample=5, + use_mlp=False ): super().__init__() self.waveform_model = NormalizedScaleEmbedding( - downsample=downsample, mlp_dims=waveform_model_dims + downsample=downsample, mlp_dims=waveform_model_dims, use_mlp=use_mlp ) self.transformer = TransformerEncoder( d_model=waveform_model_dims[-1], @@ -357,12 +418,12 @@ class WaveformFullmodel(nn.Cell): dropout=hidden_dropout, ) - self.mlp_mag = MLP((waveform_model_dims[-1],), output_mlp_dims) + self.mlp_mag = MLP((waveform_model_dims[-1],), output_mlp_dims, is_mlp=use_mlp) self.mlp_loc = MLP( - (waveform_model_dims[-1],), output_location_dims, final_activation=None + (waveform_model_dims[-1],), output_location_dims, final_activation=None, is_mlp=use_mlp ) self.mlp_pga = MLP( - (waveform_model_dims[-1],), output_mlp_dims, final_activation=None + (waveform_model_dims[-1],), output_mlp_dims, final_activation=None, is_mlp=use_mlp ) self.position_embedding = PositionEmbedding( diff --git a/MindEarth/applications/earthquake/G-TEAM/src/utils.py b/MindEarth/applications/earthquake/G-TEAM/src/utils.py index f1a4ae250..f8ae9cabf 100644 --- a/MindEarth/applications/earthquake/G-TEAM/src/utils.py +++ b/MindEarth/applications/earthquake/G-TEAM/src/utils.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== -"GTeam util" +"""GTeam util""" import os import copy import numpy as np @@ -21,12 +21,11 @@ from geopy.distance import geodesic import mindspore as ms import mindspore.ops as ops +from mindearth import create_logger from src import data from src.data import generator_from_config, D2KM from src.models import WaveformFullmodel -from mindearth.utils import create_logger - def predict_at_time( model, @@ -71,32 +70,23 @@ def predict_at_time( loc_pred_filter = [] pga_pred_filter = [] - for i, (start, end) in enumerate( - zip(generator.dataset.reverse_index[:-1], generator.dataset.reverse_index[1:]) - ): - sample_mag_pred = predictions[0][start:end].reshape( - (-1,) + predictions[0].shape[-1:] - ) - sample_mag_pred = sample_mag_pred[: len(generator.dataset.pga[i])] + for i, (start, end) in enumerate(zip(generator.reverse_index[:-1], generator.reverse_index[1:])): + sample_mag_pred = predictions[0][start:end].reshape((-1,) + predictions[0].shape[-1:]) + sample_mag_pred = sample_mag_pred[:len(generator.pga[i])] mag_pred_filter += [sample_mag_pred] - sample_loc_pred = predictions[1][start:end].reshape( - (-1,) + predictions[1].shape[-1:] - ) - sample_loc_pred = sample_loc_pred[: len(generator.dataset.pga[i])] + sample_loc_pred = predictions[1][start:end].reshape((-1,) + predictions[1].shape[-1:]) + sample_loc_pred = sample_loc_pred[:len(generator.pga[i])] loc_pred_filter += [sample_loc_pred] - sample_pga_pred = predictions[2][start:end].reshape( - (-1,) + predictions[2].shape[-1:] - ) - sample_pga_pred = sample_pga_pred[: len(generator.dataset.pga[i])] + sample_pga_pred = predictions[2][start:end].reshape((-1,) + predictions[2].shape[-1:]) + sample_pga_pred = sample_pga_pred[:len(generator.pga[i])] pga_pred_filter += [sample_pga_pred] preds = [mag_pred_filter, loc_pred_filter, pga_pred_filter] return preds - def calc_mag_stats(mag_pred, event_metadata, key): """Calculate statistical information for magnitude predictions""" mean_mag = mag_pred @@ -109,7 +99,6 @@ def calc_mag_stats(mag_pred, event_metadata, key): mae = metrics.mean_absolute_error(true_mag, mean_mag) return r2, rmse, mae - def calc_pga_stats(pga_pred, pga_true, suffix=""): """Calculate statistical information for PGA predictions""" if suffix: @@ -123,7 +112,6 @@ def calc_pga_stats(pga_pred, pga_true, suffix=""): return [r2, rmse, mae] - def calc_loc_stats(loc_pred, event_metadata, pos_offset): """Calculate statistical information for location predictions""" coord_keys = data.detect_location_keys(event_metadata.columns) @@ -153,19 +141,70 @@ def calc_loc_stats(loc_pred, event_metadata, pos_offset): return rmse_hypo, mae_hypo, rmse_epi, mae_epi +def seed_np_tf(seed): + '''Set the random seed for numpy and manual seed for mindspore.''' + np.random.seed(seed) + ms.manual_seed(seed) + + +def evaluation(full_model, val_generator, losses, loss_weights): + """ + Evaluates the performance of the full_model on the validation data provided by val_generator. + Calculates the average validation loss by accumulating losses from different components (magnitude, location, pga) + using the specified loss functions and weights. + Args: + full_model (nn.Cell): The complete model to be evaluated in inference mode. + val_generator (generator): A generator that yields batches of validation data (x, y). + Each x is expected to be a tuple of three input tensors, and y is a tuple of three target tensors. + losses (dict): A dictionary mapping loss names to their respective loss functions. + Supported keys: 'magnitude', 'location', 'pga'. + loss_weights (dict): A dictionary mapping loss names to their corresponding weights. + Returns: + float: The average validation loss over the entire validation dataset. + """ + full_model.set_train(False) + epoch_val_loss = 0 + for i in range(len(val_generator)): + x, y = val_generator[i] + outputs = full_model(x[0], x[1], x[2]) + total_val_loss = ms.Tensor(0) + + for k, loss_fn in losses.items(): + if k == 'magnitude': + mag_pre = outputs[0] + mag_target = y[0] + mag_loss = loss_fn(mag_target.squeeze(2), mag_pre) * loss_weights[k] + total_val_loss += mag_loss + elif k == 'location': + loc_pre = outputs[1] + loc_target = y[1] + loc_loss = loss_fn(loc_target.squeeze(2), loc_pre) * loss_weights[k] + total_val_loss += loc_loss + elif k == 'pga': + pga_pre = outputs[2] + pga_target = ops.log(ops.abs(y[2])) + pga_loss = loss_fn(pga_target.squeeze(3), pga_pre) * loss_weights[k] + total_val_loss += pga_loss + epoch_val_loss += total_val_loss.item() + avg_val_loss = epoch_val_loss / len(val_generator) + return avg_val_loss def init_model(arg): """set model""" tmpcfg = copy.deepcopy(arg["model"]) + tmpcfg.pop("istraining") tmpcfg.pop("no_event_token") tmpcfg.pop("run_with_less_data") tmpcfg.pop("pga") tmpcfg.pop("mode") tmpcfg.pop("times") + tmpcfg.pop("max_stations") model = WaveformFullmodel(**tmpcfg) - param_dict = ms.load_checkpoint(arg["summary"].get("ckpt_path")) - # Load parameters into the network - ms.load_param_into_net(model, param_dict) - model.set_train(False) + if arg['model']['istraining']: + model.set_train(True) + else: + param_dict = ms.load_checkpoint(arg["summary"].get("ckpt_path")) + ms.load_param_into_net(model, param_dict) + model.set_train(False) return model diff --git a/MindEarth/applications/earthquake/G-TEAM/src/visual.py b/MindEarth/applications/earthquake/G-TEAM/src/visual.py index ef3dc1195..78354a6f8 100644 --- a/MindEarth/applications/earthquake/G-TEAM/src/visual.py +++ b/MindEarth/applications/earthquake/G-TEAM/src/visual.py @@ -18,7 +18,6 @@ import matplotlib.pyplot as plt import numpy as np import sklearn.metrics as metrics - def generate_true_pred_plot(pred_values, true_values, time, path, suffix=""): """ Generate a plot comparing true values and predicted values, and calculate -- Gitee From 86394ff989c4f8d01e8347bacdbed703abe72886 Mon Sep 17 00:00:00 2001 From: goto Date: Mon, 30 Jun 2025 11:14:29 +0800 Subject: [PATCH 09/30] fix-mindflow-AdaHessian --- tests/st/mindflow/cell/test_optimizers.py | 79 ++++++++++++++++++----- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/tests/st/mindflow/cell/test_optimizers.py b/tests/st/mindflow/cell/test_optimizers.py index 1882e36df..eaa34f1fc 100644 --- a/tests/st/mindflow/cell/test_optimizers.py +++ b/tests/st/mindflow/cell/test_optimizers.py @@ -24,7 +24,7 @@ import numpy as np import mindspore as ms from mindspore import ops, set_seed, nn from mindspore import dtype as mstype -from mindflow import UNet2D, TransformerBlock, AdaHessian +from mindflow import UNet2D, TransformerBlock, MultiHeadAttention, AdaHessian from mindflow.cell.attention import FeedForward from mindflow.cell.unet2d import Down @@ -75,16 +75,62 @@ class TestUNet2D(UNet2D): class TestAttentionBlock(TransformerBlock): ''' Child class for testing optimizing Attention with AdaHessian ''' - def __init__(self, in_channels, num_heads, drop_mode="dropout", dropout_rate=0.0, compute_dtype=mstype.float16): - super().__init__( - in_channels, num_heads, drop_mode=drop_mode, dropout_rate=dropout_rate, compute_dtype=compute_dtype) + def __init__(self, + in_channels: int, + num_heads: int, + enable_flash_attn: bool = False, + fa_dtype: mstype = mstype.bfloat16, + drop_mode: str = "dropout", + dropout_rate: float = 0.0, + compute_dtype: mstype = mstype.float32, + ): + super().__init__(in_channels=in_channels, + num_heads=num_heads, + enable_flash_attn=enable_flash_attn, + fa_dtype=fa_dtype, + drop_mode=drop_mode, + dropout_rate=dropout_rate, + compute_dtype=compute_dtype, + ) class TestMlp(FeedForward): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.act_fn = nn.ReLU() # replace `gelu` with `relu` to avoid `vjp` problem - self.ffn = TestMlp(in_channels=in_channels, dropout_rate=dropout_rate, compute_dtype=compute_dtype) + class TestMultiHeadAttention(MultiHeadAttention): + ''' MultiHeadAttention modified to avoid vjp bug ''' + def get_qkv(self, x: ms.Tensor) -> tuple[ms.Tensor]: + ''' use masks to select out q, k, v, instead of tensor reshaping & indexing ''' + b, n, c = x.shape + + # use matmul with masks to select out q, k, v to avoid vjp problem + q_mask = ms.Tensor(np.vstack([np.eye(c), np.zeros([2 * c, c])]), dtype=self.compute_dtype) + k_mask = ms.Tensor(np.vstack([np.zeros([c, c]), np.eye(c), np.zeros([c, c])]), dtype=self.compute_dtype) + v_mask = ms.Tensor(np.vstack([np.zeros([2 * c, c]), np.eye(c)]), dtype=self.compute_dtype) + + qkv = self.qkv(x) + + q = ops.swapaxes(ops.matmul(qkv, q_mask).reshape(b, n, self.num_heads, -1), 1, 2) + k = ops.swapaxes(ops.matmul(qkv, k_mask).reshape(b, n, self.num_heads, -1), 1, 2) + v = ops.swapaxes(ops.matmul(qkv, v_mask).reshape(b, n, self.num_heads, -1), 1, 2) + + return q, k, v + + self.ffn = TestMlp( + in_channels=in_channels, + dropout_rate=dropout_rate, + compute_dtype=compute_dtype, + ) + self.attention = TestMultiHeadAttention( + in_channels=in_channels, + num_heads=num_heads, + enable_flash_attn=enable_flash_attn, + fa_dtype=fa_dtype, + drop_mode=drop_mode, + dropout_rate=dropout_rate, + compute_dtype=compute_dtype, + ) @pytest.mark.level0 @@ -133,7 +179,7 @@ def test_adahessian_accuracy(mode): @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) -@pytest.mark.parametrize('model_option', ['unet']) +@pytest.mark.parametrize('model_option', ['unet', 'attention']) def test_adahessian_st(mode, model_option): """ Feature: AdaHessian ST test @@ -146,7 +192,7 @@ def test_adahessian_st(mode, model_option): # default test with Attention network net = TestAttentionBlock(in_channels=256, num_heads=4) - inputs = ms.Tensor(np.sin(np.reshape(range(102400), [4, 100, 256])), dtype=ms.float32) + inputs = ms.Tensor(np.random.rand(4, 100, 256), dtype=ms.float32) # test with UNet network if model_option.lower() == 'unet': @@ -159,9 +205,9 @@ def test_adahessian_st(mode, model_option): stride=2, activation='relu', data_format="NCHW", - enable_bn=True, + enable_bn=False, # bn leads to bug in PYNATIVE_MODE for MS2.5.0 ) - inputs = ms.Tensor(np.sin(np.reshape(range(16384), [2, 2, 64, 64])), dtype=ms.float32) + inputs = ms.Tensor(np.random.rand(2, 2, 64, 64), dtype=ms.float32) def forward(a): return ops.mean(net(a)**2)**.5 @@ -173,16 +219,17 @@ def test_adahessian_st(mode, model_option): learning_rate=0.1, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.) for _ in range(4): - loss = forward(inputs) optimizer(grad_fn, inputs) + loss = forward(inputs) assert ops.isfinite(loss) -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard -def test_adahessian_compare(): +@pytest.mark.parametrize('mode', [ms.PYNATIVE_MODE]) +def test_adahessian_compare(mode): """ Feature: AdaHessian compare with Adam Description: Compare the algorithm results of the AdaHessian optimizer with Adam. @@ -190,12 +237,12 @@ def test_adahessian_compare(): The optimization runs 100 rounds to demonstrate an essential loss decrease. Expectation: The loss of AdaHessian outperforms Adam by 20% under the same configuration on an Attention network. """ - ms.set_context(mode=ms.PYNATIVE_MODE) + ms.set_context(mode=mode) def get_loss(optimizer_option): ''' compare Adam and AdaHessian ''' net = TestAttentionBlock(in_channels=256, num_heads=4) - inputs = ms.Tensor(np.sin(np.reshape(range(102400), [4, 100, 256])), dtype=ms.float32) + inputs = ms.Tensor(np.random.rand(4, 100, 256), dtype=ms.float32) def forward(a): return ops.mean(net(a)**2)**.5 @@ -211,13 +258,13 @@ def test_adahessian_compare(): net.trainable_params(), learning_rate=0.01, beta1=0.9, beta2=0.999, eps=1e-8, weight_decay=0.) - for _ in range(100): - loss = forward(inputs) + for _ in range(20): if optimizer_option.lower() == 'adam': optimizer(grad_fn(inputs)) else: optimizer(grad_fn, inputs) + loss = forward(inputs) return loss loss_adam = get_loss('adam') -- Gitee From d96f225736ec3d5d5729b1d2781473c26520b17e Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 1 Jul 2025 19:00:49 +0800 Subject: [PATCH 10/30] update --- ...y.cell.orb.AttentionInteractionNetwork.rst | 32 +++++++++++++++++ .../mindchemistry.cell.orb.EnergyHead.rst | 28 +++++++++++++++ .../cell/mindchemistry.cell.orb.GraphHead.rst | 25 +++++++++++++ .../mindchemistry.cell.orb.MoleculeGNS.rst | 34 ++++++++++++++++++ .../cell/mindchemistry.cell.orb.NodeHead.rst | 26 ++++++++++++++ .../cell/mindchemistry.cell.orb.Orb.rst | 35 +++++++++++++++++++ .../mindchemistry/mindchemistry.cell.rst | 6 ++++ .../mindchemistry/mindchemistry.cell.rst | 6 ++++ 8 files changed, 192 insertions(+) create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.AttentionInteractionNetwork.rst create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.GraphHead.rst create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.MoleculeGNS.rst create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.NodeHead.rst create mode 100644 docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.Orb.rst diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.AttentionInteractionNetwork.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.AttentionInteractionNetwork.rst new file mode 100644 index 000000000..7778f3a53 --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.AttentionInteractionNetwork.rst @@ -0,0 +1,32 @@ +mindchemistry.cell.orb.AttentionInteractionNetwork +================================================== + +.. py:class:: mindchemistry.cell.orb.AttentionInteractionNetwork(num_node_in: int, num_node_out: int, num_edge_in: int, num_edge_out: int, num_mlp_layers: int, mlp_hidden_dim: int, attention_gate: str = "sigmoid", distance_cutoff: bool = True, polynomial_order: int = 4, cutoff_rmax: float = 6.0) + + 注意力交互网络。实现基于注意力机制的消息传递神经网络层,用于分子图的边更新。 + + 参数: + - **num_node_in** (int) - 节点输入特征数量。 + - **num_node_out** (int) - 节点输出特征数量。 + - **num_edge_in** (int) - 边输入特征数量。 + - **num_edge_out** (int) - 边输出特征数量。 + - **num_mlp_layers** (int) - 节点和边更新MLP的隐藏层数量。 + - **mlp_hidden_dim** (int) - MLP的隐藏维度大小。 + - **attention_gate** (str,可选) - 注意力门类型, ``"sigmoid"`` 或 ``"softmax"``。默认值: ``"sigmoid"``。 + - **distance_cutoff** (bool,可选) - 是否使用基于距离的边截断。默认值: ``True``。 + - **polynomial_order** (int,可选) - 多项式截断函数的阶数。默认值: ``4``。 + - **cutoff_rmax** (float,可选) - 截断的最大距离。默认值: ``6.0``。 + + 输入: + - **graph_edges** (dict) - 边特征字典,必须包含键"feat",形状为 :math:`(n_{edges}, num\_edge\_in)`。 + - **graph_nodes** (dict) - 节点特征字典,必须包含键"feat",形状为 :math:`(n_{nodes}, num\_node\_in)`。 + - **senders** (Tensor) - 每条边的发送节点索引,形状为 :math:`(n_{edges},)`。 + - **receivers** (Tensor) - 每条边的接收节点索引,形状为 :math:`(n_{edges},)`。 + + 输出: + - **edges** (dict) - 更新的边特征字典,键"feat"的形状为 :math:`(n_{edges}, num\_edge\_out)`。 + - **nodes** (dict) - 更新的节点特征字典,键"feat"的形状为 :math:`(n_{nodes}, num\_node\_out)`。 + + 异常: + - **ValueError** - 如果 `attention_gate` 不是"sigmoid"或"softmax"。 + - **ValueError** - 如果边或节点特征不包含必需的"feat"键。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst new file mode 100644 index 000000000..ca1f06da2 --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst @@ -0,0 +1,28 @@ +mindchemistry.cell.orb.EnergyHead +================================== + +.. py:class:: mindchemistry.cell.orb.EnergyHead(latent_dim: int, num_mlp_layers: int, mlp_hidden_dim: int, target_property_dim: int, predict_atom_avg: bool = True, reference_energy_name: str = "mp-traj-d3", train_reference: bool = False, dropout: Optional[float] = None, node_aggregation: Optional[str] = None) + + 图级能量预测头。实现用于预测分子图总能量或原子平均能量的神经网络头。支持节点级聚合、参考能量偏移和灵活的输出模式。 + + 参数: + - **latent_dim** (int) - 每个节点的输入特征维度。 + - **num_mlp_layers** (int) - MLP中的隐藏层数量。 + - **mlp_hidden_dim** (int) - MLP的隐藏维度大小。 + - **target_property_dim** (int) - 能量属性的输出维度(通常为1)。 + - **predict_atom_avg** (bool,可选) - 是否预测每原子平均能量而不是总能量。默认值: ``True``。 + - **reference_energy_name** (str,可选) - 用于偏移的参考能量名称,例如 ``"vasp-shifted"``。默认值: ``"mp-traj-d3"``。 + - **train_reference** (bool,可选) - 是否将参考能量训练为可学习参数。默认值: ``False``。 + - **dropout** (Optional[float],可选) - MLP的dropout率。默认值: ``None``。 + - **node_aggregation** (str,可选) - 节点预测的聚合方法,例如 ``"mean"``或 ``"sum"``。默认值: ``None``。 + + 输入: + - **node_features** (dict) - 节点特征字典,必须包含键"feat",形状为 :math:`(n_{nodes}, latent\_dim)`。 + - **n_node** (Tensor) - 图中节点数量,形状为 :math:`(1,)`。 + + 输出: + - **output** (dict) - 包含键"graph_pred"的字典,值的形状为 :math:`(1, target\_property\_dim)`。 + + 异常: + - **ValueError** - 如果 `node_features` 中缺少必需的特征键。 + - **ValueError** - 如果 `node_aggregation` 不是支持的类型。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.GraphHead.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.GraphHead.rst new file mode 100644 index 000000000..75ae5ad7c --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.GraphHead.rst @@ -0,0 +1,25 @@ +mindchemistry.cell.orb.GraphHead +================================= + +.. py:class:: mindchemistry.cell.orb.GraphHead(latent_dim: int, num_mlp_layers: int, mlp_hidden_dim: int, target_property_dim: int, node_aggregation: str = "mean", dropout: Optional[float] = None, compute_stress: Optional[bool] = False) + + 图级预测头。实现可以附加到基础模型的图级预测头,用于从节点特征预测图级属性(例如应力张量),使用聚合和MLP。 + + 参数: + - **latent_dim** (int) - 每个节点的输入特征维度。 + - **num_mlp_layers** (int) - MLP中的隐藏层数量。 + - **mlp_hidden_dim** (int) - MLP的隐藏维度大小。 + - **target_property_dim** (int) - 图级属性的输出维度。 + - **node_aggregation** (str,可选) - 节点预测的聚合方法,例如 ``"mean"`` 或 ``"sum"``。默认值: ``"mean"``。 + - **dropout** (Optional[float],可选) - MLP的dropout率。默认值: ``None``。 + - **compute_stress** (bool,可选) - 是否计算和输出应力张量。默认值: ``False``。 + + 输入: + - **node_features** (dict) - 节点特征字典,必须包含键"feat",形状为 :math:`(n_{nodes}, latent\_dim)`。 + - **n_node** (Tensor) - 图中节点数量,形状为 :math:`(1,)`。 + + 输出: + - **output** (dict) - 包含键"stress_pred"的字典,值的形状为 :math:`(1, target\_property\_dim)`。 + + 异常: + - **ValueError** - 如果 `node_features` 中缺少必需的特征键。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.MoleculeGNS.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.MoleculeGNS.rst new file mode 100644 index 000000000..c44551f32 --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.MoleculeGNS.rst @@ -0,0 +1,34 @@ +mindchemistry.cell.orb.MoleculeGNS +=================================== + +.. py:class:: mindchemistry.cell.orb.MoleculeGNS(num_node_in_features: int, num_node_out_features: int, num_edge_in_features: int, latent_dim: int, num_message_passing_steps: int, num_mlp_layers: int, mlp_hidden_dim: int, node_feature_names: List[str], edge_feature_names: List[str], use_embedding: bool = True, interactions: str = "simple_attention", interaction_params: Optional[Dict[str, Any]] = None) + + 分子图神经网络。实现用于分子性质预测的灵活模块化图神经网络,基于注意力或其他交互机制的消息传递。支持节点和边嵌入、多个消息传递步骤,以及用于复杂分子图的可定制交互层。 + + 参数: + - **num_node_in_features** (int) - 每个节点的输入特征数量。 + - **num_node_out_features** (int) - 每个节点的输出特征数量。 + - **num_edge_in_features** (int) - 每条边的输入特征数量。 + - **latent_dim** (int) - 节点和边表示的潜在维度。 + - **num_message_passing_steps** (int) - 消息传递层的数量。 + - **num_mlp_layers** (int) - 节点和边更新MLP的隐藏层数量。 + - **mlp_hidden_dim** (int) - MLP的隐藏维度大小。 + - **node_feature_names** (List[str]) - 从输入字典中使用的节点特征键列表。 + - **edge_feature_names** (List[str]) - 从输入字典中使用的边特征键列表。 + - **use_embedding** (bool,可选) - 是否对节点使用原子序数嵌入。默认值: ``True``。 + - **interactions** (str,可选) - 要使用的交互层类型(例如, ``"simple_attention"``)。默认值: ``"simple_attention"``。 + - **interaction_params** (Optional[Dict[str, Any]],可选) - 交互层的参数,例如截断、多项式阶数、门类型。默认值: ``None``。 + + 输入: + - **edge_features** (dict) - 边特征字典,必须包含 `edge_feature_names` 中指定的键。 + - **node_features** (dict) - 节点特征字典,必须包含 `node_feature_names` 中指定的键。 + - **senders** (Tensor) - 每条边的发送节点索引,形状为 :math:`(n_{edges},)`。 + - **receivers** (Tensor) - 每条边的接收节点索引,形状为 :math:`(n_{edges},)`。 + + 输出: + - **edges** (dict) - 更新的边特征字典,键"feat"的形状为 :math:`(n_{edges}, latent\_dim)`。 + - **nodes** (dict) - 更新的节点特征字典,键"feat"的形状为 :math:`(n_{nodes}, latent\_dim)`。 + + 异常: + - **ValueError** - 如果 `edge_features` 或 `node_features` 中缺少必需的特征键。 + - **ValueError** - 如果 `interactions` 不是支持的类型。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.NodeHead.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.NodeHead.rst new file mode 100644 index 000000000..2e422d861 --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.NodeHead.rst @@ -0,0 +1,26 @@ +mindchemistry.cell.orb.NodeHead +=============================== + +.. py:class:: mindchemistry.cell.orb.NodeHead(latent_dim: int, num_mlp_layers: int, mlp_hidden_dim: int, target_property_dim: int, dropout: Optional[float] = None, remove_mean: bool = True) + + 节点级预测头。 + + 实现用于从节点特征预测节点级属性的神经网络头。该头可以添加到基础模型中以在预训练期间启用辅助任务,或在微调步骤中添加。 + + 参数: + - **latent_dim** (int) - 每个节点的输入特征维度。 + - **num_mlp_layers** (int) - MLP中的隐藏层数量。 + - **mlp_hidden_dim** (int) - MLP的隐藏维度大小。 + - **target_property_dim** (int) - 节点级目标属性的输出维度。 + - **dropout** (Optional[float],可选) - MLP的dropout率。默认值: ``None``。 + - **remove_mean** (bool,可选) - 如果为True,从输出中移除均值,通常用于力预测。默认值: ``True``。 + + 输入: + - **node_features** (dict) - 节点特征字典,必须包含键 "feat",形状为 :math:`(n_{nodes}, latent\_dim)`。 + - **n_node** (Tensor) - 图中节点数量,形状为 :math:`(1,)`。 + + 输出: + - **output** (dict) - 包含键 "node_pred" 的字典,值的形状为 :math:`(n_{nodes}, target\_property\_dim)`。 + + 异常: + - **ValueError** - 如果 `node_features` 中缺少必需的特征键。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.Orb.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.Orb.rst new file mode 100644 index 000000000..fe1d53a26 --- /dev/null +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.Orb.rst @@ -0,0 +1,35 @@ +mindchemistry.cell.orb.Orb +=========================== + +.. py:class:: mindchemistry.cell.orb.Orb(model: MoleculeGNS, node_head: Optional[NodeHead] = None, graph_head: Optional[GraphHead] = None, stress_head: Optional[GraphHead] = None, model_requires_grad: bool = True, cutoff_layers: Optional[int] = None) + + Orb图回归器。将预训练的基础模型(如MoleculeGNS)与可选的节点、图和应力回归头结合,支持微调或特征提取工作流程。 + + 参数: + - **model** (MoleculeGNS) - 用于消息传递和特征提取的预训练或随机初始化基础模型。 + - **node_head** (NodeHead,可选) - 节点级属性预测的回归头。默认值: ``None``。 + - **graph_head** (GraphHead,可选) - 图级属性预测(例如能量)的回归头。默认值: ``None``。 + - **stress_head** (GraphHead,可选) - 应力预测的回归头。默认值: ``None``。 + - **model_requires_grad** (bool,可选) - 是否微调基础模型(True)或冻结其参数(False)。默认值: ``True``。 + - **cutoff_layers** (int,可选) - 如果提供,仅使用基础模型的前 ``"cutoff_layers"`` 个消息传递层。默认值: ``None``。 + + 输入: + - **edge_features** (dict) - 边特征字典(例如,`{"vectors": Tensor, "r": Tensor}`)。 + - **node_features** (dict) - 节点特征字典(例如,`{"atomic_numbers": Tensor, ...}`)。 + - **senders** (Tensor) - 每条边的发送节点索引。形状::math:`(n_{edges},)`。 + - **receivers** (Tensor) - 每条边的接收节点索引。形状::math:`(n_{edges},)`。 + - **n_node** (Tensor) - 批次中每个图的节点数量。形状::math:`(n_{graphs},)`。 + + 输出: + - **output** (dict) - 包含以下内容的字典: + + - **edges** (dict) - 消息传递后的边特征,例如 `{..., "feat": Tensor}`。 + - **nodes** (dict) - 消息传递后的节点特征,例如 `{..., "feat": Tensor}`。 + - **graph_pred** (Tensor) - 图级预测,例如能量。形状::math:`(n_{graphs}, target\_property\_dim)`。 + - **node_pred** (Tensor) - 节点级预测。形状::math:`(n_{nodes}, target\_property\_dim)`。 + - **stress_pred** (Tensor) - 应力预测(如果提供stress_head)。形状::math:`(n_{graphs}, 6)`。 + + 异常: + - **ValueError** - 如果既未提供node_head也未提供graph_head。 + - **ValueError** - 如果cutoff_layers超过基础模型中的消息传递步骤数。 + - **ValueError** - 如果graph_head需要时未提供atomic_numbers。 \ No newline at end of file diff --git a/docs/api_python/mindchemistry/mindchemistry.cell.rst b/docs/api_python/mindchemistry/mindchemistry.cell.rst index 3c509e9ec..d346532e4 100644 --- a/docs/api_python/mindchemistry/mindchemistry.cell.rst +++ b/docs/api_python/mindchemistry/mindchemistry.cell.rst @@ -10,3 +10,9 @@ mindchemistry.cell mindchemistry.cell.AutoEncoder mindchemistry.cell.FCNet mindchemistry.cell.MLPNet + mindchemistry.cell.orb.AttentionInteractionNetwork + mindchemistry.cell.orb.EnergyHead + mindchemistry.cell.orb.GraphHead + mindchemistry.cell.orb.MoleculeGNS + mindchemistry.cell.orb.NodeHead + mindchemistry.cell.orb.Orb \ No newline at end of file diff --git a/docs/api_python_en/mindchemistry/mindchemistry.cell.rst b/docs/api_python_en/mindchemistry/mindchemistry.cell.rst index a5c9d4143..78cd3bfff 100644 --- a/docs/api_python_en/mindchemistry/mindchemistry.cell.rst +++ b/docs/api_python_en/mindchemistry/mindchemistry.cell.rst @@ -10,3 +10,9 @@ mindchemistry.cell mindchemistry.cell.AutoEncoder mindchemistry.cell.FCNet mindchemistry.cell.MLPNet + mindchemistry.cell.orb.AttentionInteractionNetwork + mindchemistry.cell.orb.EnergyHead + mindchemistry.cell.orb.GraphHead + mindchemistry.cell.orb.MoleculeGNS + mindchemistry.cell.orb.NodeHead + mindchemistry.cell.orb.Orb \ No newline at end of file -- Gitee From ac52e6bef45062d5920b9fd52a19664927299738 Mon Sep 17 00:00:00 2001 From: Muvyy Date: Fri, 4 Jul 2025 15:53:33 +0800 Subject: [PATCH 11/30] add orb --- .../orb/Parallel_Implementation.md | 121 +++ MindChemistry/applications/orb/README.md | 171 ++++ .../applications/orb/configs/config.yaml | 39 + .../applications/orb/configs/config_eval.yaml | 40 + .../orb/configs/config_parallel.yaml | 39 + MindChemistry/applications/orb/docs/orb.png | Bin 0 -> 840093 bytes MindChemistry/applications/orb/evaluate.py | 102 +++ MindChemistry/applications/orb/finetune.py | 311 ++++++++ .../applications/orb/requirement.txt | 8 + MindChemistry/applications/orb/run.sh | 23 + .../applications/orb/run_parallel.sh | 26 + .../applications/orb/src/__init__.py | 16 + .../applications/orb/src/ase_dataset.py | 239 ++++++ .../applications/orb/src/atomic_system.py | 222 ++++++ MindChemistry/applications/orb/src/base.py | 486 ++++++++++++ .../orb/src/featurization_utilities.py | 438 +++++++++++ .../applications/orb/src/pretrained.py | 116 +++ .../orb/src/property_definitions.py | 239 ++++++ .../applications/orb/src/segment_ops.py | 202 +++++ MindChemistry/applications/orb/src/trainer.py | 329 ++++++++ MindChemistry/applications/orb/src/utils.py | 296 +++++++ MindChemistry/mindchemistry/cell/__init__.py | 2 + .../mindchemistry/cell/orb/__init__.py | 36 + MindChemistry/mindchemistry/cell/orb/gns.py | 690 ++++++++++++++++ MindChemistry/mindchemistry/cell/orb/orb.py | 698 +++++++++++++++++ MindChemistry/mindchemistry/cell/orb/utils.py | 737 ++++++++++++++++++ tests/st/mindchemistry/cell/test_orb/base.py | 119 +++ .../mindchemistry/cell/test_orb/test_orb.py | 451 +++++++++++ tests/st/mindchemistry/cell/test_orb/utils.py | 105 +++ 29 files changed, 6301 insertions(+) create mode 100644 MindChemistry/applications/orb/Parallel_Implementation.md create mode 100644 MindChemistry/applications/orb/README.md create mode 100644 MindChemistry/applications/orb/configs/config.yaml create mode 100644 MindChemistry/applications/orb/configs/config_eval.yaml create mode 100644 MindChemistry/applications/orb/configs/config_parallel.yaml create mode 100644 MindChemistry/applications/orb/docs/orb.png create mode 100644 MindChemistry/applications/orb/evaluate.py create mode 100644 MindChemistry/applications/orb/finetune.py create mode 100644 MindChemistry/applications/orb/requirement.txt create mode 100644 MindChemistry/applications/orb/run.sh create mode 100644 MindChemistry/applications/orb/run_parallel.sh create mode 100644 MindChemistry/applications/orb/src/__init__.py create mode 100644 MindChemistry/applications/orb/src/ase_dataset.py create mode 100644 MindChemistry/applications/orb/src/atomic_system.py create mode 100644 MindChemistry/applications/orb/src/base.py create mode 100644 MindChemistry/applications/orb/src/featurization_utilities.py create mode 100644 MindChemistry/applications/orb/src/pretrained.py create mode 100644 MindChemistry/applications/orb/src/property_definitions.py create mode 100644 MindChemistry/applications/orb/src/segment_ops.py create mode 100644 MindChemistry/applications/orb/src/trainer.py create mode 100644 MindChemistry/applications/orb/src/utils.py create mode 100644 MindChemistry/mindchemistry/cell/orb/__init__.py create mode 100644 MindChemistry/mindchemistry/cell/orb/gns.py create mode 100644 MindChemistry/mindchemistry/cell/orb/orb.py create mode 100644 MindChemistry/mindchemistry/cell/orb/utils.py create mode 100644 tests/st/mindchemistry/cell/test_orb/base.py create mode 100644 tests/st/mindchemistry/cell/test_orb/test_orb.py create mode 100644 tests/st/mindchemistry/cell/test_orb/utils.py diff --git a/MindChemistry/applications/orb/Parallel_Implementation.md b/MindChemistry/applications/orb/Parallel_Implementation.md new file mode 100644 index 000000000..285183f0f --- /dev/null +++ b/MindChemistry/applications/orb/Parallel_Implementation.md @@ -0,0 +1,121 @@ +# ORB模型并行训练说明文档 + +本文档说明了ORB模型从单卡训练到多卡并行训练的实现方案、启动方式以及性能提升结果。 + +## 一、并行实现 + +对比`finetune.py`和`finetune_parallel.py`,主要有以下几处改动: + +1、引入并行训练所需的mindspore通信模块: + +```python +from mindspore.communication import init +from mindspore.communication import get_rank, get_group_size +``` + +2、训练步骤中增加梯度聚合: + +```python +# 单卡版本 +grad_fn = ms.value_and_grad(model.loss, None, optimizer.parameters, has_aux=True) + +# 并行版本 +grad_fn = ms.value_and_grad(model.loss, None, optimizer.parameters, has_aux=True) +grad_reducer = nn.DistributedGradReducer(optimizer.parameters) # 新增梯度规约器 +``` + +3、数据加载时实现数据分片: + +```python +# 单卡版本 +dataloader = [base.batch_graphs([dataset[j] for j in range(i, min(i + batch_size, len(dataset)))]) + for i in range(0, len(dataset), batch_size)] + +# 并行版本 +rank_id = get_rank() +rank_size = get_group_size() +dataloader = [[dataset[j] for j in range(i, min(i + batch_size, len(dataset)))] + for i in range(0, len(dataset), batch_size)] +dataloader = [base.batch_graphs( + data[rank_id*len(data)//rank_size : (rank_id+1)*len(data)//rank_size] +) for data in dataloader] +``` + +4、初始化并行训练环境: + +```python +ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True) +init() +``` + +## 二、启动方式 + +设置训练参数 + +> 1. 修改`configs/config_parallel.yaml`中的参数: +> a. 设置`data_path`字段指定训练和测试数据集 +> b. 设置`checkpoint_path`指定预训练模型权重路径 +> c. 根据需要调整其他训练参数 +> 2. 修改`run_parallel.sh`中的并行数: +> a. 通过`--worker_num=4 --local_worker_num=4`设置使用卡的数量 + +启动训练 + +```bash +pip install -r requirement.txt +bash run_parallel.sh +``` + +## 三、性能提升 + +单卡训练结果如下所示: + +```log +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Model has 25213610 trainable parameters. +Epoch: 0/100, + train_metrics: {'data_time': 0.00010895108183224995, 'train_time': 386.58018293464556, 'energy_reference_mae': 5.598883946736653, 'energy_mae': 3.3611322244008384, 'energy_mae_raw': 103.14391835530598, 'stress_mae': 41.36046473185221, 'stress_mae_raw': 12.710869789123535, 'node_mae': 0.02808943825463454, 'node_mae_raw': 0.0228044210622708, 'node_cosine_sim': 0.7026202281316122, 'fwt_0.03': 0.23958333333333334, 'loss': 44.74968592325846} + val_metrics: {'energy_reference_mae': 5.316623687744141, 'energy_mae': 3.594848871231079, 'energy_mae_raw': 101.00129699707031, 'stress_mae': 30.630516052246094, 'stress_mae_raw': 9.707925796508789, 'node_mae': 0.017718862742185593, 'node_mae_raw': 0.014386476017534733, 'node_cosine_sim': 0.5506304502487183, 'fwt_0.03': 0.375, 'loss': 34.24308395385742} + +... + +Epoch: 99/100, + train_metrics: {'data_time': 7.802306208759546e-05, 'train_time': 59.67856075416785, 'energy_reference_mae': 5.5912095705668134, 'energy_mae': 0.007512244085470836, 'energy_mae_raw': 0.21813046435515085, 'stress_mae': 0.7020445863405863, 'stress_mae_raw': 2.222463607788086, 'node_mae': 0.04725319395462672, 'node_mae_raw': 0.042800972859064736, 'node_cosine_sim': 0.3720853428045909, 'fwt_0.03': 0.09895833333333333, 'loss': 0.7568100094795227} + val_metrics: {'energy_reference_mae': 5.308632850646973, 'energy_mae': 0.27756747603416443, 'energy_mae_raw': 3.251189708709717, 'stress_mae': 2.8720269203186035, 'stress_mae_raw': 9.094478607177734, 'node_mae': 0.05565642938017845, 'node_mae_raw': 0.05041291564702988, 'node_cosine_sim': 0.212838813662529, 'fwt_0.03': 0.19499999284744263, 'loss': 3.2052507400512695} +Checkpoint saved to orb_ckpts/ +Training time: 7333.08717 seconds + +``` + +四卡并行训练结果如下所示: + +```log +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. + +... + +Training time: 2375.89474 seconds +Training time: 2377.02413 seconds +Training time: 2377.22778 seconds +Training time: 2376.63176 seconds + +``` + +在相同的训练配置下,并行训练相比单卡训练取得了显著的性能提升: + +- 单卡训练耗时:7293.28995 seconds +- 4卡并行训练耗时:2377.22778 seconds +- 性能提升:67.40% +- 加速比:3.07倍 diff --git a/MindChemistry/applications/orb/README.md b/MindChemistry/applications/orb/README.md new file mode 100644 index 000000000..afb66a864 --- /dev/null +++ b/MindChemistry/applications/orb/README.md @@ -0,0 +1,171 @@ + +# 模型名称 + +> Orb + +## 介绍 + +> 材料科学中,设计新型功能材料一直是新兴技术的关键部分。然而,传统的从头算计算方法在设计新型无机材料时速度慢且难以扩展到实际规模的系统。近年来,深度学习方法在多个领域展示了其强大的能力,能够通过并行架构高效运行。ORB模型的核心创新在于将这种深度学习方法应用于材料建模,通过可扩展的图神经网络架构学习原子间相互作用的复杂性。ORB模型是一个基于图神经网络(GNN)的机器学习力场(MLFF),设计为通用的原子间势能模型,适用于多种模拟任务(几何优化、蒙特卡洛模拟和分子动力学模拟)。该模型的输入是一个图结构,包含原子的位置、类型以及系统配置(如晶胞尺寸和边界条件);输出包括系统的总能量、每个原子的力向量以及单元格应力。与现有的开源神经网络势能模型(如MACE)相比,ORB模型在大系统规模下的速度提高了3-6倍。在Matbench Discovery基准测试中,ORB模型的误差比其他方法降低了31%,并且在发布时成为该基准测试的最新最佳模型。ORB模型在零样本评估中表现出色,即使在没有针对特定任务进行微调的情况下,也能在高温度非周期分子的分子动力学模拟中保持稳定。 + +![Orb模型预测自由能](docs/orb.png) + +> 上图中:(a) 通过Widom插入法在Mg-MOF-74中获得的MACE + D3(左)和Orb-D3(右)自由能表面。开放金属位点附近的蓝色区域代表最低自由能,表明这些是CO2的优势吸附位点。(b) CO2在Mg-MOF-74中的吸附位置,展示了通过Widom插入法获得的两个最有利的吸附位点,其吸附能分别为-54.5 kJ/mol和-54.4 kJ/mol。虽然Orb和MACE预测的能量极小值位置相似,但ORB的自由能最小值与实验测得的吸附热(-44 kJ/mol)数值更为接近。 + +## 环境要求 + +> 1. 安装`mindspore(2.5.0)` +> 2. 安装`mindchemistry` +> 3. 安装依赖包:`pip install -r requirement.txt` + +## 快速入门 + +> 1. 在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/dataset/)下载相应的数据集并放在`dataset`目录下 +> 2. 在[模型链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/orb_ckpts/)下载orb预训练模型ckpt并放在`orb_ckpts`目录下 +> 3. 安装依赖包:`pip install -r requirement.txt` +> 4. 单卡训练命令: `bash run.sh` +> 5. 多卡训练命令: `bash run_parallel.sh` +> 6. 评估命令: `python evaluate.py` +> 7. 模型预测结果会存在`results`目录下 + +### 代码目录结构 + +```text +代码主要模块在src文件夹下,其中dataset文件夹下是数据集,orb_ckpts文件夹下是预训练模型和训练好的模型权重文件,configs文件夹下是各代码的参数配置文件。 + +orb_models # 模型名 +├── dataset + ├── train_mptrj_ase.db # 微调阶段训练数据集 + └── val_mptrj_ase.db # 微调阶段测试数据集 +├── orb_ckpts + └── orb-mptraj-only-v2.ckpt # 预训练模型checkpoint +├── configs + ├── config.yaml # 单卡训练参数配置文件 + ├── config_parallel.yaml # 多卡并行训练参数配置文件 + └── config_eval.yaml # 推理参数配置文件 +├── src + ├── __init__.py + ├── ase_dataset.py # 处理和加载数据集 + ├── atomic_system.py # 定义原子系统的数据结构 + ├── base.py # 基础类定义 + ├── featurization_utilities.py # 提供将原子系统转换为特征向量的工具 + ├── pretrained.py # 预训练模型相关函数 + ├── property_definitions.py # 定义原子系统中各种物理性质的计算方式和命名规则 + ├── trainer.py # 模型loss类定义 + ├── segment_ops.py # 提供对数据进行分段处理的工具 + └── utils.py # 工具模块 +├── finetune.py # 模型微调代码 +├── evaluate.py # 模型推理代码 +├── run.sh # 单卡训练启动脚本 +├── run_parallel.sh # 多卡并行训练启动脚本 +└── requirement.txt # 环境 +``` + +## 下载数据集 + +在[数据集链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/dataset/)下载训练和测试数据集放置于当前路径的dataset文件夹下(如果没有需要自己手动创建);在[模型链接](https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/orb_ckpts/)下载orb预训练模型`orb-mptraj-only-v2.ckpt`放置于当前路径的orb_ckpts文件夹下(如果没有需要自己手动创建);文件路径参考[代码目录结构](#代码目录结构) + +## 训练过程 + +### 单卡训练 + +更改`configs/config.yaml`文件中训练参数: + +> 1. 设置微调阶段的训练和测试数据集,见`data_path`字段 +> 2. 设置训练加载的预训练模型权重文件,更改`checkpoint_path`路径字段 +> 3. 其它训练设置见Training Configuration部分 + +```bash +pip install -r requirement.txt +bash run.sh +``` + +代码运行结果如下所示: + +```log +============================================================================================================== +Please run the script as: +bash run.sh +============================================================================================================== +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Model has 25213610 trainable parameters. +Epoch: 0/100, + train_metrics: {'data_time': 0.00010895108183224995, 'train_time': 386.58018293464556, 'energy_reference_mae': 5.598883946736653, 'energy_mae': 3.3611322244008384, 'energy_mae_raw': 103.14391835530598, 'stress_mae': 41.36046473185221, 'stress_mae_raw': 12.710869789123535, 'node_mae': 0.02808943825463454, 'node_mae_raw': 0.0228044210622708, 'node_cosine_sim': 0.7026202281316122, 'fwt_0.03': 0.23958333333333334, 'loss': 44.74968592325846} + val_metrics: {'energy_reference_mae': 5.316623687744141, 'energy_mae': 3.594848871231079, 'energy_mae_raw': 101.00129699707031, 'stress_mae': 30.630516052246094, 'stress_mae_raw': 9.707925796508789, 'node_mae': 0.017718862742185593, 'node_mae_raw': 0.014386476017534733, 'node_cosine_sim': 0.5506304502487183, 'fwt_0.03': 0.375, 'loss': 34.24308395385742} + +... + +Epoch: 99/100, + train_metrics: {'data_time': 7.802306208759546e-05, 'train_time': 59.67856075416785, 'energy_reference_mae': 5.5912095705668134, 'energy_mae': 0.007512244085470836, 'energy_mae_raw': 0.21813046435515085, 'stress_mae': 0.7020445863405863, 'stress_mae_raw': 2.222463607788086, 'node_mae': 0.04725319395462672, 'node_mae_raw': 0.042800972859064736, 'node_cosine_sim': 0.3720853428045909, 'fwt_0.03': 0.09895833333333333, 'loss': 0.7568100094795227} + val_metrics: {'energy_reference_mae': 5.308632850646973, 'energy_mae': 0.27756747603416443, 'energy_mae_raw': 3.251189708709717, 'stress_mae': 2.8720269203186035, 'stress_mae_raw': 9.094478607177734, 'node_mae': 0.05565642938017845, 'node_mae_raw': 0.05041291564702988, 'node_cosine_sim': 0.212838813662529, 'fwt_0.03': 0.19499999284744263, 'loss': 3.2052507400512695} +Checkpoint saved to orb_ckpts/ +Training time: 7333.08717 seconds +``` + +### 多卡并行训练 + +更改`configs/config_parallel.yaml`和`run_parallel.sh`文件中训练参数: + +> 1. 设置微调阶段的训练和测试数据集,见`data_path`字段 +> 2. 设置训练加载的预训练模型权重文件,更改`checkpoint_path`路径字段 +> 3. 其它训练设置见Training Configuration部分 +> 4. 修改`run_parallel.sh`文件中`--worker_num=4 --local_worker_num=4`来设置调用的卡的数量 + +```bash +pip install -r requirement.txt +bash run_parallel.sh +``` + +代码运行结果如下所示: + +```log +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/train_mptrj_ase.dbTotal train dataset size: 800 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. +Model has 25213607 trainable parameters. + +... + +Training time: 2375.89474 seconds +Training time: 2377.02413 seconds +Training time: 2377.22778 seconds +Training time: 2376.63176 seconds +``` + +### 推理 + +更改`configs/config_eval.yaml`文件中推理参数: + +> 1. 设置测试数据集,见`val_data_path`字段 +> 2. 设置推理加载的预训练模型权重文件,更改`checkpoint_path`路径字段 +> 3. 其它训练设置见Evaluating Configuration部分 + +```bash +python evaluate.py +``` + +代码运行结果如下所示: + +```log +Loading datasets: dataset/val_mptrj_ase.dbTotal train dataset size: 200 samples +Model has 25213607 trainable parameters. +.Validation loss: 0.89507836 + energy_reference_mae: 5.3159098625183105 + energy_mae: 0.541229784488678 + energy_mae_raw: 4.244375228881836 + stress_mae: 0.22862032055854797 + stress_mae_raw: 10.575761795043945 + node_mae: 0.12522821128368378 + node_mae_raw: 0.04024107754230499 + node_cosine_sim: 0.38037967681884766 + fwt_0.03: 0.22499999403953552 + loss: 0.8950783610343933 +``` diff --git a/MindChemistry/applications/orb/configs/config.yaml b/MindChemistry/applications/orb/configs/config.yaml new file mode 100644 index 000000000..cbd4189a3 --- /dev/null +++ b/MindChemistry/applications/orb/configs/config.yaml @@ -0,0 +1,39 @@ +# Training Configuration +train_data_path: dataset/train_mptrj_ase.db +val_data_path: dataset/val_mptrj_ase.db +num_workers: 8 +batch_size: 64 +gradient_clip_val: 0.5 +max_epochs: 100 +checkpoint_path: orb_ckpts/ +lr: 3.0e-4 +random_seed: 1234 + +# Model Configuration +model: + # Energy Head Configuration + energy_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "energy" + node_aggregation: "mean" + reference_energy_name: "vasp-shifted" + train_reference: true + predict_atom_avg: true + + # Node Head Configuration + node_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "forces" + remove_mean: true + + # Stress Head Configuration + stress_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "stress" + compute_stress: true diff --git a/MindChemistry/applications/orb/configs/config_eval.yaml b/MindChemistry/applications/orb/configs/config_eval.yaml new file mode 100644 index 000000000..1e98c5f0b --- /dev/null +++ b/MindChemistry/applications/orb/configs/config_eval.yaml @@ -0,0 +1,40 @@ +# Evaluating Configuration +mode: "PYNATIVE" +device_target: "Ascend" +device_id: 0 +# Dataset config +val_data_path: dataset/val_mptrj_ase.db +num_workers: 8 +batch_size: 64 +checkpoint_path: orb_ckpts/orb-ft-checkpoint_epoch99.ckpt +random_seed: 1234 +output_dir: results/ + +# Model Configuration +model: + # Energy Head Configuration + energy_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "energy" + node_aggregation: "mean" + reference_energy_name: "vasp-shifted" + train_reference: true + predict_atom_avg: true + + # Node Head Configuration + node_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "forces" + remove_mean: true + + # Stress Head Configuration + stress_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "stress" + compute_stress: true diff --git a/MindChemistry/applications/orb/configs/config_parallel.yaml b/MindChemistry/applications/orb/configs/config_parallel.yaml new file mode 100644 index 000000000..c6a5e0857 --- /dev/null +++ b/MindChemistry/applications/orb/configs/config_parallel.yaml @@ -0,0 +1,39 @@ +# Training Configuration +train_data_path: dataset/train_mptrj_ase.db +val_data_path: dataset/val_mptrj_ase.db +num_workers: 8 +batch_size: 256 +gradient_clip_val: 0.5 +max_epochs: 100 +checkpoint_path: orb_ckpts/ +lr: 3.0e-4 +random_seed: 666 + +# Model Configuration +model: + # Energy Head Configuration + energy_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "energy" + node_aggregation: "mean" + reference_energy_name: "vasp-shifted" + train_reference: true + predict_atom_avg: true + + # Node Head Configuration + node_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "forces" + remove_mean: true + + # Stress Head Configuration + stress_head: + latent_dim: 256 + num_mlp_layers: 1 + mlp_hidden_dim: 256 + target: "stress" + compute_stress: true diff --git a/MindChemistry/applications/orb/docs/orb.png b/MindChemistry/applications/orb/docs/orb.png new file mode 100644 index 0000000000000000000000000000000000000000..6f9026b83e31dad2f48d626388e15262a831d02c GIT binary patch literal 840093 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

hoJ;(n-_}tgg(Jo-!Jp#^>A6Cxb3a% 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⁡TI#owLxle)Xz!Moca%7r2>U8=nG!jb_ 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 z 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%0A_*SZ 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
U;XKg^yh{Z$&SMEl(+Mq~Y<4`hI!mj>QM` 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
  • M_I! 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 PIq;>>1|!4{ z3mwN!ojPHg#oi>yQ^rp}{O|+xGk6)W*IaW=%z=5MB6YzIOKI6dcEC?%|AcJ@3?uPI z7^eVT;OK11;I#RZ0mV*@JaD*g(+t#?TYLVzy^ zO6G0%M#<& zt~mv+ze`-Jik!P#9%e(EZpyZq54Bs`r<;#VH`h07L{9Pt3})d4CFwWx6I}U&YK+$s(i?8%awGO))b@vm~q&oA?Kw$CSo?{Yzz>);}DP9 zFTM2AN*9|L5wcG)tSsyy!P~gY63d>dO32uUrt zUOrI#>C&YOA}83RfW@_MSkW8tSrk$*5zj!-oJg52@Xhi3{iQRJu1piZrC9rU6oBlgTq>@OiF;)=kvv{{J%PofEweOc9(Xex9Sk0fB4{6yN>DC;y(LuwY*lu4HgE_$ z9?V|6XGpcBWO7E%i_8N2zJLGzAoon{Z@&5F|Ni$sW$abjyafKo`uN5hZ@@Ld+QA61 zIuiusRj0;isop32h0e6i$BZ*s(ZP`s0s3jOu9ZVVZWNrX8qhxOS>#G9lW) zLOjpw-RSWw_jne1JpcN1&)L_I(2>V|=v{Z+6~Iv8SD7+lPU1Qon17U|K1PbbQ~=)a zQRv>?fB*e=-gzfegwd{m3(#?7RmdV?icki^HyY&xuUc5?j}WhhJFd zr&l*E-RydCOW_uWYwjLLfu`e##bQY_%dHZ7clTO{V|rnUWoyA9cKyOS_MBSQgY9Op zE>gbB^ZCvqkHrGA-kPY5DVyM4ke(rsVH%0IPG`b8iFcIu#0FIz%`*;lXwEEu*Mpcs|q_m5mm!D-fFQt>+^q2 zc&%o~h+561%~Qj+QZW{rtyBq=`uqD5kvc$#yR25MXP)qSbG_bQ-R{{Q&wRIgj@zvw zsXL1VkRC_q3DEPoE?fvh05NJ01~-7}H0w0IibfTk30KXo7~L`U zhZv5qt0aaHy^Y2S$N>qv8C0tK_U(&rlh7N5EjgJH3muhY5)hhJcKPM- z?(V-|hA1a^5`CpmE{8+fbtzbjy(Ndr3ChJ`I5~8?=|Pj#s#&elOQA`V-ml#*6OZ%7 z8V4v>r%v*y(u6m{M;>_uk%2N!XKg!E*YPKLuC62d@YiB1OD4<~a*#)>lfB*!mRx^3jKS(raD;gat)kH8L#>ZMss_B8m_W<&YH*B_J3JZbs&1o{O z6(1+@Mff;j@ai8Js%d-TDG#U-`?HnPZyjtQagFmS>Yh0upy;te7VLHvPu1*R1ChBXKq z5&9N;5fD4p3m7~U#oz%H;>8M%wg=z=ZKSawQiZ+?R@7vMn+oxfgk{UobwC|94445` zh;k*2ngmt^tB--*!w)~K*f45FX$c08sA(tZI_!Ip_lrlbEwC$Y_m?ggY~CKHlMN1X zy>rh!_sJ)pG^~`^P*5zJh`4Tfpc5%S##=~caRrY;)dwGZz$OK;IKVRu-o+PRjG2pK zJNd%-y!jf@&%>M%wh#�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 C(I|wOqVUoS3(?RIzugTbee`<3n1w$Zv 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
    NwBB4c!^<%Dp5xSc$5-EQL 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 z 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 literal 0 HcmV?d00001 diff --git a/MindSPONGE/applications/research/AlphaFold3/requirements.txt b/MindSPONGE/applications/research/AlphaFold3/requirements.txt new file mode 100644 index 000000000..1c230c665 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/requirements.txt @@ -0,0 +1,6 @@ +mindSpore==2.5.0 +absl-py==2.1.0 +numpy==1.26.0 +rdkit==2024.3.5 +scipy==1.14.1 +tqdm==4.67.0 \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py b/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py new file mode 100644 index 000000000..a9f2fe9bf --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py @@ -0,0 +1,687 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +from collections.abc import Callable, Iterable, Sequence +import csv +import dataclasses +import datetime +import functools +import multiprocessing +import os +import pathlib +import shutil +import string +import textwrap +import time +import typing +from typing import Protocol, Self, TypeVar, overload + +from absl import app +from absl import flags +from alphafold3.common import base_config +from alphafold3.common import folding_input +from alphafold3.common import resources +from alphafold3.constants import chemical_components +import alphafold3.cpp +from alphafold3.data import featurisation +from alphafold3.data import pipeline +from alphafold3.utils.attention import attention +from alphafold3.model import features +from alphafold3.model.diffusion.load_ckpt import load_diffuser +# from alphafold3.model import params +from alphafold3.model import post_processing +from alphafold3.model.components import base_model +from alphafold3.model.components import utils +from alphafold3.model.diffusion import model as diffusion_model +from alphafold3.model.feat_batch import Batch +import mindspore as ms +import numpy as np + + +_HOME_DIR = pathlib.Path(os.environ.get('HOME')) +_DEFAULT_MODEL_DIR = _HOME_DIR / 'ckpt' +_DEFAULT_DB_DIR = _HOME_DIR / 'public_databases' + + +# Input and output paths. +_JSON_PATH = flags.DEFINE_string( + 'json_path', + None, + 'Path to the input JSON file.', +) +_INPUT_DIR = flags.DEFINE_string( + 'input_dir', + None, + 'Path to the directory containing input JSON files.', +) +_OUTPUT_DIR = flags.DEFINE_string( + 'output_dir', + None, + 'Path to a directory where the results will be saved.', +) +MODEL_DIR = flags.DEFINE_string( + 'model_dir', + _DEFAULT_MODEL_DIR.as_posix(), + 'Path to the model to use for inference.', +) + +# Control which stages to run. +_RUN_DATA_PIPELINE = flags.DEFINE_bool( + 'run_data_pipeline', + True, + 'Whether to run the data pipeline on the fold inputs.', +) +_RUN_INFERENCE = flags.DEFINE_bool( + 'run_inference', + True, + 'Whether to run inference on the fold inputs.', +) + +# Binary paths. +_JACKHMMER_BINARY_PATH = flags.DEFINE_string( + 'jackhmmer_binary_path', + shutil.which('jackhmmer'), + 'Path to the Jackhmmer binary.', +) +_NHMMER_BINARY_PATH = flags.DEFINE_string( + 'nhmmer_binary_path', + shutil.which('nhmmer'), + 'Path to the Nhmmer binary.', +) +_HMMALIGN_BINARY_PATH = flags.DEFINE_string( + 'hmmalign_binary_path', + shutil.which('hmmalign'), + 'Path to the Hmmalign binary.', +) +_HMMSEARCH_BINARY_PATH = flags.DEFINE_string( + 'hmmsearch_binary_path', + shutil.which('hmmsearch'), + 'Path to the Hmmsearch binary.', +) +_HMMBUILD_BINARY_PATH = flags.DEFINE_string( + 'hmmbuild_binary_path', + shutil.which('hmmbuild'), + 'Path to the Hmmbuild binary.', +) + +# Database paths. +DB_DIR = flags.DEFINE_multi_string( + 'db_dir', + (_DEFAULT_DB_DIR.as_posix(),), + 'Path to the directory containing the databases. Can be specified multiple' + ' times to search multiple directories in order.', +) + +_SMALL_BFD_DATABASE_PATH = flags.DEFINE_string( + 'small_bfd_database_path', + '${DB_DIR}/bfd-first_non_consensus_sequences.fasta', + 'Small BFD database path, used for protein MSA search.', +) +_MGNIFY_DATABASE_PATH = flags.DEFINE_string( + 'mgnify_database_path', + '${DB_DIR}/mgy_clusters_2022_05.fa', + 'Mgnify database path, used for protein MSA search.', +) +_UNIPROT_CLUSTER_ANNOT_DATABASE_PATH = flags.DEFINE_string( + 'uniprot_cluster_annot_database_path', + '${DB_DIR}/uniprot_all_2021_04.fa', + 'UniProt database path, used for protein paired MSA search.', +) +_UNIREF90_DATABASE_PATH = flags.DEFINE_string( + 'uniref90_database_path', + '${DB_DIR}/uniref90_2022_05.fa', + 'UniRef90 database path, used for MSA search. The MSA obtained by ' + 'searching it is used to construct the profile for template search.', +) +_NTRNA_DATABASE_PATH = flags.DEFINE_string( + 'ntrna_database_path', + '${DB_DIR}/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta', + 'NT-RNA database path, used for RNA MSA search.', +) +_RFAM_DATABASE_PATH = flags.DEFINE_string( + 'rfam_database_path', + '${DB_DIR}/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta', + 'Rfam database path, used for RNA MSA search.', +) +_RNA_CENTRAL_DATABASE_PATH = flags.DEFINE_string( + 'rna_central_database_path', + '${DB_DIR}/rnacentral_active_seq_id_90_cov_80_linclust.fasta', + 'RNAcentral database path, used for RNA MSA search.', +) +_PDB_DATABASE_PATH = flags.DEFINE_string( + 'pdb_database_path', + '${DB_DIR}/mmcif_files', + 'PDB database directory with mmCIF files path, used for template search.', +) +_SEQRES_DATABASE_PATH = flags.DEFINE_string( + 'seqres_database_path', + '${DB_DIR}/pdb_seqres_2022_09_28.fasta', + 'PDB sequence database path, used for template search.', +) + +# Number of CPUs to use for MSA tools. +_JACKHMMER_N_CPU = flags.DEFINE_integer( + 'jackhmmer_n_cpu', + min(multiprocessing.cpu_count(), 8), + 'Number of CPUs to use for Jackhmmer. Default to min(cpu_count, 8). Going' + ' beyond 8 CPUs provides very little additional speedup.', +) +_NHMMER_N_CPU = flags.DEFINE_integer( + 'nhmmer_n_cpu', + min(multiprocessing.cpu_count(), 8), + 'Number of CPUs to use for Nhmmer. Default to min(cpu_count, 8). Going' + ' beyond 8 CPUs provides very little additional speedup.', +) + +# Template search configuration. +_MAX_TEMPLATE_DATE = flags.DEFINE_string( + 'max_template_date', + '2021-09-30', # By default, use the date from the AlphaFold 3 paper. + 'Maximum template release date to consider. Format: YYYY-MM-DD. All ' + 'templates released after this date will be ignored.', +) + + +_BUCKETS = flags.DEFINE_list( + 'buckets', + # pyformat: disable + ['256', '512', '768', '1024', '1280', '1536', '2048', '2560', '3072', + '3584', '4096', '4608', '5120'], + # pyformat: enable + 'Strictly increasing order of token sizes for which to cache compilations.' + ' For any input with more tokens than the largest bucket size, a new bucket' + ' is created for exactly that number of tokens.', +) +_FLASH_ATTENTION_IMPLEMENTATION = flags.DEFINE_enum( + 'flash_attention_implementation', + default='ms', + enum_values=['ms'], + help=( + "Flash attention implementation to use. 'triton' and 'cudnn' uses a" + ' Triton and cuDNN flash attention implementation, respectively. The' + ' Triton kernel is fastest and has been tested more thoroughly. The' + " Triton and cuDNN kernels require Ampere GPUs or later. 'xla' uses an" + ' XLA attention implementation (no flash attention) and is portable' + ' across GPU devices.' + ), +) + + +class ConfigurableModel(Protocol): + """A model with a nested config class.""" + + class Config(base_config.BaseConfig): + ... + + def __call__(self, config: Config) -> Self: + ... + + @classmethod + def get_inference_result( + cls: Self, + batch: features.BatchDict, + result: base_model.ModelResult, + target_name: str = '', + ) -> Iterable[base_model.InferenceResult]: + ... + + +ModelT = TypeVar('ModelT', bound=ConfigurableModel) + + +def make_model_config(): + print('not implemented make_model_config') + return 'ab' + + +def make_model_config( + *, + model_class: type[ModelT] = diffusion_model.Diffuser, + flash_attention_implementation: attention.Implementation = 'ms', +): + config = model_class.Config() + if hasattr(config, '_configglobal'): + config.global_config.flash_attention_implementation = ( + flash_attention_implementation + ) + return config + + +class ModelRunner: + """Helper class to run structure prediction stages.""" + + def __init__( + self, + model_class: ConfigurableModel, + config: base_config.BaseConfig, + model_dir: pathlib.Path, + ): + self._model_class = model_class + self._model_config = config + self._model_dir = model_dir + + @functools.cached_property + def model_params(self): + """Loads model parameters from the model directory.""" + # Load parameters from checkpoint file + # param_dict = ms.load_checkpoint(self._model_dir / "test.ckpt") + # return param_dict + + @functools.cached_property + def _model( + self + ) -> Callable[[np.ndarray, features.BatchDict], base_model.ModelResult]: + """Loads model parameters and returns a model forward pass.""" + assert isinstance(self._model_config, self._model_class.Config) + + def forward_fn(batch): + num_residues = batch.token_features.residue_index.shape[0] + model = self._model_class(self._model_config, 447, (256, 447), (num_residues, 256, 128), (256, 256, 128), + (256, 384), (256, 24, 3), 128, 4, dtype=ms.float32) + load_diffuser(model, self._model_dir, dtype=ms.float32) + res = model(batch, 42) + return res + + return forward_fn + + def run_inference( + self, featurised_example: features.BatchDict + ) -> base_model.ModelResult: + """Computes a forward pass of the model on a featurised example.""" + featurised_example = Batch.from_data_dict(featurised_example) + featurised_example.convert_to_tensor(ms.float32) + + result = self._model(featurised_example) + + # Convert identifier to bytes + if '__identifier__' in result: + result['__identifier__'] = result['__identifier__'].tobytes() + return result + + def extract_structures( + self, + batch: features.BatchDict, + result: base_model.ModelResult, + target_name: str, + ) -> list[base_model.InferenceResult]: + """Generates structures from model outputs.""" + batch = Batch.from_data_dict(batch) + batch.convert_to_tensor(ms.float32) + return list( + self._model_class.get_inference_result( + batch=batch, result=result, target_name=target_name + ) + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class ResultsForSeed: + """Stores the inference results (diffusion samples) for a single seed. + + Attributes: + seed: The seed used to generate the samples. + inference_results: The inference results, one per sample. + full_fold_input: The fold input that must also include the results of + running the data pipeline - MSA and templates. + """ + + seed: int + inference_results: Sequence[base_model.InferenceResult] + full_fold_input: folding_input.Input + + +def predict_structure( + fold_input: folding_input.Input, + model_runner: ModelRunner, + buckets: Sequence[int] | None = None, +) -> Sequence[ResultsForSeed]: + """Runs the full inference pipeline to predict structures for each seed.""" + + print(f'Featurising data for seeds {fold_input.rng_seeds}...') + featurisation_start_time = time.time() + ccd = chemical_components.cached_ccd(user_ccd=fold_input.user_ccd) + featurised_examples = featurisation.featurise_input( + fold_input=fold_input, buckets=buckets, ccd=ccd, verbose=True + ) + print( + f'Featurising data for seeds {fold_input.rng_seeds} took ' + f' {time.time() - featurisation_start_time:.2f} seconds.' + ) + all_inference_start_time = time.time() + all_inference_results = [] + for seed, example in zip(fold_input.rng_seeds, featurised_examples): + print(f'Running model inference for seed {seed}...') + inference_start_time = time.time() + result = model_runner.run_inference(example) + print( + f'Running model inference for seed {seed} took ' + f' {time.time() - inference_start_time:.2f} seconds.' + ) + print( + f'Extracting output structures (one per sample) for seed {seed}...') + extract_structures = time.time() + inference_results = model_runner.extract_structures( + batch=example, result=result, target_name=fold_input.name + ) + print( + f'Extracting output structures (one per sample) for seed {seed} took ' + f' {time.time() - extract_structures:.2f} seconds.' + ) + all_inference_results.append( + ResultsForSeed( + seed=seed, + inference_results=inference_results, + full_fold_input=fold_input, + ) + ) + print( + 'Running model inference and extracting output structures for seed' + f' {seed} took {time.time() - inference_start_time:.2f} seconds.' + ) + print( + 'Running model inference and extracting output structures for seeds' + f' {fold_input.rng_seeds} took ' + f' {time.time() - all_inference_start_time:.2f} seconds.' + ) + return all_inference_results + + +def write_fold_input_json( + fold_input: folding_input.Input, + output_dir: os.PathLike[str] | str, +) -> None: + """Writes the input JSON to the output directory.""" + os.makedirs(output_dir, exist_ok=True) + with open(os.path.join(output_dir, f'{fold_input.sanitised_name()}_data.json'), 'wt') as f: + f.write(fold_input.to_json()) + + +def write_outputs( + all_inference_results: Sequence[ResultsForSeed], + output_dir: os.PathLike[str] | str, + job_name: str, +) -> None: + """Writes outputs to the specified output directory.""" + ranking_scores = [] + max_ranking_score = None + max_ranking_result = None + + os.makedirs(output_dir, exist_ok=True) + for results_for_seed in all_inference_results: + seed = results_for_seed.seed + for sample_idx, result in enumerate(results_for_seed.inference_results): + sample_dir = os.path.join( + output_dir, f'seed-{seed}_sample-{sample_idx}') + os.makedirs(sample_dir, exist_ok=True) + post_processing.write_output( + inference_result=result, output_dir=sample_dir + ) + ranking_score = float(result.metadata['ranking_score']) + ranking_scores.append((seed, sample_idx, ranking_score)) + if max_ranking_score is None or ranking_score > max_ranking_score: + max_ranking_score = ranking_score + max_ranking_result = result + + if max_ranking_result is not None: # True iff ranking_scores non-empty. + post_processing.write_output( + inference_result=max_ranking_result, + output_dir=output_dir, + # The output terms of use are the same for all seeds/samples. + # terms_of_use=output_terms, + terms_of_use=None, + name=job_name, + ) + # Save csv of ranking scores with seeds and sample indices, to allow easier + # comparison of ranking scores across different runs. + with open(os.path.join(output_dir, 'ranking_scores.csv'), 'wt') as f: + writer = csv.writer(f) + writer.writerow(['seed', 'sample', 'ranking_score']) + writer.writerows(ranking_scores) + + +@overload +def process_fold_input( + fold_input: folding_input.Input, + data_pipeline_config: pipeline.DataPipelineConfig | None, + model_runner: None, + output_dir: os.PathLike[str] | str, + buckets: Sequence[int] | None = None, +) -> folding_input.Input: + ... + + +@overload +def process_fold_input( + fold_input: folding_input.Input, + data_pipeline_config: pipeline.DataPipelineConfig | None, + model_runner: ModelRunner, + output_dir: os.PathLike[str] | str, + buckets: Sequence[int] | None = None, +) -> Sequence[ResultsForSeed]: + ... + + +def replace_db_dir(path_with_db_dir: str, db_dirs: Sequence[str]) -> str: + """Replaces the DB_DIR placeholder in a path with the given DB_DIR.""" + template = string.Template(path_with_db_dir) + if 'DB_DIR' in template.get_identifiers(): + for db_dir in db_dirs: + path = template.substitute(DB_DIR=db_dir) + if os.path.exists(path): + return path + raise FileNotFoundError( + f'{path_with_db_dir} with ${{DB_DIR}} not found in any of {db_dirs}.' + ) + if not os.path.exists(path_with_db_dir): + raise FileNotFoundError(f'{path_with_db_dir} does not exist.') + return path_with_db_dir + + +def process_fold_input( + fold_input: folding_input.Input, + data_pipeline_config: pipeline.DataPipelineConfig | None, + model_runner: ModelRunner | None, + output_dir: os.PathLike[str] | str, + buckets: Sequence[int] | None = None, +) -> folding_input.Input | Sequence[ResultsForSeed]: + """Runs data pipeline and/or inference on a single fold input. + + Args: + fold_input: Fold input to process. + data_pipeline_config: Data pipeline config to use. If None, skip the data + pipeline. + model_runner: Model runner to use. If None, skip inference. + output_dir: Output directory to write to. + buckets: Bucket sizes to pad the data to, to avoid excessive re-compilation + of the model. If None, calculate the appropriate bucket size from the + number of tokens. If not None, must be a sequence of at least one integer, + in strictly increasing order. Will raise an error if the number of tokens + is more than the largest bucket size. + + Returns: + The processed fold input, or the inference results for each seed. + + Raises: + ValueError: If the fold input has no chains. + """ + print(f'Processing fold input {fold_input.name}') + + if not fold_input.chains: + raise ValueError('Fold input has no chains.') + + if os.path.exists(output_dir) and os.listdir(output_dir): + new_output_dir = ( + f'{output_dir}_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}' + ) + print( + f'Output directory {output_dir} exists and non-empty, using instead ' + f' {new_output_dir}.' + ) + output_dir = new_output_dir + + if model_runner is not None: + # If we're running inference, check we can load the model parameters before + # (possibly) launching the data pipeline. + print('Checking we can load the model parameters...') + _ = model_runner.model_params + + if data_pipeline_config is None: + print('Skipping data pipeline...') + else: + print('Running data pipeline...') + fold_input = pipeline.DataPipeline( + data_pipeline_config).process(fold_input) + + print(f'Output directory: {output_dir}') + print(f'Writing model input JSON to {output_dir}') + write_fold_input_json(fold_input, output_dir) + if model_runner is None: + print('Skipping inference...') + output = fold_input + else: + print( + f'Predicting 3D structure for {fold_input.name} for seed(s)' + f' {fold_input.rng_seeds}...' + ) + all_inference_results = predict_structure( + fold_input=fold_input, + model_runner=model_runner, + buckets=buckets, + ) + print( + f'Writing outputs for {fold_input.name} for seed(s)' + f' {fold_input.rng_seeds}...' + ) + write_outputs( + all_inference_results=all_inference_results, + output_dir=output_dir, + job_name=fold_input.sanitised_name(), + ) + output = all_inference_results + + print(f'Done processing fold input {fold_input.name}.') + return output + + +def main(_): + + if _JSON_PATH.value is None == _INPUT_DIR.value is None: + raise ValueError( + 'Exactly one of --json_path or --input_dir must be specified.' + ) + + if not _RUN_INFERENCE.value and not _RUN_DATA_PIPELINE.value: + raise ValueError( + 'At least one of --run_inference or --run_data_pipeline must be' + ' set to true.' + ) + + if _INPUT_DIR.value is not None: + fold_inputs = folding_input.load_fold_inputs_from_dir( + pathlib.Path(_INPUT_DIR.value) + ) + elif _JSON_PATH.value is not None: + fold_inputs = folding_input.load_fold_inputs_from_path( + pathlib.Path(_JSON_PATH.value) + ) + else: + raise AssertionError( + 'Exactly one of --json_path or --input_dir must be specified.' + ) + + # Make sure we can create the output directory before running anything. + try: + os.makedirs(_OUTPUT_DIR.value, exist_ok=True) + except OSError as e: + print(f'Failed to create output directory {_OUTPUT_DIR.value}: {e}') + raise + + notice = textwrap.wrap( + 'Running AlphaFold 3. Please note that standard AlphaFold 3 model' + ' parameters are only available under terms of use provided at' + ' https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.' + ' If you do not agree to these terms and are using AlphaFold 3 derived' + ' model parameters, cancel execution of AlphaFold 3 inference with' + ' CTRL-C, and do not use the model parameters.', + break_long_words=False, + break_on_hyphens=False, + width=80, + ) + print('\n'.join(notice)) + + if _RUN_DATA_PIPELINE.value: + def expand_path(x): + return replace_db_dir(x, DB_DIR.value) + max_template_date = datetime.date.fromisoformat( + _MAX_TEMPLATE_DATE.value) + data_pipeline_config = pipeline.DataPipelineConfig( + jackhmmer_binary_path=_JACKHMMER_BINARY_PATH.value, + nhmmer_binary_path=_NHMMER_BINARY_PATH.value, + hmmalign_binary_path=_HMMALIGN_BINARY_PATH.value, + hmmsearch_binary_path=_HMMSEARCH_BINARY_PATH.value, + hmmbuild_binary_path=_HMMBUILD_BINARY_PATH.value, + small_bfd_database_path=expand_path( + _SMALL_BFD_DATABASE_PATH.value), + mgnify_database_path=expand_path(_MGNIFY_DATABASE_PATH.value), + uniprot_cluster_annot_database_path=expand_path( + _UNIPROT_CLUSTER_ANNOT_DATABASE_PATH.value + ), + uniref90_database_path=expand_path(_UNIREF90_DATABASE_PATH.value), + ntrna_database_path=expand_path(_NTRNA_DATABASE_PATH.value), + rfam_database_path=expand_path(_RFAM_DATABASE_PATH.value), + rna_central_database_path=expand_path( + _RNA_CENTRAL_DATABASE_PATH.value), + pdb_database_path=expand_path(_PDB_DATABASE_PATH.value), + seqres_database_path=expand_path(_SEQRES_DATABASE_PATH.value), + jackhmmer_n_cpu=_JACKHMMER_N_CPU.value, + nhmmer_n_cpu=_NHMMER_N_CPU.value, + max_template_date=max_template_date, + ) + else: + print('Skipping running the data pipeline.') + data_pipeline_config = None + + if _RUN_INFERENCE.value: + print('Building model from scratch...') + model_runner = ModelRunner( + model_class=diffusion_model.Diffuser, + config=make_model_config( + flash_attention_implementation=typing.cast( + attention.Implementation, _FLASH_ATTENTION_IMPLEMENTATION.value + ) + ), + model_dir=pathlib.Path(MODEL_DIR.value), + ) + else: + print('Skipping running model inference.') + model_runner = None + + print(f'Processing {len(fold_inputs)} fold inputs.') + for fold_input in fold_inputs: + process_fold_input( + fold_input=fold_input, + data_pipeline_config=data_pipeline_config, + model_runner=model_runner, + output_dir=os.path.join( + _OUTPUT_DIR.value, fold_input.sanitised_name()), + buckets=tuple(int(bucket) for bucket in _BUCKETS.value), + ) + + print(f'Done processing {len(fold_inputs)} fold inputs.') + + +if __name__ == '__main__': + flags.mark_flags_as_required([ + 'output_dir', + ]) + app.run(main) diff --git a/MindSPONGE/applications/research/AlphaFold3/set_path.sh b/MindSPONGE/applications/research/AlphaFold3/set_path.sh new file mode 100644 index 000000000..f2efd467f --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/set_path.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Get the script directory to make paths more reliable +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# From AlphaFold3 directory, go up to the mindscience directory +MINDSCIENCE_PATH="$(cd "$SCRIPT_DIR/../../../.." && pwd)" + +# Check if the base directory exists +if [ ! -d "$MINDSCIENCE_PATH" ]; then + echo "Error: MindScience path not found: $MINDSCIENCE_PATH" + echo "Please run this script from the correct directory" + exit 1 +fi + +# Function to add to PYTHONPATH if directory exists +add_to_pythonpath() { + local dir_path="$1" + if [ -d "$dir_path" ]; then + export PYTHONPATH="$PYTHONPATH:$dir_path" + echo "Added to PYTHONPATH: $dir_path" + else + echo "Warning: Directory not found, skipping: $dir_path" + fi +} + +add_to_pythonpath "$MINDSCIENCE_PATH/MindSPONGE/src" +add_to_pythonpath "$MINDSCIENCE_PATH/MindChemistry" +add_to_pythonpath "$MINDSCIENCE_PATH/MindSPONGE/applications/research/AlphaFold3/src" + +# Add directories to PATH +export PATH=$PATH:/hmmer/bin + +# Display current PYTHONPATH +echo "Current PYTHONPATH:" +echo "$PYTHONPATH" | tr ':' '\n' | sed 's/^/ /' + +echo "Environment setup completed." diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/__init__.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/build_data.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/build_data.py new file mode 100644 index 000000000..58ae0c88b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/build_data.py @@ -0,0 +1,45 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Script for building intermediate data.""" + +from importlib import resources +import pathlib +import site + +import alphafold3.constants.converters +from alphafold3.constants.converters import ccd_pickle_gen +from alphafold3.constants.converters import chemical_component_sets_gen + + +def build_data(): + """Builds intermediate data.""" + for site_path in site.getsitepackages(): + path = pathlib.Path(site_path) / 'share/libcifpp/components.cif' + if path.exists(): + cif_path = path + break + else: + raise ValueError('Could not find components.cif') + + out_root = resources.files(alphafold3.constants.converters) + ccd_pickle_path = out_root.joinpath('ccd.pickle') + chemical_component_sets_pickle_path = out_root.joinpath( + 'chemical_component_sets.pickle' + ) + ccd_pickle_gen.main(['', str(cif_path), str(ccd_pickle_path)]) + chemical_component_sets_gen.main( + ['', str(chemical_component_sets_pickle_path)] + ) + + +if __name__ == '__main__': + build_data() diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/base_config.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/base_config.py new file mode 100644 index 000000000..27f6eba12 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/base_config.py @@ -0,0 +1,151 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ +"""Config for the protein folding model and experiment.""" + +from collections.abc import Mapping +import copy +import dataclasses +import types +import typing +from typing import Any, ClassVar, TypeVar + + +_T = TypeVar('_T') +_ConfigT = TypeVar('_ConfigT', bound='BaseConfig') + + +def _strip_optional(t: type[Any]) -> type[Any]: + """Transforms type annotations of the form `T | None` to `T`.""" + if typing.get_origin(t) in (typing.Union, types.UnionType): + args = set(typing.get_args(t)) - {types.NoneType} + if len(args) == 1: + return args.pop() + return t + + +_NO_UPDATE = object() + + +class _Autocreate: + + def __init__(self, **defaults: Any): + self.defaults = defaults + + +def autocreate(**defaults: Any) -> Any: + """Marks a field as having a default factory derived from its type.""" + return _Autocreate(**defaults) + + +def _clone_field( + field: dataclasses.Field[_T], new_default: _T +) -> dataclasses.Field[_T]: + if new_default is _NO_UPDATE: + return copy.copy(field) + return dataclasses.field( + default=new_default, + init=True, + kw_only=True, + repr=field.repr, + hash=field.hash, + compare=field.compare, + metadata=field.metadata, + ) + + +@typing.dataclass_transform() +class ConfigMeta(type): + """Metaclass that synthesizes a __post_init__ that coerces dicts to Config subclass instances.""" + + def __new__(mcs, name, bases, classdict): + cls = super().__new__(mcs, name, bases, classdict) + + def _coercable_fields(self) -> Mapping[str, tuple[ConfigMeta, Any]]: + type_hints = typing.get_type_hints(self.__class__) + fields = dataclasses.fields(self.__class__) + field_to_type_and_default = { + field.name: (_strip_optional( + type_hints[field.name]), field.default) + for field in fields + } + coercable_fields = { + f: t + for f, t in field_to_type_and_default.items() + if issubclass(type(t[0]), ConfigMeta) + } + return coercable_fields + + cls._coercable_fields = property(_coercable_fields) + + old_post_init = getattr(cls, '__post_init__', None) + + def _post_init(self) -> None: + # Use get_type_hints instead of Field.type to ensure that forward + # references are resolved. + for field_name, ( + field_type, + field_default, + ) in self._coercable_fields.items(): # pylint: disable=protected-access + field_value = getattr(self, field_name) + if field_value is None: + continue + try: + match field_value: + case _Autocreate(): + # Construct from field defaults. + setattr(self, field_name, field_type( + **field_value.defaults)) + case Mapping(): + # Field value is not yet a `Config` instance; Assume we can create + # one by splatting keys and values. + args = {} + # Apply default args first, if present. + if isinstance(field_default, _Autocreate): + args.update(field_default.defaults) + args.update(field_value) + setattr(self, field_name, field_type(**args)) + case _: + pass + except TypeError as e: + raise TypeError( + f'Failure while coercing field {field_name!r} of' + f' {self.__class__.__qualname__}' + ) from e + if old_post_init: + old_post_init(self) + + cls.__post_init__ = _post_init + + return dataclasses.dataclass(kw_only=True)(cls) + + +class BaseConfig(metaclass=ConfigMeta): + """Config base class. + + Subclassing Config automatically makes the subclass a kw_only dataclass with + a `__post_init__` that coerces Config-subclass field values from mappings to + instances of the right type. + """ + # Provided by dataclasses.make_dataclass + __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] + + # Overridden by metaclass + @property + def _coercable_fields(self) -> Mapping[str, tuple[type['BaseConfig'], Any]]: + return {} + + def as_dict(self) -> Mapping[str, Any]: + result = dataclasses.asdict(self) + for field_name in self._coercable_fields: + field_value = getattr(self, field_name, None) + if isinstance(field_value, BaseConfig): + result[field_name] = field_value.as_dict() + return result diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py new file mode 100644 index 000000000..cba8d0556 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py @@ -0,0 +1,1115 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Model input dataclass.""" + +from collections.abc import Collection, Mapping, Sequence +import dataclasses +import json +import logging +import pathlib +import random +import re +import string +from typing_extensions import Any, Final, Self, TypeAlias + +from alphafold3 import structure +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.structure import mmcif as mmcif_lib +import rdkit.Chem as rd_chem + + +BondAtomId: TypeAlias = tuple[str, int, str] + +JSON_DIALECT: Final[str] = 'alphafold3' +JSON_VERSION: Final[int] = 1 + +ALPHAFOLDSERVER_JSON_DIALECT: Final[str] = 'alphafoldserver' +ALPHAFOLDSERVER_JSON_VERSION: Final[int] = 1 + + +def _validate_keys(actual: Collection[str], expected: Collection[str]): + """Validates that the JSON doesn't contain any extra unwanted keys.""" + if bad_keys := set(actual) - set(expected): + raise ValueError( + f'Unexpected JSON keys in: {", ".join(sorted(bad_keys))}') + + +class Template: + """Structural template input.""" + + __slots__ = ('_mmcif', '_query_to_template') + + def __init__(self, mmcif: str, query_to_template_map: Mapping[int, int]): + """Initializes the template. + + Args: + mmcif: The structural template in mmCIF format. The mmCIF should have only + one protein chain. + query_to_template_map: A mapping from query residue index to template + residue index. + """ + self._mmcif = mmcif + # Needed to make the Template class hashable. + self._query_to_template = tuple(query_to_template_map.items()) + + @property + def query_to_template_map(self) -> Mapping[int, int]: + return dict(self._query_to_template) + + @property + def mmcif(self) -> str: + return self._mmcif + + def __hash__(self) -> int: + return hash((self._mmcif, tuple(sorted(self._query_to_template)))) + + def __eq__(self, other: Self) -> bool: + mmcifs_equal = self._mmcif == other._mmcif + maps_equal = sorted(self._query_to_template) == sorted( + other._query_to_template + ) + return mmcifs_equal and maps_equal + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class ProteinChain: + """Protein chain input. + + Attributes: + id: Unique protein chain identifier. + sequence: The amino acid sequence of the chain. + ptms: A list of tuples containing the post-translational modification type + and the (1-based) residue index where the modification is applied. + paired_msa: Paired A3M-formatted MSA for this chain. This MSA is not + deduplicated and will be used to compute paired features. If None, this + field is unset and must be filled in by the data pipeline before + featurisation. If set to an empty string, it will be treated as a custom + MSA with no sequences. + unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be + deduplicated and used to compute unpaired features. If None, this field is + unset and must be filled in by the data pipeline before featurisation. If + set to an empty string, it will be treated as a custom MSA with no + sequences. + templates: A list of structural templates for this chain. If None, this + field is unset and must be filled in by the data pipeline before + featurisation. The list can be empty or contain up to 20 templates. + """ + + id: str + sequence: str + ptms: Sequence[tuple[str, int]] + paired_msa: str | None = None + unpaired_msa: str | None = None + templates: Sequence[Template] | None = None + + def __post_init__(self): + if not all(res.isalpha() for res in self.sequence): + raise ValueError( + f'Protein must contain only letters, got "{self.sequence}"' + ) + if any(not 0 < mod[1] <= len(self.sequence) for mod in self.ptms): + raise ValueError( + f'Invalid protein modification index: {self.ptms}') + + # Use hashable types for ptms and templates. + if self.ptms is not None: + object.__setattr__(self, 'ptms', tuple(self.ptms)) + if self.templates is not None: + object.__setattr__(self, 'templates', tuple(self.templates)) + + @classmethod + def from_alphafoldserver_dict( + cls, json_dict: Mapping[str, Any], seq_id: str + ) -> Self: + """Constructs ProteinChain from the AlphaFoldServer JSON dict.""" + _validate_keys( + json_dict.keys(), + {'sequence', 'glycans', 'modifications', 'count'}, + ) + sequence = json_dict['sequence'] + + if 'glycans' in json_dict: + raise ValueError( + f'Specifying glycans in the `{ALPHAFOLDSERVER_JSON_DIALECT}` format' + ' is not currently supported.' + ) + + ptms = [ + (mod['ptmType'].removeprefix('CCD_'), mod['ptmPosition']) + for mod in json_dict.get('modifications', []) + ] + return cls(id=seq_id, sequence=sequence, ptms=ptms) + + @classmethod + def from_dict( + cls, json_dict: Mapping[str, Any], seq_id: str | None = None + ) -> Self: + """Constructs ProteinChain from the AlphaFold JSON dict.""" + json_dict = json_dict['protein'] + _validate_keys( + json_dict.keys(), + { + 'id', + 'sequence', + 'modifications', + 'unpairedMsa', + 'pairedMsa', + 'templates', + }, + ) + + sequence = json_dict['sequence'] + ptms = [ + (mod['ptmType'], mod['ptmPosition']) + for mod in json_dict.get('modifications', []) + ] + + unpaired_msa = json_dict.get('unpairedMsa', None) + paired_msa = json_dict.get('pairedMsa', None) + + raw_templates = json_dict.get('templates', None) + + if raw_templates is None: + templates = None + else: + templates = [ + Template( + mmcif=template['mmcif'], + query_to_template_map=dict( + zip(template['queryIndices'], + template['templateIndices']) + ), + ) + for template in raw_templates + ] + + return cls( + id=seq_id or json_dict['id'], + sequence=sequence, + ptms=ptms, + paired_msa=paired_msa, + unpaired_msa=unpaired_msa, + templates=templates, + ) + + def to_dict(self) -> Mapping[str, Mapping[str, Any]]: + """Converts ProteinChain to an AlphaFold JSON dict.""" + if self.templates is None: + templates = None + else: + templates = [ + { + 'mmcif': template.mmcif, + 'queryIndices': list(template.query_to_template_map.keys()), + 'templateIndices': ( + list(template.query_to_template_map.values()) or None + ), + } + for template in self.templates + ] + contents = { + 'id': self.id, + 'sequence': self.sequence, + 'modifications': [ + {'ptmType': ptm[0], 'ptmPosition': ptm[1]} for ptm in self.ptms + ], + 'unpairedMsa': self.unpaired_msa, + 'pairedMsa': self.paired_msa, + 'templates': templates, + } + return {'protein': contents} + + def to_ccd_sequence(self) -> Sequence[str]: + """Converts to a sequence of CCD codes.""" + ccd_coded_seq = [ + residue_names.PROTEIN_COMMON_ONE_TO_THREE.get( + res, residue_names.UNK) + for res in self.sequence + ] + for ptm_code, ptm_index in self.ptms: + ccd_coded_seq[ptm_index - 1] = ptm_code + return ccd_coded_seq + + def fill_missing_fields(self) -> Self: + """Fill missing MSA and template fields with default values.""" + return dataclasses.replace( + self, + unpaired_msa=self.unpaired_msa or '', + paired_msa=self.paired_msa or '', + templates=self.templates or [], + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class RnaChain: + """RNA chain input. + + Attributes: + id: Unique RNA chain identifier. + sequence: The RNA sequence of the chain. + modifications: A list of tuples containing the modification type and the + (1-based) residue index where the modification is applied. + unpaired_msa: Unpaired A3M-formatted MSA for this chain. This will be + deduplicated and used to compute unpaired features. If None, this field is + unset and must be filled in by the data pipeline before featurisation. If + set to an empty string, it will be treated as a custom MSA with no + sequences. + """ + + id: str + sequence: str + modifications: Sequence[tuple[str, int]] + unpaired_msa: str | None = None + + def __post_init__(self): + if not all(res.isalpha() for res in self.sequence): + raise ValueError( + f'RNA must contain only letters, got "{self.sequence}"') + if any(not 0 < mod[1] <= len(self.sequence) for mod in self.modifications): + raise ValueError( + f'Invalid RNA modification index: {self.modifications}') + + # Use hashable types for modifications. + object.__setattr__(self, 'modifications', tuple(self.modifications)) + + @classmethod + def from_alphafoldserver_dict( + cls, json_dict: Mapping[str, Any], seq_id: str + ) -> Self: + """Constructs RnaChain from the AlphaFoldServer JSON dict.""" + _validate_keys(json_dict.keys(), { + 'sequence', 'modifications', 'count'}) + sequence = json_dict['sequence'] + modifications = [ + (mod['modificationType'].removeprefix('CCD_'), mod['basePosition']) + for mod in json_dict.get('modifications', []) + ] + return cls(id=seq_id, sequence=sequence, modifications=modifications) + + @classmethod + def from_dict( + cls, json_dict: Mapping[str, Any], seq_id: str | None = None + ) -> Self: + """Constructs RnaChain from the AlphaFold JSON dict.""" + json_dict = json_dict['rna'] + _validate_keys( + json_dict.keys(), {'id', 'sequence', + 'unpairedMsa', 'modifications'} + ) + sequence = json_dict['sequence'] + modifications = [ + (mod['modificationType'], mod['basePosition']) + for mod in json_dict.get('modifications', []) + ] + unpaired_msa = json_dict.get('unpairedMsa', None) + return cls( + id=seq_id or json_dict['id'], + sequence=sequence, + modifications=modifications, + unpaired_msa=unpaired_msa, + ) + + def to_dict(self) -> Mapping[str, Mapping[str, Any]]: + """Converts RnaChain to an AlphaFold JSON dict.""" + contents = { + 'id': self.id, + 'sequence': self.sequence, + 'modifications': [ + {'modificationType': mod[0], 'basePosition': mod[1]} + for mod in self.modifications + ], + 'unpairedMsa': self.unpaired_msa, + } + return {'rna': contents} + + def to_ccd_sequence(self) -> Sequence[str]: + """Converts to a sequence of CCD codes.""" + mapping = { + r: r for r in residue_names.RNA_TYPES} # Same 1-letter and CCD. + ccd_coded_seq = [ + mapping.get(res, residue_names.UNK_RNA) for res in self.sequence + ] + for ccd_code, modification_index in self.modifications: + ccd_coded_seq[modification_index - 1] = ccd_code + return ccd_coded_seq + + def fill_missing_fields(self) -> Self: + """Fill missing MSA fields with default values.""" + return dataclasses.replace(self, unpaired_msa=self.unpaired_msa or '') + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class DnaChain: + """Single strand DNA chain input. + + Attributes: + id: Unique DNA chain identifier. + sequence: The DNA sequence of the chain. + modifications: A list of tuples containing the modification type and the + (1-based) residue index where the modification is applied. + """ + + id: str + sequence: str + modifications: Sequence[tuple[str, int]] + + def __post_init__(self): + if not all(res.isalpha() for res in self.sequence): + raise ValueError( + f'DNA must contain only letters, got "{self.sequence}"') + if any(not 0 < mod[1] <= len(self.sequence) for mod in self.modifications): + raise ValueError( + f'Invalid DNA modification index: {self.modifications}') + + # Use hashable types for modifications. + object.__setattr__(self, 'modifications', tuple(self.modifications)) + + @classmethod + def from_alphafoldserver_dict( + cls, json_dict: Mapping[str, Any], seq_id: str + ) -> Self: + """Constructs DnaChain from the AlphaFoldServer JSON dict.""" + _validate_keys(json_dict.keys(), { + 'sequence', 'modifications', 'count'}) + sequence = json_dict['sequence'] + modifications = [ + (mod['modificationType'].removeprefix('CCD_'), mod['basePosition']) + for mod in json_dict.get('modifications', []) + ] + return cls(id=seq_id, sequence=sequence, modifications=modifications) + + @classmethod + def from_dict( + cls, json_dict: Mapping[str, Any], seq_id: str | None = None + ) -> Self: + """Constructs DnaChain from the AlphaFold JSON dict.""" + json_dict = json_dict['dna'] + _validate_keys(json_dict.keys(), {'id', 'sequence', 'modifications'}) + sequence = json_dict['sequence'] + modifications = [ + (mod['modificationType'], mod['basePosition']) + for mod in json_dict.get('modifications', []) + ] + return cls( + id=seq_id or json_dict['id'], + sequence=sequence, + modifications=modifications, + ) + + def to_dict(self) -> Mapping[str, Mapping[str, Any]]: + """Converts DnaChain to an AlphaFold JSON dict.""" + contents = { + 'id': self.id, + 'sequence': self.sequence, + 'modifications': [ + {'modificationType': mod[0], 'basePosition': mod[1]} + for mod in self.modifications + ], + } + return {'dna': contents} + + def to_ccd_sequence(self) -> Sequence[str]: + """Converts to a sequence of CCD codes.""" + ccd_coded_seq = [ + residue_names.DNA_COMMON_ONE_TO_TWO.get(res, residue_names.UNK_DNA) + for res in self.sequence + ] + for ccd_code, modification_index in self.modifications: + ccd_coded_seq[modification_index - 1] = ccd_code + return ccd_coded_seq + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Ligand: + """Ligand input. + + Attributes: + id: Unique ligand "chain" identifier. + ccd_ids: The Chemical Component Dictionary or user-defined CCD IDs of the + chemical components of the ligand. Typically, this is just a single ID, + but some ligands are composed of multiple components. If that is the case, + a bond linking these components should be added to the bonded_atom_pairs + Input field. + smiles: The SMILES representation of the ligand. + """ + + id: str + ccd_ids: Sequence[str] | None = None + smiles: str | None = None + + def __post_init__(self): + if (self.ccd_ids is None) == (self.smiles is None): + raise ValueError('Ligand must have one of CCD ID or SMILES set.') + + if self.smiles is not None: + mol = rd_chem.MolFromSmiles(self.smiles) + if not mol: + raise ValueError( + f'Unable to make RDKit Mol from SMILES: {self.smiles}') + + # Use hashable types for ccd_ids. + if self.ccd_ids is not None: + object.__setattr__(self, 'ccd_ids', tuple(self.ccd_ids)) + + @classmethod + def from_alphafoldserver_dict( + cls, json_dict: Mapping[str, Any], seq_id: str + ) -> Self: + """Constructs Ligand from the AlphaFoldServer JSON dict.""" + # Ligand can be specified either as a ligand, or ion (special-case). + _validate_keys(json_dict.keys(), {'ligand', 'ion', 'count'}) + if 'ligand' in json_dict: + return cls(id=seq_id, ccd_ids=[json_dict['ligand'].removeprefix('CCD_')]) + elif 'ion' in json_dict: + return cls(id=seq_id, ccd_ids=[json_dict['ion']]) + else: + raise ValueError(f'Unknown ligand type: {json_dict}') + + @classmethod + def from_dict( + cls, json_dict: Mapping[str, Any], seq_id: str | None = None + ) -> Self: + """Constructs Ligand from the AlphaFold JSON dict.""" + json_dict = json_dict['ligand'] + _validate_keys(json_dict.keys(), {'id', 'ccdCodes', 'smiles'}) + if json_dict.get('ccdCodes') and json_dict.get('smiles'): + raise ValueError( + 'Ligand cannot have both CCD code and SMILES set at the same time, ' + f'got CCD: {json_dict["ccdCodes"]} and SMILES: {json_dict["smiles"]}' + ) + + if 'ccdCodes' in json_dict: + return cls(id=seq_id or json_dict['id'], ccd_ids=json_dict['ccdCodes']) + elif 'smiles' in json_dict: + return cls(id=seq_id or json_dict['id'], smiles=json_dict['smiles']) + else: + raise ValueError(f'Unknown ligand type: {json_dict}') + + def to_dict(self) -> Mapping[str, Any]: + """Converts Ligand to an AlphaFold JSON dict.""" + contents = {'id': self.id} + if self.ccd_ids is not None: + contents['ccdCodes'] = self.ccd_ids + if self.smiles is not None: + contents['smiles'] = self.smiles + return {'ligand': contents} + + +def _sample_rng_seed() -> int: + """Sample a random seed for AlphaFoldServer job.""" + # See https://alphafoldserver.com/faq#what-are-seeds-and-how-are-they-set. + return random.randint(0, 2**32 - 1) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Input: + """AlphaFold input. + + Attributes: + name: The name of the target. + chains: Protein chains, RNA chains, DNA chains, or ligands. + protein_chains: Protein chains. + rna_chains: RNA chains. + dna_chains: Single strand DNA chains. + ligands: Ligand (including ion) inputs. + rng_seeds: Random number generator seeds, one for each model execution. + bonded_atom_pairs: A list of tuples of atoms that are bonded to each other. + Each atom is defined by a tuple of (chain_id, res_id, atom_name). Chain + IDs must be set if there are any bonded atoms. Residue IDs are 1-indexed. + Atoms in ligands defined by SMILES can't be bonded since SMILES doesn't + define unique atom names. + user_ccd: Optional user-defined chemical component dictionary in the CIF + format. This can be used to provide additional CCD entries that are not + present in the default CCD and thus define arbitrary new ligands. This is + more expressive than SMILES since it allows to name all atoms within the + ligand which in turn makes it possible to define bonds using those atoms. + """ + + name: str + chains: Sequence[ProteinChain | RnaChain | DnaChain | Ligand] + rng_seeds: Sequence[int] + bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None = None + user_ccd: str | None = None + + def __post_init__(self): + if not self.rng_seeds: + raise ValueError('Input must have at least one RNG seed.') + + if not self.name.strip() or not self.sanitised_name(): + raise ValueError( + 'Input name must be non-empty and contain at least one valid' + ' character (letters, numbers, dots, dashes, underscores).' + ) + + chain_ids = [c.id for c in self.chains] + if any(not c.id.isalpha() or c.id.islower() for c in self.chains): + raise ValueError( + f'IDs must be upper case letters, got: {chain_ids}') + if len(set(chain_ids)) != len(chain_ids): + raise ValueError( + 'Input JSON contains sequences with duplicate IDs.') + + # Use hashable types for chains, rng_seeds, and bonded_atom_pairs. + object.__setattr__(self, 'chains', tuple(self.chains)) + object.__setattr__(self, 'rng_seeds', tuple(self.rng_seeds)) + if self.bonded_atom_pairs is not None: + object.__setattr__( + self, 'bonded_atom_pairs', tuple(self.bonded_atom_pairs) + ) + + @property + def protein_chains(self) -> Sequence[ProteinChain]: + return [chain for chain in self.chains if isinstance(chain, ProteinChain)] + + @property + def rna_chains(self) -> Sequence[RnaChain]: + return [chain for chain in self.chains if isinstance(chain, RnaChain)] + + @property + def dna_chains(self) -> Sequence[DnaChain]: + return [chain for chain in self.chains if isinstance(chain, DnaChain)] + + @property + def ligands(self) -> Sequence[Ligand]: + return [chain for chain in self.chains if isinstance(chain, Ligand)] + + @classmethod + def from_alphafoldserver_fold_job(cls, fold_job: Mapping[str, Any]) -> Self: + """Constructs Input from an AlphaFoldServer fold job.""" + + # Validate the fold job has the correct format. + _validate_keys( + fold_job.keys(), + {'name', 'modelSeeds', 'sequences', 'dialect', 'version'}, + ) + if 'dialect' not in fold_job and 'version' not in fold_job: + dialect = ALPHAFOLDSERVER_JSON_DIALECT + version = ALPHAFOLDSERVER_JSON_VERSION + elif 'dialect' in fold_job and 'version' in fold_job: + dialect = fold_job['dialect'] + version = fold_job['version'] + else: + raise ValueError( + 'AlphaFold Server input JSON must either contain both `dialect` and' + ' `version` fields, or neither. If neither is specified, it is' + f' assumed that `dialect="{ALPHAFOLDSERVER_JSON_DIALECT}"` and' + f' `version="{ALPHAFOLDSERVER_JSON_VERSION}"`.' + ) + + if dialect != ALPHAFOLDSERVER_JSON_DIALECT: + raise ValueError( + f'AlphaFold Server input JSON has unsupported dialect: {dialect}, ' + f'expected {ALPHAFOLDSERVER_JSON_DIALECT}.' + ) + + # For now, there is only one AlphaFold Server JSON version. + if version != ALPHAFOLDSERVER_JSON_VERSION: + raise ValueError( + f'AlphaFold Server input JSON has unsupported version: {version}, ' + f'expected {ALPHAFOLDSERVER_JSON_VERSION}.' + ) + + # Parse the chains. + chains = [] + for sequence in fold_job['sequences']: + if 'proteinChain' in sequence: + for _ in range(sequence['proteinChain'].get('count', 1)): + chains.append( + ProteinChain.from_alphafoldserver_dict( + sequence['proteinChain'], + seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), + ) + ) + elif 'rnaSequence' in sequence: + for _ in range(sequence['rnaSequence'].get('count', 1)): + chains.append( + RnaChain.from_alphafoldserver_dict( + sequence['rnaSequence'], + seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), + ) + ) + elif 'dnaSequence' in sequence: + for _ in range(sequence['dnaSequence'].get('count', 1)): + chains.append( + DnaChain.from_alphafoldserver_dict( + sequence['dnaSequence'], + seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), + ) + ) + elif 'ion' in sequence: + for _ in range(sequence['ion'].get('count', 1)): + chains.append( + Ligand.from_alphafoldserver_dict( + sequence['ion'], + seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), + ) + ) + elif 'ligand' in sequence: + for _ in range(sequence['ligand'].get('count', 1)): + chains.append( + Ligand.from_alphafoldserver_dict( + sequence['ligand'], + seq_id=mmcif_lib.int_id_to_str_id(len(chains) + 1), + ) + ) + else: + raise ValueError(f'Unknown sequence type: {sequence}') + + if 'modelSeeds' in fold_job and fold_job['modelSeeds']: + rng_seeds = [int(seed) for seed in fold_job['modelSeeds']] + else: + rng_seeds = [_sample_rng_seed()] + + return cls(name=fold_job['name'], chains=chains, rng_seeds=rng_seeds) + + @classmethod + def from_json(cls, json_str: str) -> Self: + """Loads the input from the AlphaFold JSON string.""" + raw_json = json.loads(json_str) + + _validate_keys( + raw_json.keys(), + { + 'dialect', + 'version', + 'name', + 'modelSeeds', + 'sequences', + 'bondedAtomPairs', + 'userCCD', + }, + ) + + if 'dialect' not in raw_json or 'version' not in raw_json: + raise ValueError( + 'AlphaFold 3 input JSON must contain `dialect` and `version` fields.' + ) + + if raw_json['dialect'] != JSON_DIALECT: + raise ValueError( + 'AlphaFold 3 input JSON has unsupported dialect:' + f' {raw_json["dialect"]}, expected {JSON_DIALECT}.' + ) + + # For now, there is only one AlphaFold 3 JSON version. + if raw_json['version'] != JSON_VERSION: + raise ValueError( + 'AlphaFold 3 input JSON has unsupported version:' + f' {raw_json["version"]}, expected {JSON_VERSION}.' + ) + + if 'sequences' not in raw_json: + raise ValueError( + 'AlphaFold 3 input JSON does not contain any sequences.') + + if 'modelSeeds' not in raw_json or not raw_json['modelSeeds']: + raise ValueError( + 'AlphaFold 3 input JSON must specify at least one rng seed in' + ' `modelSeeds`.' + ) + + sequences = raw_json['sequences'] + + # Make sure sequence IDs are all set. + raw_sequence_ids = [next(iter(s.values())).get('id') + for s in sequences] + if all(raw_sequence_ids): + sequence_ids = [] + for sequence_id in raw_sequence_ids: + if isinstance(sequence_id, list): + sequence_ids.append(sequence_id) + else: + sequence_ids.append([sequence_id]) + else: + raise ValueError( + 'AlphaFold 3 input JSON contains sequences with unset IDs.' + ) + + flat_seq_ids = [] + for seq_ids in sequence_ids: + flat_seq_ids.extend(seq_ids) + + chains = [] + for seq_ids, sequence in zip(sequence_ids, sequences, strict=True): + if len(sequence) != 1: + raise ValueError(f'Chain {seq_ids} has more than 1 sequence.') + for seq_id in seq_ids: + if 'protein' in sequence: + chains.append(ProteinChain.from_dict( + sequence, seq_id=seq_id)) + elif 'rna' in sequence: + chains.append(RnaChain.from_dict(sequence, seq_id=seq_id)) + elif 'dna' in sequence: + chains.append(DnaChain.from_dict(sequence, seq_id=seq_id)) + elif 'ligand' in sequence: + chains.append(Ligand.from_dict(sequence, seq_id=seq_id)) + else: + raise ValueError(f'Unknown sequence type: {sequence}') + + ligands = [chain for chain in chains if isinstance(chain, Ligand)] + bonded_atom_pairs = None + if bonds := raw_json.get('bondedAtomPairs'): + bonded_atom_pairs = [] + for bond in bonds: + if len(bond) != 2: + raise ValueError( + f'Bond {bond} must have 2 atoms, got {len(bond)}.') + bond_beg, bond_end = bond + if ( + len(bond_beg) != 3 + or not isinstance(bond_beg[0], str) + or not isinstance(bond_beg[1], int) + or not isinstance(bond_beg[2], str) + ): + raise ValueError( + f'Atom {bond_beg} in bond {bond} must have 3 components: ' + '(chain_id: str, res_id: int, atom_name: str).' + ) + if ( + len(bond_end) != 3 + or not isinstance(bond_end[0], str) + or not isinstance(bond_end[1], int) + or not isinstance(bond_end[2], str) + ): + raise ValueError( + f'Atom {bond_end} in bond {bond} must have 3 components: ' + '(chain_id: str, res_id: int, atom_name: str).' + ) + if bond_beg[0] not in flat_seq_ids or bond_end[0] not in flat_seq_ids: + raise ValueError(f'Invalid chain ID(s) in bond {bond}') + if bond_beg[1] <= 0 or bond_end[1] <= 0: + raise ValueError(f'Invalid residue ID(s) in bond {bond}') + smiles_ligand_ids = set( + l.id for l in ligands if l.smiles is not None) + if bond_beg[0] in smiles_ligand_ids: + raise ValueError( + f'Bond {bond} involves an unsupported SMILES ligand {bond_beg[0]}' + ) + if bond_end[0] in smiles_ligand_ids: + raise ValueError( + f'Bond {bond} involves an unsupported SMILES ligand {bond_end[0]}' + ) + bonded_atom_pairs.append((tuple(bond_beg), tuple(bond_end))) + + return cls( + name=raw_json['name'], + chains=chains, + rng_seeds=[int(seed) for seed in raw_json['modelSeeds']], + bonded_atom_pairs=bonded_atom_pairs, + user_ccd=raw_json.get('userCCD'), + ) + + @classmethod + def from_mmcif(cls, mmcif_str: str, ccd: chemical_components.Ccd) -> Self: + """Loads the input from an mmCIF string. + + WARNING: Since rng seeds are not stored in mmCIFs, an rng seed is sampled + in the returned `Input`. + + Args: + mmcif_str: The mmCIF string. + ccd: The chemical components dictionary. + + Returns: + The input in an Input format. + """ + + struct = structure.from_mmcif( + mmcif_str, + include_water=False, + fix_mse_residues=True, + fix_unknown_dna=True, + include_bonds=True, + include_other=False, + ) + + # Create default bioassembly, expanding structures implied by stoichiometry. + struct = struct.generate_bioassembly(None) + + sequences = struct.chain_single_letter_sequence( + include_missing_residues=True + ) + + chains = [] + for chain_id, chain_type in zip( + struct.group_by_chain.chain_id, struct.group_by_chain.chain_type + ): + sequence = sequences[chain_id] + + if chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: + residues = list(struct.chain_res_name_sequence()[chain_id]) + if all(ccd.get(res) is not None for res in residues): + chains.append(Ligand(id=chain_id, ccd_ids=residues)) + elif len(residues) == 1: + comp_name = residues[0] + comps = struct.chemical_components_data + if comps is None: + raise ValueError( + 'Missing mmCIF chemical components data - this is required for ' + f'a non-CCD ligand {comp_name} defined using SMILES string.' + ) + chains.append( + Ligand(id=chain_id, + smiles=comps.chem_comp[comp_name].pdbx_smiles) + ) + else: + raise ValueError( + 'Multi-component ligand must be defined using CCD IDs, defining' + ' using SMILES is supported only for single-component ligands. ' + f'Got {residues}' + ) + else: + residues = struct.chain_res_name_sequence()[chain_id] + fixed = struct.chain_res_name_sequence( + fix_non_standard_polymer_res=True + )[chain_id] + modifications = [ + (orig, i + 1) + for i, (orig, fixed) in enumerate(zip(residues, fixed, strict=True)) + if orig != fixed + ] + + if chain_type == mmcif_names.PROTEIN_CHAIN: + chains.append( + ProteinChain(id=chain_id, sequence=sequence, + ptms=modifications) + ) + elif chain_type == mmcif_names.RNA_CHAIN: + chains.append( + RnaChain( + id=chain_id, sequence=sequence, modifications=modifications + ) + ) + elif chain_type == mmcif_names.DNA_CHAIN: + chains.append( + DnaChain( + id=chain_id, sequence=sequence, modifications=modifications + ) + ) + + bonded_atom_pairs = [] + chain_ids = set(c.id for c in chains) + for atom_a, atom_b, _ in struct.iter_bonds(): + if atom_a['chain_id'] in chain_ids and atom_b['chain_id'] in chain_ids: + beg = (atom_a['chain_id'], int( + atom_a['res_id']), atom_a['atom_name']) + end = (atom_b['chain_id'], int( + atom_b['res_id']), atom_b['atom_name']) + bonded_atom_pairs.append((beg, end)) + + return cls( + name=struct.name, + chains=chains, + # mmCIFs don't store rng seeds, so we need to sample one here. + rng_seeds=[_sample_rng_seed()], + bonded_atom_pairs=bonded_atom_pairs or None, + ) + + def to_structure(self, ccd: chemical_components.Ccd) -> structure.Structure: + """Converts Input to a Structure. + + WARNING: This method does not preserve the rng seeds. + + Args: + ccd: The chemical components dictionary. + + Returns: + The input in a structure.Structure format. + """ + ids: list[str] = [] + sequences: list[str] = [] + poly_types: list[str] = [] + formats: list[structure.SequenceFormat] = [] + + for chain in self.chains: + ids.append(chain.id) + match chain: + case ProteinChain(): + sequences.append( + '(' + ')('.join(chain.to_ccd_sequence()) + ')') + poly_types.append(mmcif_names.PROTEIN_CHAIN) + formats.append(structure.SequenceFormat.CCD_CODES) + case RnaChain(): + sequences.append( + '(' + ')('.join(chain.to_ccd_sequence()) + ')') + poly_types.append(mmcif_names.RNA_CHAIN) + formats.append(structure.SequenceFormat.CCD_CODES) + case DnaChain(): + sequences.append( + '(' + ')('.join(chain.to_ccd_sequence()) + ')') + poly_types.append(mmcif_names.DNA_CHAIN) + formats.append(structure.SequenceFormat.CCD_CODES) + case Ligand(): + if chain.ccd_ids is not None: + sequences.append('(' + ')('.join(chain.ccd_ids) + ')') + if len(chain.ccd_ids) == 1: + poly_types.append(mmcif_names.NON_POLYMER_CHAIN) + else: + poly_types.append(mmcif_names.BRANCHED_CHAIN) + formats.append(structure.SequenceFormat.CCD_CODES) + elif chain.smiles is not None: + # Convert to `:` format that is expected + # by structure.from_sequences_and_bonds. + sequences.append(f'LIG_{chain.id}:{chain.smiles}') + poly_types.append(mmcif_names.NON_POLYMER_CHAIN) + formats.append(structure.SequenceFormat.LIGAND_SMILES) + else: + raise ValueError( + 'Ligand must have one of CCD ID or SMILES set.') + + # Remap bond chain IDs from chain IDs to chain indices and convert to + # 0-based residue indexing. + bonded_atom_pairs = [] + chain_indices = {cid: i for i, cid in enumerate(ids)} + if self.bonded_atom_pairs is not None: + for bond_beg, bond_end in self.bonded_atom_pairs: + bonded_atom_pairs.append(( + (chain_indices[bond_beg[0]], bond_beg[1] - 1, bond_beg[2]), + (chain_indices[bond_end[0]], bond_end[1] - 1, bond_end[2]), + )) + + struct = structure.from_sequences_and_bonds( + sequences=sequences, + chain_types=poly_types, + sequence_formats=formats, + bonded_atom_pairs=bonded_atom_pairs, + ccd=ccd, + name=self.sanitised_name(), + bond_type=mmcif_names.COVALENT_BOND, + release_date=None, + ) + # Rename chain IDs to the original ones. + return struct.rename_chain_ids(dict(zip(struct.chains, ids, strict=True))) + + def to_json(self) -> str: + """Converts Input to an AlphaFold JSON.""" + alphafold_json = json.dumps( + { + 'dialect': JSON_DIALECT, + 'version': JSON_VERSION, + 'name': self.name, + 'sequences': [chain.to_dict() for chain in self.chains], + 'modelSeeds': self.rng_seeds, + 'bondedAtomPairs': self.bonded_atom_pairs, + 'userCCD': self.user_ccd, + }, + indent=2, + ) + # Remove newlines from the query/template indices arrays. We match the + # queryIndices/templatesIndices with a non-capturing group. We then match + # the entire region between the square brackets by looking for lines + # containing only whitespace, number, or a comma. + return re.sub( + r'("(?:queryIndices|templateIndices)": \[)([\s\n\d,]+)(\],?)', + lambda mtch: mtch[1] + + re.sub(r'\n\s+', ' ', mtch[2].strip()) + mtch[3], + alphafold_json, + ) + + def fill_missing_fields(self) -> Self: + """Fill missing MSA and template fields with default values.""" + with_missing_fields = [ + c.fill_missing_fields() + if isinstance(c, (ProteinChain, RnaChain)) + else c + for c in self.chains + ] + return dataclasses.replace(self, chains=with_missing_fields) + + def sanitised_name(self) -> str: + """Returns sanitised version of the name that can be used as a filename.""" + lower_spaceless_name = self.name.lower().replace(' ', '_') + allowed_chars = set(string.ascii_lowercase + string.digits + '_-.') + return ''.join(l for l in lower_spaceless_name if l in allowed_chars) + + +def check_unique_sanitised_names(fold_inputs: Sequence[Input]) -> None: + """Checks that the names of the fold inputs are unique.""" + names = [fi.sanitised_name() for fi in fold_inputs] + if len(set(names)) != len(names): + raise ValueError( + f'Fold inputs must have unique sanitised names, got {names}.' + ) + + +def load_fold_inputs_from_path(json_path: pathlib.Path) -> Sequence[Input]: + """Loads multiple fold inputs from a JSON string.""" + with open(json_path, 'r') as f: + json_str = f.read() + + # Parse the JSON string, so we can detect its format. + raw_json = json.loads(json_str) + + fold_inputs = [] + if isinstance(raw_json, list): + # AlphaFold Server JSON. + logging.info( + 'Detected %s is an AlphaFold Server JSON since the top-level is a' + ' list.', + json_path, + ) + + logging.info('Loading %d fold jobs from %s', len(raw_json), json_path) + for fold_job_idx, fold_job in enumerate(raw_json): + try: + fold_inputs.append( + Input.from_alphafoldserver_fold_job(fold_job)) + except ValueError as e: + raise ValueError( + f'Failed to load fold job {fold_job_idx} from {json_path}. The JSON' + f' at {json_path} was detected to be an AlphaFold Server JSON since' + ' the top-level is a list.' + ) from e + else: + logging.info( + 'Detected %s is an AlphaFold 3 JSON since the top-level is not a list.', + json_path, + ) + # AlphaFold 3 JSON. + try: + fold_inputs.append(Input.from_json(json_str)) + except ValueError as e: + raise ValueError( + f'Failed to load fold input from {json_path}. The JSON at' + f' {json_path} was detected to be an AlphaFold 3 JSON since the' + ' top-level is not a list.' + ) from e + + check_unique_sanitised_names(fold_inputs) + + return fold_inputs + + +def load_fold_inputs_from_dir(input_dir: pathlib.Path) -> Sequence[Input]: + """Loads multiple fold inputs from all JSON files in a given input_dir. + + Args: + input_dir: The directory containing the JSON files. + + Returns: + The fold inputs from all JSON files in the input directory. + + Raises: + ValueError: If the fold inputs have non-unique sanitised names. + """ + fold_inputs = [] + for file_path in input_dir.glob('*.json'): + if not file_path.is_file(): + continue + + fold_inputs.extend(load_fold_inputs_from_path(file_path)) + + check_unique_sanitised_names(fold_inputs) + + return fold_inputs diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py new file mode 100644 index 000000000..74b40c148 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py @@ -0,0 +1,77 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Load external resources, such as external tools or data resources.""" + +from collections.abc import Iterator +import os +import pathlib +import typing +from typing import BinaryIO, Final, Literal, TextIO + +from importlib import resources +import alphafold3.common + + +_DATA_ROOT: Final[pathlib.Path] = ( + resources.files(alphafold3.common).joinpath('..').resolve() +) +ROOT = _DATA_ROOT + + +def filename(name: str | os.PathLike[str]) -> str: + """Returns the absolute path to an external resource. + + Note that this calls resources.GetResourceFilename under the hood and hence + causes par file unpacking, which might be unfriendly on diskless machines. + + + Args: + name: the name of the resource corresponding to its path relative to the + root of the repository. + """ + return (_DATA_ROOT / name).as_posix() + + +@typing.overload +def open_resource( + name: str | os.PathLike[str], mode: Literal['r', 'rt'] = 'rt' +) -> TextIO: + ... + + +@typing.overload +def open_resource( + name: str | os.PathLike[str], mode: Literal['rb'] +) -> BinaryIO: + ... + + +def open_resource( + name: str | os.PathLike[str], mode: str = 'rb' +) -> TextIO | BinaryIO: + """Returns an open file object for the named resource. + + Args: + name: the name of the resource corresponding to its path relative to the + root of the repository. + mode: the mode to use when opening the file. + """ + return (_DATA_ROOT / name).open(mode) + + +def get_resource_dir(path: str | os.PathLike[str]) -> os.PathLike[str]: + return _DATA_ROOT / path + + +def walk(path: str) -> Iterator[tuple[str, list[str], list[str]]]: + """Walks the directory tree of resources similar to os.walk.""" + return os.walk((_DATA_ROOT / path).as_posix()) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py new file mode 100644 index 000000000..97a69d2c1 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py @@ -0,0 +1,70 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Module that provides an abstraction for accessing test data.""" + +import os +import pathlib +from typing import Literal, overload + +from absl.testing import absltest + + +class Data: + """Provides an abstraction for accessing test data.""" + + def __init__(self, data_dir: os.PathLike[str] | str): + """Initiailizes data wrapper, providing users with high level data access. + + Args: + data_dir: Directory containing test data. + """ + self._data_dir = pathlib.Path(data_dir) + + def path(self, data_name: str | os.PathLike[str] | None = None) -> str: + """Returns the path to a given test data. + + Args: + data_name: the name of the test data file relative to data_dir. If not + set, this will return the absolute path to the data directory. + """ + data_dir_path = ( + pathlib.Path(absltest.get_default_test_srcdir()) / self._data_dir + ) + + if data_name: + return str(data_dir_path / data_name) + + return str(data_dir_path) + + @overload + def load( + self, data_name: str | os.PathLike[str], mode: Literal['rt'] = 'rt' + ) -> str: + ... + + @overload + def load( + self, data_name: str | os.PathLike[str], mode: Literal['rb'] = 'rb' + ) -> bytes: + ... + + def load( + self, data_name: str | os.PathLike[str], mode: str = 'rt' + ) -> str | bytes: + """Returns the contents of a given test data. + + Args: + data_name: the name of the test data file relative to data_dir. + mode: the mode in which to read the data file. Defaults to text ('rt'). + """ + with open(self.path(data_name), mode=mode) as f: + return f.read() diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/atom_types.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/atom_types.py new file mode 100644 index 000000000..8630278a1 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/atom_types.py @@ -0,0 +1,262 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""List of atom types with reverse look-up.""" + +from collections.abc import Mapping, Sequence, Set +import itertools +import sys +from typing import Final +from alphafold3.constants import residue_names + +# Note: +# `sys.intern` places the values in the Python internal db for fast lookup. + +# 37 common residue atoms. +N = sys.intern('N') +CA = sys.intern('CA') +C = sys.intern('C') +CB = sys.intern('CB') +O = sys.intern('O') +CG = sys.intern('CG') +CG1 = sys.intern('CG1') +CG2 = sys.intern('CG2') +OG = sys.intern('OG') +OG1 = sys.intern('OG1') +SG = sys.intern('SG') +CD = sys.intern('CD') +CD1 = sys.intern('CD1') +CD2 = sys.intern('CD2') +ND1 = sys.intern('ND1') +ND2 = sys.intern('ND2') +OD1 = sys.intern('OD1') +OD2 = sys.intern('OD2') +SD = sys.intern('SD') +CE = sys.intern('CE') +CE1 = sys.intern('CE1') +CE2 = sys.intern('CE2') +CE3 = sys.intern('CE3') +NE = sys.intern('NE') +NE1 = sys.intern('NE1') +NE2 = sys.intern('NE2') +OE1 = sys.intern('OE1') +OE2 = sys.intern('OE2') +CH2 = sys.intern('CH2') +NH1 = sys.intern('NH1') +NH2 = sys.intern('NH2') +OH = sys.intern('OH') +CZ = sys.intern('CZ') +CZ2 = sys.intern('CZ2') +CZ3 = sys.intern('CZ3') +NZ = sys.intern('NZ') +OXT = sys.intern('OXT') + +# 29 common nucleic acid atoms. +C1PRIME = sys.intern("C1'") +C2 = sys.intern('C2') +C2PRIME = sys.intern("C2'") +C3PRIME = sys.intern("C3'") +C4 = sys.intern('C4') +C4PRIME = sys.intern("C4'") +C5 = sys.intern('C5') +C5PRIME = sys.intern("C5'") +C6 = sys.intern('C6') +C7 = sys.intern('C7') +C8 = sys.intern('C8') +N1 = sys.intern('N1') +N2 = sys.intern('N2') +N3 = sys.intern('N3') +N4 = sys.intern('N4') +N6 = sys.intern('N6') +N7 = sys.intern('N7') +N9 = sys.intern('N9') +O2 = sys.intern('O2') +O2PRIME = sys.intern("O2'") +O3PRIME = sys.intern("O3'") +O4 = sys.intern('O4') +O4PRIME = sys.intern("O4'") +O5PRIME = sys.intern("O5'") +O6 = sys.intern('O6') +OP1 = sys.intern('OP1') +OP2 = sys.intern('OP2') +OP3 = sys.intern('OP3') +P = sys.intern('P') + +# A list of atoms (excluding hydrogen) for each AA type. PDB naming convention. +RESIDUE_ATOMS: Mapping[str, tuple[str, ...]] = { + residue_names.ALA: (C, CA, CB, N, O), + residue_names.ARG: (C, CA, CB, CG, CD, CZ, N, NE, O, NH1, NH2), + residue_names.ASN: (C, CA, CB, CG, N, ND2, O, OD1), + residue_names.ASP: (C, CA, CB, CG, N, O, OD1, OD2), + residue_names.CYS: (C, CA, CB, N, O, SG), + residue_names.GLN: (C, CA, CB, CG, CD, N, NE2, O, OE1), + residue_names.GLU: (C, CA, CB, CG, CD, N, O, OE1, OE2), + residue_names.GLY: (C, CA, N, O), + residue_names.HIS: (C, CA, CB, CG, CD2, CE1, N, ND1, NE2, O), + residue_names.ILE: (C, CA, CB, CG1, CG2, CD1, N, O), + residue_names.LEU: (C, CA, CB, CG, CD1, CD2, N, O), + residue_names.LYS: (C, CA, CB, CG, CD, CE, N, NZ, O), + residue_names.MET: (C, CA, CB, CG, CE, N, O, SD), + residue_names.PHE: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O), + residue_names.PRO: (C, CA, CB, CG, CD, N, O), + residue_names.SER: (C, CA, CB, N, O, OG), + residue_names.THR: (C, CA, CB, CG2, N, O, OG1), + residue_names.TRP: + (C, CA, CB, CG, CD1, CD2, CE2, CE3, CZ2, CZ3, CH2, N, NE1, O), + residue_names.TYR: (C, CA, CB, CG, CD1, CD2, CE1, CE2, CZ, N, O, OH), + residue_names.VAL: (C, CA, CB, CG1, CG2, N, O), +} # pyformat: disable + +# Used to identify backbone for alignment and distance calculation for sterics. +PROTEIN_BACKBONE_ATOMS: tuple[str, ...] = (N, CA, C) + +# Naming swaps for ambiguous atom names. Due to symmetries in the amino acids +# the naming of atoms is ambiguous in 4 of the 20 amino acids. (The LDDT paper +# lists 7 amino acids as ambiguous, but the naming ambiguities in LEU, VAL and +# ARG can be resolved by using the 3D constellations of the 'ambiguous' atoms +# and their neighbours) +AMBIGUOUS_ATOM_NAMES: Mapping[str, Mapping[str, str]] = { + residue_names.ASP: {OD1: OD2}, + residue_names.GLU: {OE1: OE2}, + residue_names.PHE: {CD1: CD2, CE1: CE2}, + residue_names.TYR: {CD1: CD2, CE1: CE2}, +} + +# Used when we need to store atom data in a format that requires fixed atom data +# size for every protein residue (e.g. a numpy array). +ATOM37: tuple[str, ...] = ( + N, CA, C, CB, O, CG, CG1, CG2, OG, OG1, SG, CD, CD1, CD2, ND1, ND2, OD1, + OD2, SD, CE, CE1, CE2, CE3, NE, NE1, NE2, OE1, OE2, CH2, NH1, NH2, OH, CZ, + CZ2, CZ3, NZ, OXT) # pyformat: disable +ATOM37_ORDER: Mapping[str, int] = {name: i for i, name in enumerate(ATOM37)} +ATOM37_NUM: Final[int] = len(ATOM37) # := 37. + +# Used when we need to store protein atom data in a format that requires fixed +# atom data size for any residue but takes less space than ATOM37 by having 14 +# fields, which is sufficient for storing atoms of all protein residues (e.g. a +# numpy array). +ATOM14: Mapping[str, tuple[str, ...]] = { + residue_names.ALA: (N, CA, C, O, CB), + residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2), + residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2), + residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2), + residue_names.CYS: (N, CA, C, O, CB, SG), + residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2), + residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2), + residue_names.GLY: (N, CA, C, O), + residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2), + residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1), + residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2), + residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ), + residue_names.MET: (N, CA, C, O, CB, CG, SD, CE), + residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ), + residue_names.PRO: (N, CA, C, O, CB, CG, CD), + residue_names.SER: (N, CA, C, O, CB, OG), + residue_names.THR: (N, CA, C, O, CB, OG1, CG2), + residue_names.TRP: + (N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2), + residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH), + residue_names.VAL: (N, CA, C, O, CB, CG1, CG2), + residue_names.UNK: (), +} # pyformat: disable + +# A compact atom encoding with 14 columns, padded with '' in empty slots. +ATOM14_PADDED: Mapping[str, Sequence[str]] = { + k: [v for _, v in itertools.zip_longest(range(14), values, fillvalue='')] + for k, values in ATOM14.items() +} + +ATOM14_ORDER: Mapping[str, Mapping[str, int]] = { + k: {name: i for i, name in enumerate(v)} for k, v in ATOM14.items() +} +ATOM14_NUM: Final[int] = max(len(v) for v in ATOM14.values()) + +# Used when we need to store protein and nucleic atom library. +DENSE_ATOM: Mapping[str, tuple[str, ...]] = { + # Protein. + residue_names.ALA: (N, CA, C, O, CB), + residue_names.ARG: (N, CA, C, O, CB, CG, CD, NE, CZ, NH1, NH2), + residue_names.ASN: (N, CA, C, O, CB, CG, OD1, ND2), + residue_names.ASP: (N, CA, C, O, CB, CG, OD1, OD2), + residue_names.CYS: (N, CA, C, O, CB, SG), + residue_names.GLN: (N, CA, C, O, CB, CG, CD, OE1, NE2), + residue_names.GLU: (N, CA, C, O, CB, CG, CD, OE1, OE2), + residue_names.GLY: (N, CA, C, O), + residue_names.HIS: (N, CA, C, O, CB, CG, ND1, CD2, CE1, NE2), + residue_names.ILE: (N, CA, C, O, CB, CG1, CG2, CD1), + residue_names.LEU: (N, CA, C, O, CB, CG, CD1, CD2), + residue_names.LYS: (N, CA, C, O, CB, CG, CD, CE, NZ), + residue_names.MET: (N, CA, C, O, CB, CG, SD, CE), + residue_names.PHE: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ), + residue_names.PRO: (N, CA, C, O, CB, CG, CD), + residue_names.SER: (N, CA, C, O, CB, OG), + residue_names.THR: (N, CA, C, O, CB, OG1, CG2), + residue_names.TRP: + (N, CA, C, O, CB, CG, CD1, CD2, NE1, CE2, CE3, CZ2, CZ3, CH2), + residue_names.TYR: (N, CA, C, O, CB, CG, CD1, CD2, CE1, CE2, CZ, OH), + residue_names.VAL: (N, CA, C, O, CB, CG1, CG2), + residue_names.UNK: (), + # RNA. + residue_names.A: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4), + residue_names.C: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6), + residue_names.G: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, O2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4), + residue_names.U: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, O2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C6), + residue_names.UNK_RNA: (), + # DNA. + residue_names.DA: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, C1PRIME, N9, C8, N7, C5, C6, N6, N1, C2, N3, C4), + residue_names.DC: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, C1PRIME, N1, C2, O2, N3, C4, N4, C5, C6), + residue_names.DG: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, C1PRIME, N9, C8, N7, C5, C6, O6, N1, C2, N2, N3, C4), + residue_names.DT: + (OP3, P, OP1, OP2, O5PRIME, C5PRIME, C4PRIME, O4PRIME, C3PRIME, O3PRIME, + C2PRIME, C1PRIME, N1, C2, O2, N3, C4, O4, C5, C7, C6), + # Unknown nucleic. + residue_names.UNK_DNA: (), +} # pyformat: disable + +DENSE_ATOM_ORDER: Mapping[str, Mapping[str, int]] = { + k: {name: i for i, name in enumerate(v)} for k, v in DENSE_ATOM.items() +} +DENSE_ATOM_NUM: Final[int] = max(len(v) for v in DENSE_ATOM.values()) + +# Used when we need to store atom data in a format that requires fixed atom data +# size for every nucleic molecule (e.g. a numpy array). +ATOM29: tuple[str, ...] = ( + "C1'", 'C2', "C2'", "C3'", 'C4', "C4'", 'C5', "C5'", 'C6', 'C7', 'C8', 'N1', + 'N2', 'N3', 'N4', 'N6', 'N7', 'N9', 'OP3', 'O2', "O2'", "O3'", 'O4', "O4'", + "O5'", 'O6', 'OP1', 'OP2', 'P') # pyformat: disable +ATOM29_ORDER: Mapping[str, int] = { + atom_type: i for i, atom_type in enumerate(ATOM29) +} +ATOM29_NUM: Final[int] = len(ATOM29) # := 29 + +# Hydrogens that exist depending on the protonation state of the residue. +# Extracted from third_party/py/openmm/app/data/hydrogens.xml +PROTONATION_HYDROGENS: Mapping[str, Set[str]] = { + 'ASP': {'HD2'}, + 'CYS': {'HG'}, + 'GLU': {'HE2'}, + 'HIS': {'HD1', 'HE2'}, + 'LYS': {'HZ3'}, +} diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_component_sets.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_component_sets.py new file mode 100644 index 000000000..eaf7b5db4 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_component_sets.py @@ -0,0 +1,38 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Sets of chemical components.""" + +import pickle +from typing import Final + +from alphafold3.common import resources + + +_CCD_SETS_CCD_PICKLE_FILE = resources.filename( + resources.ROOT / 'constants/converters/chemical_component_sets.pickle' +) + +_CCD_SET = pickle.load(open(_CCD_SETS_CCD_PICKLE_FILE, 'rb')) + +# Glycan (or 'Saccharide') ligands. +# _chem_comp.type containing 'saccharide' and 'linking' (when lower-case). +GLYCAN_LINKING_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_linking'] + +# _chem_comp.type containing 'saccharide' and not 'linking' (when lower-case). +GLYCAN_OTHER_LIGANDS: Final[frozenset[str]] = _CCD_SET['glycans_other'] + +# Each of these molecules appears in over 1k PDB structures, are used to +# facilitate crystallization conditions, but do not have biological relevance. +COMMON_CRYSTALLIZATION_AIDS: Final[frozenset[str]] = frozenset({ + 'SO4', 'GOL', 'EDO', 'PO4', 'ACT', 'PEG', 'DMS', 'TRS', 'PGE', 'PG4', 'FMT', + 'EPE', 'MPD', 'MES', 'CD', 'IOD', +}) # pyformat: disable diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py new file mode 100644 index 000000000..d1132d995 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py @@ -0,0 +1,192 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Chemical Components found in PDB (CCD) constants.""" + +from collections.abc import ItemsView, Iterator, KeysView, Mapping, Sequence, ValuesView +import dataclasses +import functools +import os +import pickle + +from alphafold3.common import resources +from alphafold3.cpp import cif_dict + + +_CCD_PICKLE_FILE = resources.filename( + resources.ROOT / 'constants/converters/ccd.pickle' +) + + +class Ccd(Mapping[str, Mapping[str, Sequence[str]]]): + """Chemical Components found in PDB (CCD) constants. + + See https://academic.oup.com/bioinformatics/article/31/8/1274/212200 for CCD + CIF format documentation. + + Wraps the dict to prevent accidental mutation. + """ + + __slots__ = ('_dict', '_ccd_pickle_path') + + def __init__( + self, + ccd_pickle_path: os.PathLike[str] | None = None, + user_ccd: str | None = None, + ): + """Initialises the chemical components dictionary. + + Args: + ccd_pickle_path: Path to the CCD pickle file. If None, uses the default + CCD pickle file included in the source code. + user_ccd: A string containing the user-provided CCD. This has to conform + to the same format as the CCD, see https://www.wwpdb.org/data/ccd. If + provided, takes precedence over the CCD for the the same key. This can + be used to override specific entries in the CCD if desired. + """ + self._ccd_pickle_path = ccd_pickle_path or _CCD_PICKLE_FILE + with open(self._ccd_pickle_path, 'rb') as f: + self._dict = pickle.loads(f.read()) + + if user_ccd is not None: + if not user_ccd: + raise ValueError('User CCD cannot be an empty string.') + user_ccd_cifs = { + key: {k: tuple(v) for k, v in value.items()} + for key, value in cif_dict.parse_multi_data_cif(user_ccd).items() + } + self._dict.update(user_ccd_cifs) + + def __getitem__(self, key: str) -> Mapping[str, Sequence[str]]: + return self._dict[key] + + def __contains__(self, key: str) -> bool: + return key in self._dict + + def __iter__(self) -> Iterator[str]: + return self._dict.__iter__() + + def __len__(self) -> int: + return len(self._dict) + + def __hash__(self) -> int: + return id(self) # Ok since this is immutable. + + def get( + self, key: str, default: None | Mapping[str, Sequence[str]] = None + ) -> Mapping[str, Sequence[str]] | None: + return self._dict.get(key, default) + + def items(self) -> ItemsView[str, Mapping[str, Sequence[str]]]: + return self._dict.items() + + def values(self) -> ValuesView[Mapping[str, Sequence[str]]]: + return self._dict.values() + + def keys(self) -> KeysView[str]: + return self._dict.keys() + + +@functools.cache +def cached_ccd(user_ccd: str | None = None) -> Ccd: + return Ccd(user_ccd=user_ccd) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class ComponentInfo: + name: str + type: str + pdbx_synonyms: str + formula: str + formula_weight: str + mon_nstd_parent_comp_id: str + mon_nstd_flag: str + pdbx_smiles: str + + +def mmcif_to_info(mmcif: Mapping[str, Sequence[str]]) -> ComponentInfo: + """Converts CCD mmCIFs to component info. Missing fields are left empty.""" + names = mmcif['_chem_comp.name'] + types = mmcif['_chem_comp.type'] + mon_nstd_parent_comp_ids = mmcif['_chem_comp.mon_nstd_parent_comp_id'] + pdbx_synonyms = mmcif['_chem_comp.pdbx_synonyms'] + formulas = mmcif['_chem_comp.formula'] + formula_weights = mmcif['_chem_comp.formula_weight'] + + def front_or_empty(values: Sequence[str]) -> str: + return values[0] if values else '' + + type_ = front_or_empty(types) + mon_nstd_parent_comp_id = front_or_empty(mon_nstd_parent_comp_ids) + if type_.lower() == 'non-polymer': + # Unset for non-polymers, e.g. water or ions. + mon_nstd_flag = '.' + elif mon_nstd_parent_comp_id == '?': + # A standard component - it doesn't have a standard parent, e.g. MET. + mon_nstd_flag = 'y' + else: + # A non-standard component, e.g. MSE. + mon_nstd_flag = 'n' + + canonical_pdbx_smiles = '' + fallback_pdbx_smiles = '' + descriptor_types = mmcif.get('_pdbx_chem_comp_descriptor.type', []) + descriptors = mmcif.get('_pdbx_chem_comp_descriptor.descriptor', []) + programs = mmcif.get('_pdbx_chem_comp_descriptor.program', []) + + for descriptor_type, descriptor in zip(descriptor_types, descriptors): + if descriptor_type == 'SMILES_CANONICAL': + if (not canonical_pdbx_smiles) or programs == 'OpenEye OEToolkits': + canonical_pdbx_smiles = descriptor + if not fallback_pdbx_smiles and descriptor_type == 'SMILES': + fallback_pdbx_smiles = descriptor + pdbx_smiles = canonical_pdbx_smiles or fallback_pdbx_smiles + + return ComponentInfo( + name=front_or_empty(names), + type=type_, + pdbx_synonyms=front_or_empty(pdbx_synonyms), + formula=front_or_empty(formulas), + formula_weight=front_or_empty(formula_weights), + mon_nstd_parent_comp_id=mon_nstd_parent_comp_id, + mon_nstd_flag=mon_nstd_flag, + pdbx_smiles=pdbx_smiles, + ) + + +@functools.lru_cache(maxsize=128) +def component_name_to_info(ccd: Ccd, res_name: str) -> ComponentInfo | None: + component = ccd.get(res_name) + if component is None: + return None + return mmcif_to_info(component) + + +def type_symbol(ccd: Ccd, res_name: str, atom_name: str) -> str: + """Returns the element type for the given component name and atom name. + + Args: + ccd: The chemical components dictionary. + res_name: The component name, e.g. ARG. + atom_name: The atom name, e.g. CB, OXT, or NH1. + + Returns: + Element type, e.g. C for (ARG, CB), O for (ARG, OXT), N for (ARG, NH1). + """ + res = ccd.get(res_name) + if res is None: + return '?' + try: + return res['_chem_comp_atom.type_symbol'][ + res['_chem_comp_atom.atom_id'].index(atom_name) + ] + except (ValueError, IndexError, KeyError): + return '?' diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/ccd_pickle_gen.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/ccd_pickle_gen.py new file mode 100644 index 000000000..e793f216b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/ccd_pickle_gen.py @@ -0,0 +1,53 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Reads Chemical Components gz file and generates a CCD pickle file.""" + +from collections.abc import Sequence +import gzip +import pickle +import sys + +from alphafold3.cpp import cif_dict +import tqdm + + +def main(argv: Sequence[str]) -> None: + if len(argv) != 3: + raise ValueError( + 'Must specify input_file components.cif and output_file') + + _, input_file, output_file = argv + + print(f'Parsing {input_file}', flush=True) + if input_file.endswith('.gz'): + opener = gzip.open + else: + opener = open + + with opener(input_file, 'rb') as f: + whole_file = f.read() + result = { + key: {k: tuple(v) for k, v in value.items()} + for key, value in tqdm.tqdm( + cif_dict.parse_multi_data_cif(whole_file).items() + ) + } + assert len(result) == whole_file.count(b'data_') + + print(f'Writing {output_file}', flush=True) + with open(output_file, 'wb') as f: + pickle.dump(result, f, protocol=pickle.HIGHEST_PROTOCOL) + print('Done', flush=True) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/chemical_component_sets_gen.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/chemical_component_sets_gen.py new file mode 100644 index 000000000..31d05f7d2 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/chemical_component_sets_gen.py @@ -0,0 +1,81 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Script for updating chemical_component_sets.py.""" + +from collections.abc import Mapping, Sequence +import pathlib +import pickle +import re +import sys + +from alphafold3.common import resources +import tqdm + + +_CCD_PICKLE_FILE = resources.filename( + 'constants/converters/ccd.pickle' +) + + +def find_ions_and_glycans_in_ccd( + ccd: Mapping[str, Mapping[str, Sequence[str]]], +) -> dict[str, frozenset[str]]: + """Finds glycans and ions in all version of CCD.""" + glycans_linking = [] + glycans_other = [] + ions = [] + for name, comp in tqdm.tqdm(ccd.items()): + if name == 'UNX': + continue # Skip "unknown atom or ion". + comp_type = comp['_chem_comp.type'][0].lower() + # Glycans have the type 'saccharide'. + if re.findall(r'\bsaccharide\b', comp_type): + # Separate out linking glycans from others. + if 'linking' in comp_type: + glycans_linking.append(name) + else: + glycans_other.append(name) + + # Ions have the word 'ion' in their name. + comp_name = comp['_chem_comp.name'][0].lower() + if re.findall(r'\bion\b', comp_name): + ions.append(name) + result = dict( + glycans_linking=frozenset(glycans_linking), + glycans_other=frozenset(glycans_other), + ions=frozenset(ions), + ) + + return result + + +def main(argv: Sequence[str]) -> None: + if len(argv) != 2: + raise ValueError( + 'Directory to write to must be specified as a command-line arguments.' + ) + + print(f'Loading {_CCD_PICKLE_FILE}', flush=True) + with open(_CCD_PICKLE_FILE, 'rb') as f: + ccd: Mapping[str, Mapping[str, Sequence[str]]] = pickle.load(f) + output_path = pathlib.Path(argv[1]) + output_path.parent.mkdir(exist_ok=True) + print('Finding ions and glycans', flush=True) + result = find_ions_and_glycans_in_ccd(ccd) + print(f'writing to {output_path}', flush=True) + with output_path.open('wb') as f: + pickle.dump(result, f) + print('Done', flush=True) + + +if __name__ == '__main__': + main(sys.argv) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/mmcif_names.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/mmcif_names.py new file mode 100644 index 000000000..15eabf2f9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/mmcif_names.py @@ -0,0 +1,218 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Names of things in mmCIF format. + +See https://www.iucr.org/__data/iucr/cifdic_html/2/cif_mm.dic/index.html +""" + +from collections.abc import Mapping, Sequence, Set +from typing import Final + +from alphafold3.constants import atom_types +from alphafold3.constants import residue_names + + +# The following are all possible values for the "_entity.type". +# https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity.type.html +BRANCHED_CHAIN: Final[str] = 'branched' +MACROLIDE_CHAIN: Final[str] = 'macrolide' +NON_POLYMER_CHAIN: Final[str] = 'non-polymer' +POLYMER_CHAIN: Final[str] = 'polymer' +WATER: Final[str] = 'water' + +CYCLIC_PSEUDO_PEPTIDE_CHAIN: Final[str] = 'cyclic-pseudo-peptide' +DNA_CHAIN: Final[str] = 'polydeoxyribonucleotide' +DNA_RNA_HYBRID_CHAIN: Final[str] = ( + 'polydeoxyribonucleotide/polyribonucleotide hybrid' +) +OTHER_CHAIN: Final[str] = 'other' +PEPTIDE_NUCLEIC_ACID_CHAIN: Final[str] = 'peptide nucleic acid' +POLYPEPTIDE_D_CHAIN: Final[str] = 'polypeptide(D)' +PROTEIN_CHAIN: Final[str] = 'polypeptide(L)' +RNA_CHAIN: Final[str] = 'polyribonucleotide' + +# Most common _entity_poly.types. +STANDARD_POLYMER_CHAIN_TYPES: Final[Set[str]] = { + PROTEIN_CHAIN, + DNA_CHAIN, + RNA_CHAIN, +} + +# Possible values for _entity.type other than polymer and water. +LIGAND_CHAIN_TYPES: Final[Set[str]] = { + BRANCHED_CHAIN, + MACROLIDE_CHAIN, + NON_POLYMER_CHAIN, +} + +# Possible values for _entity.type other than polymer. +NON_POLYMER_CHAIN_TYPES: Final[Set[str]] = { + *LIGAND_CHAIN_TYPES, + WATER, +} + +# Peptide possible values for _entity_poly.type. +PEPTIDE_CHAIN_TYPES: Final[Set[str]] = { + CYCLIC_PSEUDO_PEPTIDE_CHAIN, + POLYPEPTIDE_D_CHAIN, + PROTEIN_CHAIN, + PEPTIDE_NUCLEIC_ACID_CHAIN, +} + + +# Nucleic-acid possible values for _entity_poly.type. +NUCLEIC_ACID_CHAIN_TYPES: Final[Set[str]] = { + RNA_CHAIN, + DNA_CHAIN, + DNA_RNA_HYBRID_CHAIN, +} + +# All possible values for _entity_poly.type. +POLYMER_CHAIN_TYPES: Final[Set[str]] = { + *NUCLEIC_ACID_CHAIN_TYPES, + *PEPTIDE_CHAIN_TYPES, + OTHER_CHAIN, +} + + +TERMINAL_OXYGENS: Final[Mapping[str, str]] = { + PROTEIN_CHAIN: 'OXT', + DNA_CHAIN: 'OP3', + RNA_CHAIN: 'OP3', +} + + +# For each chain type, which atom should be used to represent each residue. +RESIDUE_REPRESENTATIVE_ATOMS: Final[Mapping[str, str]] = { + PROTEIN_CHAIN: atom_types.CA, + DNA_CHAIN: atom_types.C1PRIME, + RNA_CHAIN: atom_types.C1PRIME, +} + +# Methods involving crystallization. See the documentation at +# mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_exptl.method.html +# for the full list of experimental methods. +CRYSTALLIZATION_METHODS: Final[Set[str]] = { + 'X-RAY DIFFRACTION', + 'NEUTRON DIFFRACTION', + 'ELECTRON CRYSTALLOGRAPHY', + 'POWDER CRYSTALLOGRAPHY', + 'FIBER DIFFRACTION', +} + +# Possible bond types. +COVALENT_BOND: Final[str] = 'covale' +HYDROGEN_BOND: Final[str] = 'hydrog' +METAL_COORDINATION: Final[str] = 'metalc' +DISULFIDE_BRIDGE: Final[str] = 'disulf' + + +def is_standard_polymer_type(chain_type: str) -> bool: + """Returns if chain type is a protein, DNA or RNA chain type. + + Args: + chain_type: The type of the chain. + + Returns: + A bool for if the chain_type matches protein, DNA, or RNA. + """ + return chain_type in STANDARD_POLYMER_CHAIN_TYPES + + +def guess_polymer_type(chain_residues: Sequence[str]) -> str: + """Guess the polymer type (protein/rna/dna/other) based on the residues. + + The polymer type is guessed by first checking for any of the standard + protein residues. If one is present then the chain is considered to be a + polypeptide. Otherwise we decide by counting residue types and deciding by + majority voting (e.g. mostly DNA residues -> DNA). If there is a tie between + the counts, the ordering is rna > dna > other. + + Note that we count MSE and UNK as protein residues. + + Args: + chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters + for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF. + + Returns: + The most probable chain type as set in the _entity_poly mmCIF table: + protein - polypeptide(L), rna - polyribonucleotide, + dna - polydeoxyribonucleotide or other. + """ + residue_types = { + **{r: RNA_CHAIN for r in residue_names.RNA_TYPES}, + **{r: DNA_CHAIN for r in residue_names.DNA_TYPES}, + **{r: PROTEIN_CHAIN for r in residue_names.PROTEIN_TYPES_WITH_UNKNOWN}, + residue_names.MSE: PROTEIN_CHAIN, + } + + counts = {PROTEIN_CHAIN: 0, RNA_CHAIN: 0, DNA_CHAIN: 0, OTHER_CHAIN: 0} + for residue in chain_residues: + residue_type = residue_types.get(residue, OTHER_CHAIN) + # If we ever see a protein residue we'll consider this a polypeptide(L). + if residue_type == PROTEIN_CHAIN: + return residue_type + counts[residue_type] += 1 + + # Make sure protein > rna > dna > other if there is a tie. + tie_braker = {PROTEIN_CHAIN: 3, RNA_CHAIN: 2, DNA_CHAIN: 1, OTHER_CHAIN: 0} + + def order_fn(item): + name, count = item + return count, tie_braker[name] + + most_probable_type = max(counts.items(), key=order_fn)[0] + return most_probable_type + + +def fix_non_standard_polymer_res(*, res_name: str, chain_type: str) -> str: + """Returns the res_name of the closest standard protein/RNA/DNA residue. + + Optimized for the case where a single residue needs to be converted. + + If res_name is already a standard type, it is returned unaltered. + If a match cannot be found, returns 'UNK' for protein chains and 'N' for + RNA/DNA chains. + + Args: + res_name: A residue_name (monomer code from the CCD). + chain_type: The type of the chain, must be PROTEIN_CHAIN, RNA_CHAIN or + DNA_CHAIN. + + Returns: + An element from PROTEIN_TYPES_WITH_UNKNOWN | RNA_TYPES | DNA_TYPES | {'N'}. + + Raises: + ValueError: If chain_type not in PEPTIDE_CHAIN_TYPES or + {OTHER_CHAIN, RNA_CHAIN, DNA_CHAIN, DNA_RNA_HYBRID_CHAIN}. + """ + # Map to one letter code, then back to common res_names. + one_letter_code = residue_names.letters_three_to_one(res_name, default='X') + + if chain_type in PEPTIDE_CHAIN_TYPES or chain_type == OTHER_CHAIN: + return residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(one_letter_code, 'UNK') + elif chain_type == RNA_CHAIN: + # RNA's CCD monomer code is single-letter. + return ( + one_letter_code if one_letter_code in residue_names.RNA_TYPES else 'N' + ) + elif chain_type == DNA_CHAIN: + return residue_names.DNA_COMMON_ONE_TO_TWO.get(one_letter_code, 'N') + elif chain_type == DNA_RNA_HYBRID_CHAIN: + return ( + res_name + if res_name in residue_names.NUCLEIC_TYPES_WITH_UNKNOWN + else 'N' + ) + else: + raise ValueError( + f'Expected a protein/DNA/RNA chain but got {chain_type}') diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/periodic_table.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/periodic_table.py new file mode 100644 index 000000000..7385245ff --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/periodic_table.py @@ -0,0 +1,399 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Periodic table of elements.""" + +from collections.abc import Mapping, Sequence +import dataclasses +from typing import Final + +import numpy as np + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Element: + name: str + number: int + symbol: str + weight: float + + +# Weights taken from rdkit/Code/GraphMol/atomic_data.cpp for compatibility. +# pylint: disable=invalid-name + +# X is an unknown element that can be present in the CCD, +# https://www.rcsb.org/ligand/UNX. +X: Final[Element] = Element(name='Unknown', number=0, symbol='X', weight=0.0) +H: Final[Element] = Element( + name='Hydrogen', number=1, symbol='H', weight=1.008) +He: Final[Element] = Element( + name='Helium', number=2, symbol='He', weight=4.003) +Li: Final[Element] = Element( + name='Lithium', number=3, symbol='Li', weight=6.941 +) +Be: Final[Element] = Element( + name='Beryllium', number=4, symbol='Be', weight=9.012 +) +B: Final[Element] = Element(name='Boron', number=5, symbol='B', weight=10.812) +C: Final[Element] = Element(name='Carbon', number=6, symbol='C', weight=12.011) +N: Final[Element] = Element( + name='Nitrogen', number=7, symbol='N', weight=14.007 +) +O: Final[Element] = Element(name='Oxygen', number=8, symbol='O', weight=15.999) +F: Final[Element] = Element( + name='Fluorine', number=9, symbol='F', weight=18.998 +) +Ne: Final[Element] = Element(name='Neon', number=10, symbol='Ne', weight=20.18) +Na: Final[Element] = Element( + name='Sodium', number=11, symbol='Na', weight=22.99 +) +Mg: Final[Element] = Element( + name='Magnesium', number=12, symbol='Mg', weight=24.305 +) +Al: Final[Element] = Element( + name='Aluminium', number=13, symbol='Al', weight=26.982 +) +Si: Final[Element] = Element( + name='Silicon', number=14, symbol='Si', weight=28.086 +) +P: Final[Element] = Element( + name='Phosphorus', number=15, symbol='P', weight=30.974 +) +S: Final[Element] = Element( + name='Sulfur', number=16, symbol='S', weight=32.067) +Cl: Final[Element] = Element( + name='Chlorine', number=17, symbol='Cl', weight=35.453 +) +Ar: Final[Element] = Element( + name='Argon', number=18, symbol='Ar', weight=39.948 +) +K: Final[Element] = Element( + name='Potassium', number=19, symbol='K', weight=39.098 +) +Ca: Final[Element] = Element( + name='Calcium', number=20, symbol='Ca', weight=40.078 +) +Sc: Final[Element] = Element( + name='Scandium', number=21, symbol='Sc', weight=44.956 +) +Ti: Final[Element] = Element( + name='Titanium', number=22, symbol='Ti', weight=47.867 +) +V: Final[Element] = Element( + name='Vanadium', number=23, symbol='V', weight=50.942 +) +Cr: Final[Element] = Element( + name='Chromium', number=24, symbol='Cr', weight=51.996 +) +Mn: Final[Element] = Element( + name='Manganese', number=25, symbol='Mn', weight=54.938 +) +Fe: Final[Element] = Element( + name='Iron', number=26, symbol='Fe', weight=55.845) +Co: Final[Element] = Element( + name='Cobalt', number=27, symbol='Co', weight=58.933 +) +Ni: Final[Element] = Element( + name='Nickel', number=28, symbol='Ni', weight=58.693 +) +Cu: Final[Element] = Element( + name='Copper', number=29, symbol='Cu', weight=63.546 +) +Zn: Final[Element] = Element(name='Zinc', number=30, symbol='Zn', weight=65.39) +Ga: Final[Element] = Element( + name='Gallium', number=31, symbol='Ga', weight=69.723 +) +Ge: Final[Element] = Element( + name='Germanium', number=32, symbol='Ge', weight=72.61 +) +As: Final[Element] = Element( + name='Arsenic', number=33, symbol='As', weight=74.922 +) +Se: Final[Element] = Element( + name='Selenium', number=34, symbol='Se', weight=78.96 +) +Br: Final[Element] = Element( + name='Bromine', number=35, symbol='Br', weight=79.904 +) +Kr: Final[Element] = Element( + name='Krypton', number=36, symbol='Kr', weight=83.8 +) +Rb: Final[Element] = Element( + name='Rubidium', number=37, symbol='Rb', weight=85.468 +) +Sr: Final[Element] = Element( + name='Strontium', number=38, symbol='Sr', weight=87.62 +) +Y: Final[Element] = Element( + name='Yttrium', number=39, symbol='Y', weight=88.906 +) +Zr: Final[Element] = Element( + name='Zirconium', number=40, symbol='Zr', weight=91.224 +) +Nb: Final[Element] = Element( + name='Niobium', number=41, symbol='Nb', weight=92.906 +) +Mo: Final[Element] = Element( + name='Molybdenum', number=42, symbol='Mo', weight=95.94 +) +Tc: Final[Element] = Element( + name='Technetium', number=43, symbol='Tc', weight=98 +) +Ru: Final[Element] = Element( + name='Ruthenium', number=44, symbol='Ru', weight=101.07 +) +Rh: Final[Element] = Element( + name='Rhodium', number=45, symbol='Rh', weight=102.906 +) +Pd: Final[Element] = Element( + name='Palladium', number=46, symbol='Pd', weight=106.42 +) +Ag: Final[Element] = Element( + name='Silver', number=47, symbol='Ag', weight=107.868 +) +Cd: Final[Element] = Element( + name='Cadmium', number=48, symbol='Cd', weight=112.412 +) +In: Final[Element] = Element( + name='Indium', number=49, symbol='In', weight=114.818 +) +Sn: Final[Element] = Element( + name='Tin', number=50, symbol='Sn', weight=118.711) +Sb: Final[Element] = Element( + name='Antimony', number=51, symbol='Sb', weight=121.76 +) +Te: Final[Element] = Element( + name='Tellurium', number=52, symbol='Te', weight=127.6 +) +I: Final[Element] = Element( + name='Iodine', number=53, symbol='I', weight=126.904 +) +Xe: Final[Element] = Element( + name='Xenon', number=54, symbol='Xe', weight=131.29 +) +Cs: Final[Element] = Element( + name='Caesium', number=55, symbol='Cs', weight=132.905 +) +Ba: Final[Element] = Element( + name='Barium', number=56, symbol='Ba', weight=137.328 +) +La: Final[Element] = Element( + name='Lanthanum', number=57, symbol='La', weight=138.906 +) +Ce: Final[Element] = Element( + name='Cerium', number=58, symbol='Ce', weight=140.116 +) +Pr: Final[Element] = Element( + name='Praseodymium', number=59, symbol='Pr', weight=140.908 +) +Nd: Final[Element] = Element( + name='Neodymium', number=60, symbol='Nd', weight=144.24 +) +Pm: Final[Element] = Element( + name='Promethium', number=61, symbol='Pm', weight=145 +) +Sm: Final[Element] = Element( + name='Samarium', number=62, symbol='Sm', weight=150.36 +) +Eu: Final[Element] = Element( + name='Europium', number=63, symbol='Eu', weight=151.964 +) +Gd: Final[Element] = Element( + name='Gadolinium', number=64, symbol='Gd', weight=157.25 +) +Tb: Final[Element] = Element( + name='Terbium', number=65, symbol='Tb', weight=158.925 +) +Dy: Final[Element] = Element( + name='Dysprosium', number=66, symbol='Dy', weight=162.5 +) +Ho: Final[Element] = Element( + name='Holmium', number=67, symbol='Ho', weight=164.93 +) +Er: Final[Element] = Element( + name='Erbium', number=68, symbol='Er', weight=167.26 +) +Tm: Final[Element] = Element( + name='Thulium', number=69, symbol='Tm', weight=168.934 +) +Yb: Final[Element] = Element( + name='Ytterbium', number=70, symbol='Yb', weight=173.04 +) +Lu: Final[Element] = Element( + name='Lutetium', number=71, symbol='Lu', weight=174.967 +) +Hf: Final[Element] = Element( + name='Hafnium', number=72, symbol='Hf', weight=178.49 +) +Ta: Final[Element] = Element( + name='Tantalum', number=73, symbol='Ta', weight=180.948 +) +W: Final[Element] = Element( + name='Tungsten', number=74, symbol='W', weight=183.84 +) +Re: Final[Element] = Element( + name='Rhenium', number=75, symbol='Re', weight=186.207 +) +Os: Final[Element] = Element( + name='Osmium', number=76, symbol='Os', weight=190.23 +) +Ir: Final[Element] = Element( + name='Iridium', number=77, symbol='Ir', weight=192.217 +) +Pt: Final[Element] = Element( + name='Platinum', number=78, symbol='Pt', weight=195.078 +) +Au: Final[Element] = Element( + name='Gold', number=79, symbol='Au', weight=196.967 +) +Hg: Final[Element] = Element( + name='Mercury', number=80, symbol='Hg', weight=200.59 +) +Tl: Final[Element] = Element( + name='Thallium', number=81, symbol='Tl', weight=204.383 +) +Pb: Final[Element] = Element(name='Lead', number=82, symbol='Pb', weight=207.2) +Bi: Final[Element] = Element( + name='Bismuth', number=83, symbol='Bi', weight=208.98 +) +Po: Final[Element] = Element( + name='Polonium', number=84, symbol='Po', weight=209 +) +At: Final[Element] = Element( + name='Astatine', number=85, symbol='At', weight=210 +) +Rn: Final[Element] = Element(name='Radon', number=86, symbol='Rn', weight=222) +Fr: Final[Element] = Element( + name='Francium', number=87, symbol='Fr', weight=223 +) +Ra: Final[Element] = Element(name='Radium', number=88, symbol='Ra', weight=226) +Ac: Final[Element] = Element( + name='Actinium', number=89, symbol='Ac', weight=227 +) +Th: Final[Element] = Element( + name='Thorium', number=90, symbol='Th', weight=232.038 +) +Pa: Final[Element] = Element( + name='Protactinium', number=91, symbol='Pa', weight=231.036 +) +U: Final[Element] = Element( + name='Uranium', number=92, symbol='U', weight=238.029 +) +Np: Final[Element] = Element( + name='Neptunium', number=93, symbol='Np', weight=237 +) +Pu: Final[Element] = Element( + name='Plutonium', number=94, symbol='Pu', weight=244 +) +Am: Final[Element] = Element( + name='Americium', number=95, symbol='Am', weight=243 +) +Cm: Final[Element] = Element(name='Curium', number=96, symbol='Cm', weight=247) +Bk: Final[Element] = Element( + name='Berkelium', number=97, symbol='Bk', weight=247 +) +Cf: Final[Element] = Element( + name='Californium', number=98, symbol='Cf', weight=251 +) +Es: Final[Element] = Element( + name='Einsteinium', number=99, symbol='Es', weight=252 +) +Fm: Final[Element] = Element( + name='Fermium', number=100, symbol='Fm', weight=257 +) +Md: Final[Element] = Element( + name='Mendelevium', number=101, symbol='Md', weight=258 +) +No: Final[Element] = Element( + name='Nobelium', number=102, symbol='No', weight=259 +) +Lr: Final[Element] = Element( + name='Lawrencium', number=103, symbol='Lr', weight=262 +) +Rf: Final[Element] = Element( + name='Rutherfordium', number=104, symbol='Rf', weight=267 +) +Db: Final[Element] = Element( + name='Dubnium', number=105, symbol='Db', weight=268 +) +Sg: Final[Element] = Element( + name='Seaborgium', number=106, symbol='Sg', weight=269 +) +Bh: Final[Element] = Element( + name='Bohrium', number=107, symbol='Bh', weight=270 +) +Hs: Final[Element] = Element( + name='Hassium', number=108, symbol='Hs', weight=269 +) +Mt: Final[Element] = Element( + name='Meitnerium', number=109, symbol='Mt', weight=278 +) +Ds: Final[Element] = Element( + name='Darmstadtium', number=110, symbol='Ds', weight=281 +) +Rg: Final[Element] = Element( + name='Roentgenium', number=111, symbol='Rg', weight=281 +) +Cn: Final[Element] = Element( + name='Copernicium', number=112, symbol='Cn', weight=285 +) +Nh: Final[Element] = Element( + name='Nihonium', number=113, symbol='Nh', weight=284 +) +Fl: Final[Element] = Element( + name='Flerovium', number=114, symbol='Fl', weight=289 +) +Mc: Final[Element] = Element( + name='Moscovium', number=115, symbol='Mc', weight=288 +) +Lv: Final[Element] = Element( + name='Livermorium', number=116, symbol='Lv', weight=293 +) +Ts: Final[Element] = Element( + name='Tennessine', number=117, symbol='Ts', weight=292 +) +Og: Final[Element] = Element( + name='Oganesson', number=118, symbol='Og', weight=294 +) +# pylint: enable=invalid-name + +# fmt: off +# Lanthanides +_L: Final[Sequence[Element]] = ( + La, Ce, Pr, Nd, Pm, Sm, Eu, Gd, Tb, Dy, Ho, Er, Tm, Yb, Lu) +# Actinides +_A: Final[Sequence[Element]] = ( + Ac, Th, Pa, U, Np, Pu, Am, Cm, Bk, Cf, Es, Fm, Md, No, Lr) + +# pylint: disable=bad-whitespace +PERIODIC_TABLE: Final[Sequence[Element]] = ( + X, # Unknown + H, He, + Li, Be, B, C, N, O, F, Ne, + Na, Mg, Al, Si, P, S, Cl, Ar, + K, Ca, Sc, Ti, V, Cr, Mn, Fe, Co, Ni, Cu, Zn, Ga, Ge, As, Se, Br, Kr, + Rb, Sr, Y, Zr, Nb, Mo, Tc, Ru, Rh, Pd, Ag, Cd, In, Sn, Sb, Te, I, Xe, + Cs, Ba, *_L, Hf, Ta, W, Re, Os, Ir, Pt, Au, Hg, Tl, Pb, Bi, Po, At, Rn, + Fr, Ra, *_A, Rf, Db, Sg, Bh, Hs, Mt, Ds, Rg, Cn, Nh, Fl, Mc, Lv, Ts, Og +) +# pylint: enable=bad-whitespace +# fmt: on +ATOMIC_SYMBOL: Mapping[int, str] = {e.number: e.symbol for e in PERIODIC_TABLE} +ATOMIC_NUMBER = {e.symbol: e.number for e in PERIODIC_TABLE} +# Add Deuterium as previous table contained it. +ATOMIC_NUMBER['D'] = 1 + +ATOMIC_NUMBER: Mapping[str, int] = ATOMIC_NUMBER +ATOMIC_WEIGHT: np.ndarray = np.zeros(len(PERIODIC_TABLE), dtype=np.float64) + +for e in PERIODIC_TABLE: + ATOMIC_WEIGHT[e.number] = e.weight +ATOMIC_WEIGHT.setflags(write=False) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/residue_names.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/residue_names.py new file mode 100644 index 000000000..40d42587c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/residue_names.py @@ -0,0 +1,421 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Constants associated with residue names.""" + +from collections.abc import Mapping +import functools +import sys + +# pyformat: disable +# common_typos_disable +CCD_NAME_TO_ONE_LETTER: Mapping[str, str] = { + '00C': 'C', '01W': 'X', '02K': 'A', '03Y': 'C', '07O': 'C', '08P': 'C', + '0A0': 'D', '0A1': 'Y', '0A2': 'K', '0A8': 'C', '0AA': 'V', '0AB': 'V', + '0AC': 'G', '0AD': 'G', '0AF': 'W', '0AG': 'L', '0AH': 'S', '0AK': 'D', + '0AM': 'A', '0AP': 'C', '0AU': 'U', '0AV': 'A', '0AZ': 'P', '0BN': 'F', + '0C': 'C', '0CS': 'A', '0DC': 'C', '0DG': 'G', '0DT': 'T', '0FL': 'A', + '0G': 'G', '0NC': 'A', '0SP': 'A', '0U': 'U', '10C': 'C', '125': 'U', + '126': 'U', '127': 'U', '128': 'N', '12A': 'A', '143': 'C', '193': 'X', + '1AP': 'A', '1MA': 'A', '1MG': 'G', '1PA': 'F', '1PI': 'A', '1PR': 'N', + '1SC': 'C', '1TQ': 'W', '1TY': 'Y', '1X6': 'S', '200': 'F', '23F': 'F', + '23S': 'X', '26B': 'T', '2AD': 'X', '2AG': 'A', '2AO': 'X', '2AR': 'A', + '2AS': 'X', '2AT': 'T', '2AU': 'U', '2BD': 'I', '2BT': 'T', '2BU': 'A', + '2CO': 'C', '2DA': 'A', '2DF': 'N', '2DM': 'N', '2DO': 'X', '2DT': 'T', + '2EG': 'G', '2FE': 'N', '2FI': 'N', '2FM': 'M', '2GT': 'T', '2HF': 'H', + '2LU': 'L', '2MA': 'A', '2MG': 'G', '2ML': 'L', '2MR': 'R', '2MT': 'P', + '2MU': 'U', '2NT': 'T', '2OM': 'U', '2OT': 'T', '2PI': 'X', '2PR': 'G', + '2SA': 'N', '2SI': 'X', '2ST': 'T', '2TL': 'T', '2TY': 'Y', '2VA': 'V', + '2XA': 'C', '32S': 'X', '32T': 'X', '3AH': 'H', '3AR': 'X', '3CF': 'F', + '3DA': 'A', '3DR': 'N', '3GA': 'A', '3MD': 'D', '3ME': 'U', '3NF': 'Y', + '3QN': 'K', '3TY': 'X', '3XH': 'G', '4AC': 'N', '4BF': 'Y', '4CF': 'F', + '4CY': 'M', '4DP': 'W', '4FB': 'P', '4FW': 'W', '4HT': 'W', '4IN': 'W', + '4MF': 'N', '4MM': 'X', '4OC': 'C', '4PC': 'C', '4PD': 'C', '4PE': 'C', + '4PH': 'F', '4SC': 'C', '4SU': 'U', '4TA': 'N', '4U7': 'A', '56A': 'H', + '5AA': 'A', '5AB': 'A', '5AT': 'T', '5BU': 'U', '5CG': 'G', '5CM': 'C', + '5CS': 'C', '5FA': 'A', '5FC': 'C', '5FU': 'U', '5HP': 'E', '5HT': 'T', + '5HU': 'U', '5IC': 'C', '5IT': 'T', '5IU': 'U', '5MC': 'C', '5MD': 'N', + '5MU': 'U', '5NC': 'C', '5PC': 'C', '5PY': 'T', '5SE': 'U', '64T': 'T', + '6CL': 'K', '6CT': 'T', '6CW': 'W', '6HA': 'A', '6HC': 'C', '6HG': 'G', + '6HN': 'K', '6HT': 'T', '6IA': 'A', '6MA': 'A', '6MC': 'A', '6MI': 'N', + '6MT': 'A', '6MZ': 'N', '6OG': 'G', '70U': 'U', '7DA': 'A', '7GU': 'G', + '7JA': 'I', '7MG': 'G', '8AN': 'A', '8FG': 'G', '8MG': 'G', '8OG': 'G', + '9NE': 'E', '9NF': 'F', '9NR': 'R', '9NV': 'V', 'A': 'A', 'A1P': 'N', + 'A23': 'A', 'A2L': 'A', 'A2M': 'A', 'A34': 'A', 'A35': 'A', 'A38': 'A', + 'A39': 'A', 'A3A': 'A', 'A3P': 'A', 'A40': 'A', 'A43': 'A', 'A44': 'A', + 'A47': 'A', 'A5L': 'A', 'A5M': 'C', 'A5N': 'N', 'A5O': 'A', 'A66': 'X', + 'AA3': 'A', 'AA4': 'A', 'AAR': 'R', 'AB7': 'X', 'ABA': 'A', 'ABR': 'A', + 'ABS': 'A', 'ABT': 'N', 'ACB': 'D', 'ACL': 'R', 'AD2': 'A', 'ADD': 'X', + 'ADX': 'N', 'AEA': 'X', 'AEI': 'D', 'AET': 'A', 'AFA': 'N', 'AFF': 'N', + 'AFG': 'G', 'AGM': 'R', 'AGT': 'C', 'AHB': 'N', 'AHH': 'X', 'AHO': 'A', + 'AHP': 'A', 'AHS': 'X', 'AHT': 'X', 'AIB': 'A', 'AKL': 'D', 'AKZ': 'D', + 'ALA': 'A', 'ALC': 'A', 'ALM': 'A', 'ALN': 'A', 'ALO': 'T', 'ALQ': 'X', + 'ALS': 'A', 'ALT': 'A', 'ALV': 'A', 'ALY': 'K', 'AN8': 'A', 'AP7': 'A', + 'APE': 'X', 'APH': 'A', 'API': 'K', 'APK': 'K', 'APM': 'X', 'APP': 'X', + 'AR2': 'R', 'AR4': 'E', 'AR7': 'R', 'ARG': 'R', 'ARM': 'R', 'ARO': 'R', + 'ARV': 'X', 'AS': 'A', 'AS2': 'D', 'AS9': 'X', 'ASA': 'D', 'ASB': 'D', + 'ASI': 'D', 'ASK': 'D', 'ASL': 'D', 'ASM': 'X', 'ASN': 'N', 'ASP': 'D', + 'ASQ': 'D', 'ASU': 'N', 'ASX': 'B', 'ATD': 'T', 'ATL': 'T', 'ATM': 'T', + 'AVC': 'A', 'AVN': 'X', 'AYA': 'A', 'AZK': 'K', 'AZS': 'S', 'AZY': 'Y', + 'B1F': 'F', 'B1P': 'N', 'B2A': 'A', 'B2F': 'F', 'B2I': 'I', 'B2V': 'V', + 'B3A': 'A', 'B3D': 'D', 'B3E': 'E', 'B3K': 'K', 'B3L': 'X', 'B3M': 'X', + 'B3Q': 'X', 'B3S': 'S', 'B3T': 'X', 'B3U': 'H', 'B3X': 'N', 'B3Y': 'Y', + 'BB6': 'C', 'BB7': 'C', 'BB8': 'F', 'BB9': 'C', 'BBC': 'C', 'BCS': 'C', + 'BE2': 'X', 'BFD': 'D', 'BG1': 'S', 'BGM': 'G', 'BH2': 'D', 'BHD': 'D', + 'BIF': 'F', 'BIL': 'X', 'BIU': 'I', 'BJH': 'X', 'BLE': 'L', 'BLY': 'K', + 'BMP': 'N', 'BMT': 'T', 'BNN': 'F', 'BNO': 'X', 'BOE': 'T', 'BOR': 'R', + 'BPE': 'C', 'BRU': 'U', 'BSE': 'S', 'BT5': 'N', 'BTA': 'L', 'BTC': 'C', + 'BTR': 'W', 'BUC': 'C', 'BUG': 'V', 'BVP': 'U', 'BZG': 'N', 'C': 'C', + 'C1X': 'K', 'C25': 'C', 'C2L': 'C', 'C2S': 'C', 'C31': 'C', 'C32': 'C', + 'C34': 'C', 'C36': 'C', 'C37': 'C', 'C38': 'C', 'C3Y': 'C', 'C42': 'C', + 'C43': 'C', 'C45': 'C', 'C46': 'C', 'C49': 'C', 'C4R': 'C', 'C4S': 'C', + 'C5C': 'C', 'C66': 'X', 'C6C': 'C', 'CAF': 'C', 'CAL': 'X', 'CAR': 'C', + 'CAS': 'C', 'CAV': 'X', 'CAY': 'C', 'CB2': 'C', 'CBR': 'C', 'CBV': 'C', + 'CCC': 'C', 'CCL': 'K', 'CCS': 'C', 'CDE': 'X', 'CDV': 'X', 'CDW': 'C', + 'CEA': 'C', 'CFL': 'C', 'CG1': 'G', 'CGA': 'E', 'CGU': 'E', 'CH': 'C', + 'CHF': 'X', 'CHG': 'X', 'CHP': 'G', 'CHS': 'X', 'CIR': 'R', 'CLE': 'L', + 'CLG': 'K', 'CLH': 'K', 'CM0': 'N', 'CME': 'C', 'CMH': 'C', 'CML': 'C', + 'CMR': 'C', 'CMT': 'C', 'CNU': 'U', 'CP1': 'C', 'CPC': 'X', 'CPI': 'X', + 'CR5': 'G', 'CS0': 'C', 'CS1': 'C', 'CS3': 'C', 'CS4': 'C', 'CS8': 'N', + 'CSA': 'C', 'CSB': 'C', 'CSD': 'C', 'CSE': 'C', 'CSF': 'C', 'CSI': 'G', + 'CSJ': 'C', 'CSL': 'C', 'CSO': 'C', 'CSP': 'C', 'CSR': 'C', 'CSS': 'C', + 'CSU': 'C', 'CSW': 'C', 'CSX': 'C', 'CSZ': 'C', 'CTE': 'W', 'CTG': 'T', + 'CTH': 'T', 'CUC': 'X', 'CWR': 'S', 'CXM': 'M', 'CY0': 'C', 'CY1': 'C', + 'CY3': 'C', 'CY4': 'C', 'CYA': 'C', 'CYD': 'C', 'CYF': 'C', 'CYG': 'C', + 'CYJ': 'X', 'CYM': 'C', 'CYQ': 'C', 'CYR': 'C', 'CYS': 'C', 'CZ2': 'C', + 'CZZ': 'C', 'D11': 'T', 'D1P': 'N', 'D3': 'N', 'D33': 'N', 'D3P': 'G', + 'D3T': 'T', 'D4M': 'T', 'D4P': 'X', 'DA': 'A', 'DA2': 'X', 'DAB': 'A', + 'DAH': 'F', 'DAL': 'A', 'DAR': 'R', 'DAS': 'D', 'DBB': 'T', 'DBM': 'N', + 'DBS': 'S', 'DBU': 'T', 'DBY': 'Y', 'DBZ': 'A', 'DC': 'C', 'DC2': 'C', + 'DCG': 'G', 'DCI': 'X', 'DCL': 'X', 'DCT': 'C', 'DCY': 'C', 'DDE': 'H', + 'DDG': 'G', 'DDN': 'U', 'DDX': 'N', 'DFC': 'C', 'DFG': 'G', 'DFI': 'X', + 'DFO': 'X', 'DFT': 'N', 'DG': 'G', 'DGH': 'G', 'DGI': 'G', 'DGL': 'E', + 'DGN': 'Q', 'DHA': 'S', 'DHI': 'H', 'DHL': 'X', 'DHN': 'V', 'DHP': 'X', + 'DHU': 'U', 'DHV': 'V', 'DI': 'I', 'DIL': 'I', 'DIR': 'R', 'DIV': 'V', + 'DLE': 'L', 'DLS': 'K', 'DLY': 'K', 'DM0': 'K', 'DMH': 'N', 'DMK': 'D', + 'DMT': 'X', 'DN': 'N', 'DNE': 'L', 'DNG': 'L', 'DNL': 'K', 'DNM': 'L', + 'DNP': 'A', 'DNR': 'C', 'DNS': 'K', 'DOA': 'X', 'DOC': 'C', 'DOH': 'D', + 'DON': 'L', 'DPB': 'T', 'DPH': 'F', 'DPL': 'P', 'DPP': 'A', 'DPQ': 'Y', + 'DPR': 'P', 'DPY': 'N', 'DRM': 'U', 'DRP': 'N', 'DRT': 'T', 'DRZ': 'N', + 'DSE': 'S', 'DSG': 'N', 'DSN': 'S', 'DSP': 'D', 'DT': 'T', 'DTH': 'T', + 'DTR': 'W', 'DTY': 'Y', 'DU': 'U', 'DVA': 'V', 'DXD': 'N', 'DXN': 'N', + 'DYS': 'C', 'DZM': 'A', 'E': 'A', 'E1X': 'A', 'ECC': 'Q', 'EDA': 'A', + 'EFC': 'C', 'EHP': 'F', 'EIT': 'T', 'ENP': 'N', 'ESB': 'Y', 'ESC': 'M', + 'EXB': 'X', 'EXY': 'L', 'EY5': 'N', 'EYS': 'X', 'F2F': 'F', 'FA2': 'A', + 'FA5': 'N', 'FAG': 'N', 'FAI': 'N', 'FB5': 'A', 'FB6': 'A', 'FCL': 'F', + 'FFD': 'N', 'FGA': 'E', 'FGL': 'G', 'FGP': 'S', 'FHL': 'X', 'FHO': 'K', + 'FHU': 'U', 'FLA': 'A', 'FLE': 'L', 'FLT': 'Y', 'FME': 'M', 'FMG': 'G', + 'FMU': 'N', 'FOE': 'C', 'FOX': 'G', 'FP9': 'P', 'FPA': 'F', 'FRD': 'X', + 'FT6': 'W', 'FTR': 'W', 'FTY': 'Y', 'FVA': 'V', 'FZN': 'K', 'G': 'G', + 'G25': 'G', 'G2L': 'G', 'G2S': 'G', 'G31': 'G', 'G32': 'G', 'G33': 'G', + 'G36': 'G', 'G38': 'G', 'G42': 'G', 'G46': 'G', 'G47': 'G', 'G48': 'G', + 'G49': 'G', 'G4P': 'N', 'G7M': 'G', 'GAO': 'G', 'GAU': 'E', 'GCK': 'C', + 'GCM': 'X', 'GDP': 'G', 'GDR': 'G', 'GFL': 'G', 'GGL': 'E', 'GH3': 'G', + 'GHG': 'Q', 'GHP': 'G', 'GL3': 'G', 'GLH': 'Q', 'GLJ': 'E', 'GLK': 'E', + 'GLM': 'X', 'GLN': 'Q', 'GLQ': 'E', 'GLU': 'E', 'GLX': 'Z', 'GLY': 'G', + 'GLZ': 'G', 'GMA': 'E', 'GMS': 'G', 'GMU': 'U', 'GN7': 'G', 'GND': 'X', + 'GNE': 'N', 'GOM': 'G', 'GPL': 'K', 'GS': 'G', 'GSC': 'G', 'GSR': 'G', + 'GSS': 'G', 'GSU': 'E', 'GT9': 'C', 'GTP': 'G', 'GVL': 'X', 'H2U': 'U', + 'H5M': 'P', 'HAC': 'A', 'HAR': 'R', 'HBN': 'H', 'HCS': 'X', 'HDP': 'U', + 'HEU': 'U', 'HFA': 'X', 'HGL': 'X', 'HHI': 'H', 'HIA': 'H', 'HIC': 'H', + 'HIP': 'H', 'HIQ': 'H', 'HIS': 'H', 'HL2': 'L', 'HLU': 'L', 'HMR': 'R', + 'HOL': 'N', 'HPC': 'F', 'HPE': 'F', 'HPH': 'F', 'HPQ': 'F', 'HQA': 'A', + 'HRG': 'R', 'HRP': 'W', 'HS8': 'H', 'HS9': 'H', 'HSE': 'S', 'HSL': 'S', + 'HSO': 'H', 'HTI': 'C', 'HTN': 'N', 'HTR': 'W', 'HV5': 'A', 'HVA': 'V', + 'HY3': 'P', 'HYP': 'P', 'HZP': 'P', 'I': 'I', 'I2M': 'I', 'I58': 'K', + 'I5C': 'C', 'IAM': 'A', 'IAR': 'R', 'IAS': 'D', 'IC': 'C', 'IEL': 'K', + 'IG': 'G', 'IGL': 'G', 'IGU': 'G', 'IIL': 'I', 'ILE': 'I', 'ILG': 'E', + 'ILX': 'I', 'IMC': 'C', 'IML': 'I', 'IOY': 'F', 'IPG': 'G', 'IPN': 'N', + 'IRN': 'N', 'IT1': 'K', 'IU': 'U', 'IYR': 'Y', 'IYT': 'T', 'IZO': 'M', + 'JJJ': 'C', 'JJK': 'C', 'JJL': 'C', 'JW5': 'N', 'K1R': 'C', 'KAG': 'G', + 'KCX': 'K', 'KGC': 'K', 'KNB': 'A', 'KOR': 'M', 'KPI': 'K', 'KST': 'K', + 'KYQ': 'K', 'L2A': 'X', 'LA2': 'K', 'LAA': 'D', 'LAL': 'A', 'LBY': 'K', + 'LC': 'C', 'LCA': 'A', 'LCC': 'N', 'LCG': 'G', 'LCH': 'N', 'LCK': 'K', + 'LCX': 'K', 'LDH': 'K', 'LED': 'L', 'LEF': 'L', 'LEH': 'L', 'LEI': 'V', + 'LEM': 'L', 'LEN': 'L', 'LET': 'X', 'LEU': 'L', 'LEX': 'L', 'LG': 'G', + 'LGP': 'G', 'LHC': 'X', 'LHU': 'U', 'LKC': 'N', 'LLP': 'K', 'LLY': 'K', + 'LME': 'E', 'LMF': 'K', 'LMQ': 'Q', 'LMS': 'N', 'LP6': 'K', 'LPD': 'P', + 'LPG': 'G', 'LPL': 'X', 'LPS': 'S', 'LSO': 'X', 'LTA': 'X', 'LTR': 'W', + 'LVG': 'G', 'LVN': 'V', 'LYF': 'K', 'LYK': 'K', 'LYM': 'K', 'LYN': 'K', + 'LYR': 'K', 'LYS': 'K', 'LYX': 'K', 'LYZ': 'K', 'M0H': 'C', 'M1G': 'G', + 'M2G': 'G', 'M2L': 'K', 'M2S': 'M', 'M30': 'G', 'M3L': 'K', 'M5M': 'C', + 'MA': 'A', 'MA6': 'A', 'MA7': 'A', 'MAA': 'A', 'MAD': 'A', 'MAI': 'R', + 'MBQ': 'Y', 'MBZ': 'N', 'MC1': 'S', 'MCG': 'X', 'MCL': 'K', 'MCS': 'C', + 'MCY': 'C', 'MD3': 'C', 'MD6': 'G', 'MDH': 'X', 'MDR': 'N', 'MEA': 'F', + 'MED': 'M', 'MEG': 'E', 'MEN': 'N', 'MEP': 'U', 'MEQ': 'Q', 'MET': 'M', + 'MEU': 'G', 'MF3': 'X', 'MG1': 'G', 'MGG': 'R', 'MGN': 'Q', 'MGQ': 'A', + 'MGV': 'G', 'MGY': 'G', 'MHL': 'L', 'MHO': 'M', 'MHS': 'H', 'MIA': 'A', + 'MIS': 'S', 'MK8': 'L', 'ML3': 'K', 'MLE': 'L', 'MLL': 'L', 'MLY': 'K', + 'MLZ': 'K', 'MME': 'M', 'MMO': 'R', 'MMT': 'T', 'MND': 'N', 'MNL': 'L', + 'MNU': 'U', 'MNV': 'V', 'MOD': 'X', 'MP8': 'P', 'MPH': 'X', 'MPJ': 'X', + 'MPQ': 'G', 'MRG': 'G', 'MSA': 'G', 'MSE': 'M', 'MSL': 'M', 'MSO': 'M', + 'MSP': 'X', 'MT2': 'M', 'MTR': 'T', 'MTU': 'A', 'MTY': 'Y', 'MVA': 'V', + 'N': 'N', 'N10': 'S', 'N2C': 'X', 'N5I': 'N', 'N5M': 'C', 'N6G': 'G', + 'N7P': 'P', 'NA8': 'A', 'NAL': 'A', 'NAM': 'A', 'NB8': 'N', 'NBQ': 'Y', + 'NC1': 'S', 'NCB': 'A', 'NCX': 'N', 'NCY': 'X', 'NDF': 'F', 'NDN': 'U', + 'NEM': 'H', 'NEP': 'H', 'NF2': 'N', 'NFA': 'F', 'NHL': 'E', 'NIT': 'X', + 'NIY': 'Y', 'NLE': 'L', 'NLN': 'L', 'NLO': 'L', 'NLP': 'L', 'NLQ': 'Q', + 'NMC': 'G', 'NMM': 'R', 'NMS': 'T', 'NMT': 'T', 'NNH': 'R', 'NP3': 'N', + 'NPH': 'C', 'NPI': 'A', 'NSK': 'X', 'NTY': 'Y', 'NVA': 'V', 'NYM': 'N', + 'NYS': 'C', 'NZH': 'H', 'O12': 'X', 'O2C': 'N', 'O2G': 'G', 'OAD': 'N', + 'OAS': 'S', 'OBF': 'X', 'OBS': 'X', 'OCS': 'C', 'OCY': 'C', 'ODP': 'N', + 'OHI': 'H', 'OHS': 'D', 'OIC': 'X', 'OIP': 'I', 'OLE': 'X', 'OLT': 'T', + 'OLZ': 'S', 'OMC': 'C', 'OMG': 'G', 'OMT': 'M', 'OMU': 'U', 'ONE': 'U', + 'ONH': 'A', 'ONL': 'X', 'OPR': 'R', 'ORN': 'A', 'ORQ': 'R', 'OSE': 'S', + 'OTB': 'X', 'OTH': 'T', 'OTY': 'Y', 'OXX': 'D', 'P': 'G', 'P1L': 'C', + 'P1P': 'N', 'P2T': 'T', 'P2U': 'U', 'P2Y': 'P', 'P5P': 'A', 'PAQ': 'Y', + 'PAS': 'D', 'PAT': 'W', 'PAU': 'A', 'PBB': 'C', 'PBF': 'F', 'PBT': 'N', + 'PCA': 'E', 'PCC': 'P', 'PCE': 'X', 'PCS': 'F', 'PDL': 'X', 'PDU': 'U', + 'PEC': 'C', 'PF5': 'F', 'PFF': 'F', 'PFX': 'X', 'PG1': 'S', 'PG7': 'G', + 'PG9': 'G', 'PGL': 'X', 'PGN': 'G', 'PGP': 'G', 'PGY': 'G', 'PHA': 'F', + 'PHD': 'D', 'PHE': 'F', 'PHI': 'F', 'PHL': 'F', 'PHM': 'F', 'PIV': 'X', + 'PLE': 'L', 'PM3': 'F', 'PMT': 'C', 'POM': 'P', 'PPN': 'F', 'PPU': 'A', + 'PPW': 'G', 'PQ1': 'N', 'PR3': 'C', 'PR5': 'A', 'PR9': 'P', 'PRN': 'A', + 'PRO': 'P', 'PRS': 'P', 'PSA': 'F', 'PSH': 'H', 'PST': 'T', 'PSU': 'U', + 'PSW': 'C', 'PTA': 'X', 'PTH': 'Y', 'PTM': 'Y', 'PTR': 'Y', 'PU': 'A', + 'PUY': 'N', 'PVH': 'H', 'PVL': 'X', 'PYA': 'A', 'PYO': 'U', 'PYX': 'C', + 'PYY': 'N', 'QMM': 'Q', 'QPA': 'C', 'QPH': 'F', 'QUO': 'G', 'R': 'A', + 'R1A': 'C', 'R4K': 'W', 'RE0': 'W', 'RE3': 'W', 'RIA': 'A', 'RMP': 'A', + 'RON': 'X', 'RT': 'T', 'RTP': 'N', 'S1H': 'S', 'S2C': 'C', 'S2D': 'A', + 'S2M': 'T', 'S2P': 'A', 'S4A': 'A', 'S4C': 'C', 'S4G': 'G', 'S4U': 'U', + 'S6G': 'G', 'SAC': 'S', 'SAH': 'C', 'SAR': 'G', 'SBL': 'S', 'SC': 'C', + 'SCH': 'C', 'SCS': 'C', 'SCY': 'C', 'SD2': 'X', 'SDG': 'G', 'SDP': 'S', + 'SEB': 'S', 'SEC': 'A', 'SEG': 'A', 'SEL': 'S', 'SEM': 'S', 'SEN': 'S', + 'SEP': 'S', 'SER': 'S', 'SET': 'S', 'SGB': 'S', 'SHC': 'C', 'SHP': 'G', + 'SHR': 'K', 'SIB': 'C', 'SLA': 'P', 'SLR': 'P', 'SLZ': 'K', 'SMC': 'C', + 'SME': 'M', 'SMF': 'F', 'SMP': 'A', 'SMT': 'T', 'SNC': 'C', 'SNN': 'N', + 'SOC': 'C', 'SOS': 'N', 'SOY': 'S', 'SPT': 'T', 'SRA': 'A', 'SSU': 'U', + 'STY': 'Y', 'SUB': 'X', 'SUN': 'S', 'SUR': 'U', 'SVA': 'S', 'SVV': 'S', + 'SVW': 'S', 'SVX': 'S', 'SVY': 'S', 'SVZ': 'X', 'SYS': 'C', 'T': 'T', + 'T11': 'F', 'T23': 'T', 'T2S': 'T', 'T2T': 'N', 'T31': 'U', 'T32': 'T', + 'T36': 'T', 'T37': 'T', 'T38': 'T', 'T39': 'T', 'T3P': 'T', 'T41': 'T', + 'T48': 'T', 'T49': 'T', 'T4S': 'T', 'T5O': 'U', 'T5S': 'T', 'T66': 'X', + 'T6A': 'A', 'TA3': 'T', 'TA4': 'X', 'TAF': 'T', 'TAL': 'N', 'TAV': 'D', + 'TBG': 'V', 'TBM': 'T', 'TC1': 'C', 'TCP': 'T', 'TCQ': 'Y', 'TCR': 'W', + 'TCY': 'A', 'TDD': 'L', 'TDY': 'T', 'TFE': 'T', 'TFO': 'A', 'TFQ': 'F', + 'TFT': 'T', 'TGP': 'G', 'TH6': 'T', 'THC': 'T', 'THO': 'X', 'THR': 'T', + 'THX': 'N', 'THZ': 'R', 'TIH': 'A', 'TLB': 'N', 'TLC': 'T', 'TLN': 'U', + 'TMB': 'T', 'TMD': 'T', 'TNB': 'C', 'TNR': 'S', 'TOX': 'W', 'TP1': 'T', + 'TPC': 'C', 'TPG': 'G', 'TPH': 'X', 'TPL': 'W', 'TPO': 'T', 'TPQ': 'Y', + 'TQI': 'W', 'TQQ': 'W', 'TRF': 'W', 'TRG': 'K', 'TRN': 'W', 'TRO': 'W', + 'TRP': 'W', 'TRQ': 'W', 'TRW': 'W', 'TRX': 'W', 'TS': 'N', 'TST': 'X', + 'TT': 'N', 'TTD': 'T', 'TTI': 'U', 'TTM': 'T', 'TTQ': 'W', 'TTS': 'Y', + 'TY1': 'Y', 'TY2': 'Y', 'TY3': 'Y', 'TY5': 'Y', 'TYB': 'Y', 'TYI': 'Y', + 'TYJ': 'Y', 'TYN': 'Y', 'TYO': 'Y', 'TYQ': 'Y', 'TYR': 'Y', 'TYS': 'Y', + 'TYT': 'Y', 'TYU': 'N', 'TYW': 'Y', 'TYX': 'X', 'TYY': 'Y', 'TZB': 'X', + 'TZO': 'X', 'U': 'U', 'U25': 'U', 'U2L': 'U', 'U2N': 'U', 'U2P': 'U', + 'U31': 'U', 'U33': 'U', 'U34': 'U', 'U36': 'U', 'U37': 'U', 'U8U': 'U', + 'UAR': 'U', 'UCL': 'U', 'UD5': 'U', 'UDP': 'N', 'UFP': 'N', 'UFR': 'U', + 'UFT': 'U', 'UMA': 'A', 'UMP': 'U', 'UMS': 'U', 'UN1': 'X', 'UN2': 'X', + 'UNK': 'X', 'UR3': 'U', 'URD': 'U', 'US1': 'U', 'US2': 'U', 'US3': 'T', + 'US5': 'U', 'USM': 'U', 'VAD': 'V', 'VAF': 'V', 'VAL': 'V', 'VB1': 'K', + 'VDL': 'X', 'VLL': 'X', 'VLM': 'X', 'VMS': 'X', 'VOL': 'X', 'X': 'G', + 'X2W': 'E', 'X4A': 'N', 'XAD': 'A', 'XAE': 'N', 'XAL': 'A', 'XAR': 'N', + 'XCL': 'C', 'XCN': 'C', 'XCP': 'X', 'XCR': 'C', 'XCS': 'N', 'XCT': 'C', + 'XCY': 'C', 'XGA': 'N', 'XGL': 'G', 'XGR': 'G', 'XGU': 'G', 'XPR': 'P', + 'XSN': 'N', 'XTH': 'T', 'XTL': 'T', 'XTR': 'T', 'XTS': 'G', 'XTY': 'N', + 'XUA': 'A', 'XUG': 'G', 'XX1': 'K', 'Y': 'A', 'YCM': 'C', 'YG': 'G', + 'YOF': 'Y', 'YRR': 'N', 'YYG': 'G', 'Z': 'C', 'Z01': 'A', 'ZAD': 'A', + 'ZAL': 'A', 'ZBC': 'C', 'ZBU': 'U', 'ZCL': 'F', 'ZCY': 'C', 'ZDU': 'U', + 'ZFB': 'X', 'ZGU': 'G', 'ZHP': 'N', 'ZTH': 'T', 'ZU0': 'T', 'ZZJ': 'A', +} +# common_typos_enable +# pyformat: enable + + +@functools.lru_cache(maxsize=64) +def letters_three_to_one(restype: str, *, default: str) -> str: + """Returns single letter name if one exists otherwise returns default.""" + return CCD_NAME_TO_ONE_LETTER.get(restype, default) + + +ALA = sys.intern('ALA') +ARG = sys.intern('ARG') +ASN = sys.intern('ASN') +ASP = sys.intern('ASP') +CYS = sys.intern('CYS') +GLN = sys.intern('GLN') +GLU = sys.intern('GLU') +GLY = sys.intern('GLY') +HIS = sys.intern('HIS') +ILE = sys.intern('ILE') +LEU = sys.intern('LEU') +LYS = sys.intern('LYS') +MET = sys.intern('MET') +PHE = sys.intern('PHE') +PRO = sys.intern('PRO') +SER = sys.intern('SER') +THR = sys.intern('THR') +TRP = sys.intern('TRP') +TYR = sys.intern('TYR') +VAL = sys.intern('VAL') +UNK = sys.intern('UNK') +GAP = sys.intern('-') + +# Unknown ligand. +UNL = sys.intern('UNL') + +# Non-standard version of MET (with Se instead of S), but often appears in PDB. +MSE = sys.intern('MSE') + +# 20 standard protein amino acids (no unknown). +PROTEIN_TYPES: tuple[str, ...] = ( + ALA, ARG, ASN, ASP, CYS, GLN, GLU, GLY, HIS, ILE, LEU, LYS, MET, PHE, PRO, + SER, THR, TRP, TYR, VAL, +) # pyformat: disable + +# 20 standard protein amino acids plus the unknown (UNK) amino acid. +PROTEIN_TYPES_WITH_UNKNOWN: tuple[str, ...] = PROTEIN_TYPES + (UNK,) + +# This is the standard residue order when coding AA type as a number. +# Reproduce it by taking 3-letter AA codes and sorting them alphabetically. +# For legacy reasons this only refers to protein residues. + +PROTEIN_TYPES_ONE_LETTER: tuple[str, ...] = ( + 'A', 'R', 'N', 'D', 'C', 'Q', 'E', 'G', 'H', 'I', 'L', 'K', 'M', 'F', 'P', + 'S', 'T', 'W', 'Y', 'V', +) # pyformat: disable + +PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = ( + PROTEIN_TYPES_ONE_LETTER + ('X',) +) +PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = ( + PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN + (GAP,) +) + +PROTEIN_TYPES_ONE_LETTER_TO_INT: Mapping[str, int] = { + r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER) +} +PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { + r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN) +} + +PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP_TO_INT: Mapping[str, int] = { + r: i for i, r in enumerate(PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP) +} + + +PROTEIN_COMMON_ONE_TO_THREE: Mapping[str, str] = { + 'A': ALA, + 'R': ARG, + 'N': ASN, + 'D': ASP, + 'C': CYS, + 'Q': GLN, + 'E': GLU, + 'G': GLY, + 'H': HIS, + 'I': ILE, + 'L': LEU, + 'K': LYS, + 'M': MET, + 'F': PHE, + 'P': PRO, + 'S': SER, + 'T': THR, + 'W': TRP, + 'Y': TYR, + 'V': VAL, +} + +PROTEIN_COMMON_THREE_TO_ONE: Mapping[str, str] = { + v: k for k, v in PROTEIN_COMMON_ONE_TO_THREE.items() +} + +A = sys.intern('A') +G = sys.intern('G') +C = sys.intern('C') +U = sys.intern('U') +T = sys.intern('T') + +DA = sys.intern('DA') +DG = sys.intern('DG') +DC = sys.intern('DC') +DT = sys.intern('DT') + +UNK_NUCLEIC_ONE_LETTER = sys.intern('N') # Unknown nucleic acid single letter. +UNK_RNA = sys.intern('N') # Unknown RNA. +UNK_DNA = sys.intern('DN') # Unknown DNA residue (differs from N). + +RNA_TYPES: tuple[str, ...] = (A, G, C, U) +DNA_TYPES: tuple[str, ...] = (DA, DG, DC, DT) + +NUCLEIC_TYPES: tuple[str, ...] = RNA_TYPES + DNA_TYPES +# Without UNK DNA. +NUCLEIC_TYPES_WITH_UNKNOWN: tuple[str, ...] = NUCLEIC_TYPES + ( + UNK_NUCLEIC_ONE_LETTER, +) +NUCLEIC_TYPES_WITH_2_UNKS: tuple[str, ...] = NUCLEIC_TYPES + ( + UNK_RNA, + UNK_DNA, +) + +RNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = RNA_TYPES + (UNK_RNA,) +RNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { + r: i for i, r in enumerate(RNA_TYPES_ONE_LETTER_WITH_UNKNOWN) +} + +DNA_TYPES_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES + (UNK_DNA,) +DNA_TYPES_ONE_LETTER: tuple[str, ...] = (A, G, C, T) +DNA_TYPES_ONE_LETTER_WITH_UNKNOWN: tuple[str, ...] = DNA_TYPES_ONE_LETTER + ( + UNK_NUCLEIC_ONE_LETTER, +) +DNA_TYPES_ONE_LETTER_WITH_UNKNOWN_TO_INT: Mapping[str, int] = { + r: i for i, r in enumerate(DNA_TYPES_ONE_LETTER_WITH_UNKNOWN) +} +DNA_COMMON_ONE_TO_TWO: Mapping[str, str] = { + 'A': 'DA', + 'G': 'DG', + 'C': 'DC', + 'T': 'DT', +} + +STANDARD_POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES + NUCLEIC_TYPES +POLYMER_TYPES: tuple[str, ...] = PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES +POLYMER_TYPES_WITH_UNKNOWN: tuple[str, ...] = ( + PROTEIN_TYPES_WITH_UNKNOWN + NUCLEIC_TYPES_WITH_UNKNOWN +) +POLYMER_TYPES_WITH_GAP: tuple[str, ...] = PROTEIN_TYPES + \ + (GAP,) + NUCLEIC_TYPES +POLYMER_TYPES_WITH_UNKNOWN_AND_GAP: tuple[str, ...] = ( + PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_UNKNOWN +) +POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP: tuple[str, ...] = ( + PROTEIN_TYPES_WITH_UNKNOWN + (GAP,) + NUCLEIC_TYPES_WITH_2_UNKS +) + +POLYMER_TYPES_ORDER = {restype: i for i, restype in enumerate(POLYMER_TYPES)} + +POLYMER_TYPES_ORDER_WITH_UNKNOWN = { + restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN) +} + +POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP = { + restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_UNKNOWN_AND_GAP) +} + +POLYMER_TYPES_ORDER_WITH_ALL_UNKS_AND_GAP = { + restype: i for i, restype in enumerate(POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP) +} + +POLYMER_TYPES_NUM = len(POLYMER_TYPES) # := 29. +POLYMER_TYPES_NUM_WITH_UNKNOWN = len(POLYMER_TYPES_WITH_UNKNOWN) # := 30. +POLYMER_TYPES_NUM_WITH_GAP = len(POLYMER_TYPES_WITH_GAP) # := 29. +POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP = len( + POLYMER_TYPES_WITH_UNKNOWN_AND_GAP +) # := 31. +POLYMER_TYPES_NUM_ORDER_WITH_ALL_UNKS_AND_GAP = len( + POLYMER_TYPES_WITH_ALL_UNKS_AND_GAP +) # := 32. + +WATER_TYPES: tuple[str, ...] = ('HOH', 'DOD') + +UNKNOWN_TYPES: tuple[str, ...] = (UNK, UNK_RNA, UNK_DNA, UNL) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/side_chains.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/side_chains.py new file mode 100644 index 000000000..0e8cd1297 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/side_chains.py @@ -0,0 +1,112 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Constants associated with side chains.""" + +from collections.abc import Mapping, Sequence +import itertools + +# Format: The list for each AA type contains chi1, chi2, chi3, chi4 in +# this order (or a relevant subset from chi1 onwards). ALA and GLY don't have +# chi angles so their chi angle lists are empty. +CHI_ANGLES_ATOMS: Mapping[str, Sequence[tuple[str, ...]]] = { + 'ALA': [], + # Chi5 in arginine is always 0 +- 5 degrees, so ignore it. + 'ARG': [ + ('N', 'CA', 'CB', 'CG'), + ('CA', 'CB', 'CG', 'CD'), + ('CB', 'CG', 'CD', 'NE'), + ('CG', 'CD', 'NE', 'CZ'), + ], + 'ASN': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')], + 'ASP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'OD1')], + 'CYS': [('N', 'CA', 'CB', 'SG')], + 'GLN': [ + ('N', 'CA', 'CB', 'CG'), + ('CA', 'CB', 'CG', 'CD'), + ('CB', 'CG', 'CD', 'OE1'), + ], + 'GLU': [ + ('N', 'CA', 'CB', 'CG'), + ('CA', 'CB', 'CG', 'CD'), + ('CB', 'CG', 'CD', 'OE1'), + ], + 'GLY': [], + 'HIS': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'ND1')], + 'ILE': [('N', 'CA', 'CB', 'CG1'), ('CA', 'CB', 'CG1', 'CD1')], + 'LEU': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], + 'LYS': [ + ('N', 'CA', 'CB', 'CG'), + ('CA', 'CB', 'CG', 'CD'), + ('CB', 'CG', 'CD', 'CE'), + ('CG', 'CD', 'CE', 'NZ'), + ], + 'MET': [ + ('N', 'CA', 'CB', 'CG'), + ('CA', 'CB', 'CG', 'SD'), + ('CB', 'CG', 'SD', 'CE'), + ], + 'PHE': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], + 'PRO': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD')], + 'SER': [('N', 'CA', 'CB', 'OG')], + 'THR': [('N', 'CA', 'CB', 'OG1')], + 'TRP': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], + 'TYR': [('N', 'CA', 'CB', 'CG'), ('CA', 'CB', 'CG', 'CD1')], + 'VAL': [('N', 'CA', 'CB', 'CG1')], +} + +CHI_GROUPS_FOR_ATOM = {} +for res_name, chi_angle_atoms_for_res in CHI_ANGLES_ATOMS.items(): + for chi_group_i, chi_group in enumerate(chi_angle_atoms_for_res): + for atom_i, atom in enumerate(chi_group): + CHI_GROUPS_FOR_ATOM.setdefault((res_name, atom), []).append( + (chi_group_i, atom_i) + ) + +# Mapping from (residue_name, atom_name) pairs to the atom's chi group index +# and atom index within that group. +CHI_GROUPS_FOR_ATOM: Mapping[tuple[str, str], Sequence[tuple[int, int]]] = ( + CHI_GROUPS_FOR_ATOM +) + +MAX_NUM_CHI_ANGLES: int = 4 +ATOMS_PER_CHI_ANGLE: int = 4 + +# A list of atoms for each AA type that are involved in chi angle calculations. +CHI_ATOM_SETS: Mapping[str, set[str]] = { + residue_name: set(itertools.chain(*atoms)) + for residue_name, atoms in CHI_ANGLES_ATOMS.items() +} + +# If chi angles given in fixed-length array, this matrix determines how to mask +# them for each AA type. The order is as per restype_order (see below). +CHI_ANGLES_MASK: Sequence[Sequence[float]] = ( + (0.0, 0.0, 0.0, 0.0), # ALA + (1.0, 1.0, 1.0, 1.0), # ARG + (1.0, 1.0, 0.0, 0.0), # ASN + (1.0, 1.0, 0.0, 0.0), # ASP + (1.0, 0.0, 0.0, 0.0), # CYS + (1.0, 1.0, 1.0, 0.0), # GLN + (1.0, 1.0, 1.0, 0.0), # GLU + (0.0, 0.0, 0.0, 0.0), # GLY + (1.0, 1.0, 0.0, 0.0), # HIS + (1.0, 1.0, 0.0, 0.0), # ILE + (1.0, 1.0, 0.0, 0.0), # LEU + (1.0, 1.0, 1.0, 1.0), # LYS + (1.0, 1.0, 1.0, 0.0), # MET + (1.0, 1.0, 0.0, 0.0), # PHE + (1.0, 1.0, 0.0, 0.0), # PRO + (1.0, 0.0, 0.0, 0.0), # SER + (1.0, 0.0, 0.0, 0.0), # THR + (1.0, 1.0, 0.0, 0.0), # TRP + (1.0, 1.0, 0.0, 0.0), # TYR + (1.0, 0.0, 0.0, 0.0), # VAL +) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/cpp.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/cpp.cc new file mode 100644 index 000000000..b2286b5c3 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/cpp.cc @@ -0,0 +1,48 @@ +/* +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + */ + +#include "alphafold3/data/cpp/msa_profile_pybind.h" +#include "alphafold3/model/mkdssp_pybind.h" +#include "alphafold3/parsers/cpp/cif_dict_pybind.h" +#include "alphafold3/parsers/cpp/fasta_iterator_pybind.h" +#include "alphafold3/parsers/cpp/msa_conversion_pybind.h" +#include "alphafold3/structure/cpp/aggregation_pybind.h" +#include "alphafold3/structure/cpp/membership_pybind.h" +#include "alphafold3/structure/cpp/mmcif_atom_site_pybind.h" +#include "alphafold3/structure/cpp/mmcif_layout_pybind.h" +#include "alphafold3/structure/cpp/mmcif_struct_conn_pybind.h" +#include "alphafold3/structure/cpp/mmcif_utils_pybind.h" +#include "alphafold3/structure/cpp/string_array_pybind.h" +#include "pybind11/pybind11.h" + +namespace alphafold3 { +namespace { + +// Include all modules as submodules to simplify building. +PYBIND11_MODULE(cpp, m) { + RegisterModuleCifDict(m.def_submodule("cif_dict")); + RegisterModuleFastaIterator(m.def_submodule("fasta_iterator")); + RegisterModuleMsaConversion(m.def_submodule("msa_conversion")); + RegisterModuleMmcifLayout(m.def_submodule("mmcif_layout")); + RegisterModuleMmcifStructConn(m.def_submodule("mmcif_struct_conn")); + RegisterModuleMembership(m.def_submodule("membership")); + RegisterModuleMmcifUtils(m.def_submodule("mmcif_utils")); + RegisterModuleAggregation(m.def_submodule("aggregation")); + RegisterModuleStringArray(m.def_submodule("string_array")); + RegisterModuleMmcifAtomSite(m.def_submodule("mmcif_atom_site")); + RegisterModuleMkdssp(m.def_submodule("mkdssp")); + RegisterModuleMsaProfile(m.def_submodule("msa_profile")); +} + +} // namespace +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc new file mode 100644 index 000000000..83b86f4e2 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc @@ -0,0 +1,79 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include "absl/strings/str_cat.h" +#include "pybind11/cast.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" + +namespace { + +namespace py = pybind11; + +py::array_t ComputeMsaProfile( + const py::array_t& msa, int num_residue_types) { + if (msa.size() == 0) { + throw py::value_error("The MSA must be non-empty."); + } + if (msa.ndim() != 2) { + throw py::value_error(absl::StrCat("The MSA must be rectangular, got ", + msa.ndim(), "-dimensional MSA array.")); + } + const int msa_depth = msa.shape()[0]; + const int sequence_length = msa.shape()[1]; + + py::array_t profile({sequence_length, num_residue_types}); + std::fill(profile.mutable_data(), profile.mutable_data() + profile.size(), + 0.0f); + auto profile_unchecked = profile.mutable_unchecked<2>(); + + const double normalized_count = 1.0 / msa_depth; + const int* msa_it = msa.data(); + for (int row_index = 0; row_index < msa_depth; ++row_index) { + for (int column_index = 0; column_index < sequence_length; ++column_index) { + const int residue_code = *(msa_it++); + if (residue_code < 0 || residue_code >= num_residue_types) { + throw py::value_error( + absl::StrCat("All residue codes must be positive and smaller than " + "num_residue_types ", + num_residue_types, ", got ", residue_code)); + } + profile_unchecked(column_index, residue_code) += normalized_count; + } + } + return profile; +} + +constexpr char kComputeMsaProfileDoc[] = R"( +Computes MSA profile for the given encoded MSA. + +Args: + msa: A Numpy array of shape (num_msa, num_res) with the integer coded MSA. + num_residue_types: Integer that determines the number of unique residue types. + This will determine the shape of the output profile. + +Returns: + A float Numpy array of shape (num_res, num_residue_types) with residue + frequency (residue type count normalized by MSA depth) for every column of the + MSA. +)"; + +} // namespace + +namespace alphafold3 { + +void RegisterModuleMsaProfile(pybind11::module m) { + m.def("compute_msa_profile", &ComputeMsaProfile, py::arg("msa"), + py::arg("num_residue_types"), py::doc(kComputeMsaProfileDoc + 1)); +} + +} // namespace alphafold3 \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.h new file mode 100644 index 000000000..1145d331b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.h @@ -0,0 +1,25 @@ +/* +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMsaProfile(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_DATA_PYTHON_MSA_PROFILE_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/featurisation.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/featurisation.py new file mode 100644 index 000000000..ee351626c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/featurisation.py @@ -0,0 +1,90 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""AlphaFold 3 featurisation pipeline.""" + +from collections.abc import Sequence +import datetime +import time + +from alphafold3.common import folding_input +from alphafold3.constants import chemical_components +from alphafold3.model import features +from alphafold3.model.pipeline import pipeline +import numpy as np + + +def validate_fold_input(fold_input: folding_input.Input): + """Validates the fold input contains MSA and templates for featurisation.""" + for i, chain in enumerate(fold_input.protein_chains): + if chain.unpaired_msa is None: + raise ValueError(f'Protein chain {i + 1} is missing unpaired MSA.') + if chain.paired_msa is None: + raise ValueError(f'Protein chain {i + 1} is missing paired MSA.') + if chain.templates is None: + raise ValueError(f'Protein chain {i + 1} is missing Templates.') + for i, chain in enumerate(fold_input.rna_chains): + if chain.unpaired_msa is None: + raise ValueError(f'RNA chain {i + 1} is missing unpaired MSA.') + + +def featurise_input( + fold_input: folding_input.Input, + ccd: chemical_components.Ccd, + buckets: Sequence[int] | None, + max_template_date: datetime.date | None = None, + verbose: bool = False, +) -> Sequence[features.BatchDict]: + """Featurise the folding input. + + Args: + fold_input: The input to featurise. + ccd: The chemical components dictionary. + buckets: Bucket sizes to pad the data to, to avoid excessive re-compilation + of the model. If None, calculate the appropriate bucket size from the + number of tokens. If not None, must be a sequence of at least one integer, + in strictly increasing order. Will raise an error if the number of tokens + is more than the largest bucket size. + max_template_date: Optional max template date to prevent data leakage in + validation. + verbose: Whether to print progress messages. + + Returns: + A featurised batch for each rng_seed in the input. + """ + validate_fold_input(fold_input) + + # Set up data pipeline for single use. + data_pipeline = pipeline.WholePdbPipeline( + config=pipeline.WholePdbPipeline.Config( + buckets=buckets, max_template_date=max_template_date + ), + ) + + batches = [] + for rng_seed in fold_input.rng_seeds: + featurisation_start_time = time.time() + if verbose: + print(f'Featurising {fold_input.name} with rng_seed {rng_seed}.') + batch = data_pipeline.process_item( + fold_input=fold_input, + ccd=ccd, + random_state=np.random.RandomState(rng_seed), + random_seed=rng_seed, + ) + if verbose: + print( + f'Featurising {fold_input.name} with rng_seed {rng_seed} ' + f'took {time.time() - featurisation_start_time:.2f} seconds.' + ) + batches.append(batch) + + return batches diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa.py new file mode 100644 index 000000000..51fe21177 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa.py @@ -0,0 +1,346 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Functions for getting MSA and calculating alignment features.""" + +from collections.abc import MutableMapping, Sequence +import string +from typing import Self + +from absl import logging +from alphafold3.constants import mmcif_names +from alphafold3.data import msa_config +from alphafold3.data import msa_features +from alphafold3.data import parsers +from alphafold3.data.tools import jackhmmer +from alphafold3.data.tools import msa_tool +from alphafold3.data.tools import nhmmer +import numpy as np + + +class Error(Exception): + """Error indicatating a problem with MSA Search.""" + + +def _featurize(seq: str, chain_poly_type: str) -> str | list[int]: + if mmcif_names.is_standard_polymer_type(chain_poly_type): + featurized_seqs, _ = msa_features.extract_msa_features( + msa_sequences=[seq], chain_poly_type=chain_poly_type + ) + return featurized_seqs[0].tolist() + # For anything else simply require an identical match. + return seq + + +def sequences_are_feature_equivalent( + sequence1: str, + sequence2: str, + chain_poly_type: str, +) -> bool: + feat1 = _featurize(sequence1, chain_poly_type) + feat2 = _featurize(sequence2, chain_poly_type) + return feat1 == feat2 + + +class Msa: + """Multiple Sequence Alignment container with methods for manipulating it.""" + + def __init__( + self, + query_sequence: str, + chain_poly_type: str, + sequences: Sequence[str], + descriptions: Sequence[str], + deduplicate: bool = True, + ): + """Raw constructor, prefer using the from_{a3m,multiple_msas} class methods. + + The first sequence must be equal (in featurised form) to the query sequence. + If sequences/descriptions are empty, they will be initialised to the query. + + Args: + query_sequence: The sequence that was used to search for MSA. + chain_poly_type: Polymer type of the query sequence, see mmcif_names. + sequences: The sequences returned by the MSA search tool. + descriptions: Metadata for the sequences returned by the MSA search tool. + deduplicate: If True, the MSA sequences will be deduplicated in the input + order. Lowercase letters (insertions) are ignored when deduplicating. + """ + if len(sequences) != len(descriptions): + raise ValueError( + 'The number of sequences and descriptions must match.') + + self.query_sequence = query_sequence + self.chain_poly_type = chain_poly_type + + if not deduplicate: + self.sequences = sequences + self.descriptions = descriptions + else: + self.sequences = [] + self.descriptions = [] + # A replacement table that removes all lowercase characters. + deletion_table = str.maketrans('', '', string.ascii_lowercase) + unique_sequences = set() + for seq, desc in zip(sequences, descriptions, strict=True): + # Using string.translate is faster than re.sub('[a-z]+', ''). + sequence_no_deletions = seq.translate(deletion_table) + if sequence_no_deletions not in unique_sequences: + unique_sequences.add(sequence_no_deletions) + self.sequences.append(seq) + self.descriptions.append(desc) + + # Make sure the MSA always has at least the query. + self.sequences = self.sequences or [query_sequence] + self.descriptions = self.descriptions or ['Original query'] + + # Check if the 1st MSA sequence matches the query sequence. Since it may be + # mutated by the search tool (jackhmmer) check using the featurized version. + if not sequences_are_feature_equivalent( + self.sequences[0], query_sequence, chain_poly_type + ): + raise ValueError( + f'First MSA sequence {self.sequences[0]} is not the {query_sequence=}' + ) + + @classmethod + def from_multiple_msas( + cls, msas: Sequence[Self], deduplicate: bool = True + ) -> Self: + """Initializes the MSA from multiple MSAs. + + Args: + msas: A sequence of Msa objects representing individual MSAs produced by + different tools/dbs. + deduplicate: If True, the MSA sequences will be deduplicated in the input + order. Lowercase letters (insertions) are ignored when deduplicating. + + Returns: + An Msa object created by merging multiple MSAs. + """ + if not msas: + raise ValueError('At least one MSA must be provided.') + + query_sequence = msas[0].query_sequence + chain_poly_type = msas[0].chain_poly_type + sequences = [] + descriptions = [] + + for msa in msas: + if msa.query_sequence != query_sequence: + raise ValueError( + f'Query sequences must match: {[m.query_sequence for m in msas]}' + ) + if msa.chain_poly_type != chain_poly_type: + raise ValueError( + f'Chain poly types must match: {[m.chain_poly_type for m in msas]}' + ) + sequences.extend(msa.sequences) + descriptions.extend(msa.descriptions) + + return cls( + query_sequence=query_sequence, + chain_poly_type=chain_poly_type, + sequences=sequences, + descriptions=descriptions, + deduplicate=deduplicate, + ) + + @classmethod + def from_multiple_a3ms( + cls, a3ms: Sequence[str], chain_poly_type: str, deduplicate: bool = True + ) -> Self: + """Initializes the MSA from multiple A3M strings. + + Args: + a3ms: A sequence of A3M strings representing individual MSAs produced by + different tools/dbs. + chain_poly_type: Polymer type of the query sequence, see mmcif_names. + deduplicate: If True, the MSA sequences will be deduplicated in the input + order. Lowercase letters (insertions) are ignored when deduplicating. + + Returns: + An Msa object created by merging multiple A3Ms. + """ + if not a3ms: + raise ValueError('At least one A3M must be provided.') + + query_sequence = None + all_sequences = [] + all_descriptions = [] + + for a3m in a3ms: + sequences, descriptions = parsers.parse_fasta(a3m) + if query_sequence is None: + query_sequence = sequences[0] + + if sequences[0] != query_sequence: + raise ValueError( + f'Query sequences must match: {sequences[0]=} != {query_sequence=}' + ) + all_sequences.extend(sequences) + all_descriptions.extend(descriptions) + + return cls( + query_sequence=query_sequence, + chain_poly_type=chain_poly_type, + sequences=all_sequences, + descriptions=all_descriptions, + deduplicate=deduplicate, + ) + + @classmethod + def from_a3m( + cls, + query_sequence: str, + chain_poly_type: str, + a3m: str, + max_depth: int | None = None, + deduplicate: bool = True, + ) -> Self: + """Parses the single A3M and builds the Msa object.""" + sequences, descriptions = parsers.parse_fasta(a3m) + + if max_depth is not None and 0 < max_depth < len(sequences): + logging.info( + 'MSA cropped from depth of %d to %d for %s.', + len(sequences), + max_depth, + query_sequence, + ) + sequences = sequences[:max_depth] + descriptions = descriptions[:max_depth] + + return cls( + query_sequence=query_sequence, + chain_poly_type=chain_poly_type, + sequences=sequences, + descriptions=descriptions, + deduplicate=deduplicate, + ) + + @classmethod + def from_empty(cls, query_sequence: str, chain_poly_type: str) -> Self: + """Creates an empty Msa containing just the query sequence.""" + return cls( + query_sequence=query_sequence, + chain_poly_type=chain_poly_type, + sequences=[], + descriptions=[], + deduplicate=False, + ) + + @property + def depth(self) -> int: + return len(self.sequences) + + def __repr__(self) -> str: + return f'Msa({self.depth} sequences, {self.chain_poly_type})' + + def to_a3m(self) -> str: + """Returns the MSA in the A3M format.""" + a3m_lines = [] + for desc, seq in zip(self.descriptions, self.sequences, strict=True): + a3m_lines.append(f'>{desc}') + a3m_lines.append(seq) + return '\n'.join(a3m_lines) + '\n' + + def featurize(self) -> MutableMapping[str, np.ndarray]: + """Featurises the MSA and returns a map of feature names to features. + + Returns: + A dictionary mapping feature names to values. + + Raises: + msa.Error: + * If the sequences in the MSA don't have the same length after deletions + (lower case letters) are removed. + * If the MSA contains an unknown amino acid code. + * If there are no sequences after aligning. + """ + try: + msa, deletion_matrix = msa_features.extract_msa_features( + msa_sequences=self.sequences, chain_poly_type=self.chain_poly_type + ) + except ValueError as e: + raise Error( + f'Error extracting MSA or deletion features: {e}') from e + + if msa.shape == (0, 0): + raise Error(f'Empty MSA feature for {self}') + + species_ids = msa_features.extract_species_ids(self.descriptions) + + return { + 'msa_species_identifiers': np.array(species_ids, dtype=object), + 'num_alignments': np.array(self.depth, dtype=np.int32), + 'msa': msa, + 'deletion_matrix_int': deletion_matrix, + } + + +def get_msa_tool( + msa_tool_config: msa_config.JackhmmerConfig | msa_config.NhmmerConfig, +) -> msa_tool.MsaTool: + """Returns the requested MSA tool.""" + + match msa_tool_config: + case msa_config.JackhmmerConfig(): + return jackhmmer.Jackhmmer( + binary_path=msa_tool_config.binary_path, + database_path=msa_tool_config.database_config.path, + n_cpu=msa_tool_config.n_cpu, + n_iter=msa_tool_config.n_iter, + e_value=msa_tool_config.e_value, + z_value=msa_tool_config.z_value, + max_sequences=msa_tool_config.max_sequences, + ) + case msa_config.NhmmerConfig(): + return nhmmer.Nhmmer( + binary_path=msa_tool_config.binary_path, + hmmalign_binary_path=msa_tool_config.hmmalign_binary_path, + hmmbuild_binary_path=msa_tool_config.hmmbuild_binary_path, + database_path=msa_tool_config.database_config.path, + n_cpu=msa_tool_config.n_cpu, + e_value=msa_tool_config.e_value, + max_sequences=msa_tool_config.max_sequences, + alphabet=msa_tool_config.alphabet, + ) + case _: + raise ValueError(f'Unknown MSA tool: {msa_tool_config}.') + + +def get_msa( + target_sequence: str, + run_config: msa_config.RunConfig, + chain_poly_type: str, + deduplicate: bool = False, +) -> Msa: + """Computes the MSA for a given query sequence. + + Args: + target_sequence: The target amino-acid sequence. + run_config: MSA run configuration. + chain_poly_type: The type of chain for which to get an MSA. + deduplicate: If True, the MSA sequences will be deduplicated in the input + order. Lowercase letters (insertions) are ignored when deduplicating. + + Returns: + Aligned MSA sequences. + """ + + return Msa.from_a3m( + query_sequence=target_sequence, + chain_poly_type=chain_poly_type, + a3m=get_msa_tool(run_config.config).query(target_sequence).a3m, + max_depth=run_config.crop_size, + deduplicate=deduplicate, + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_config.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_config.py new file mode 100644 index 000000000..efa2d9b9e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_config.py @@ -0,0 +1,170 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Genetic search config settings for data pipelines.""" + +import dataclasses +import datetime +from typing import Self +from alphafold3.constants import mmcif_names + + +def _validate_chain_poly_type(chain_poly_type: str) -> None: + if chain_poly_type not in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES: + raise ValueError( + 'chain_poly_type must be one of' + f' {mmcif_names.STANDARD_POLYMER_CHAIN_TYPES}: {chain_poly_type}' + ) + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class DatabaseConfig: + """Configuration for a database.""" + + name: str + path: str + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class JackhmmerConfig: + """Configuration for a jackhmmer run. + + Attributes: + binary_path: Path to the binary of the msa tool. + database_config: Database configuration. + n_cpu: An integer with the number of CPUs to use. + n_iter: An integer with the number of database search iterations. + e_value: e-value for the database lookup. + z_value: The Z-value representing the number of comparisons done (i.e + correct database size) for E-value calculation. + max_sequences: Max sequences to return in MSA. + """ + + binary_path: str + database_config: DatabaseConfig + n_cpu: int + n_iter: int + e_value: float + z_value: float | int | None + max_sequences: int + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class NhmmerConfig: + """Configuration for a nhmmer run. + + Attributes: + binary_path: Path to the binary of the msa tool. + hmmalign_binary_path: Path to the hmmalign binary. + hmmbuild_binary_path: Path to the hmmbuild binary. + database_config: Database configuration. + n_cpu: An integer with the number of CPUs to use. + e_value: e-value for the database lookup. + max_sequences: Max sequences to return in MSA. + alphabet: The alphabet when building a profile with hmmbuild. + """ + + binary_path: str + hmmalign_binary_path: str + hmmbuild_binary_path: str + database_config: DatabaseConfig + n_cpu: int + e_value: float + max_sequences: int + alphabet: str | None + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class RunConfig: + """Configuration for an MSA run. + + Attributes: + config: MSA tool config. + chain_poly_type: The chain type for which the tools will be run. + crop_size: The maximum number of sequences to keep in the MSA. If None, all + sequences are kept. Note that the query is included in the MSA, so it + doesn't make sense to set this to less than 2. + """ + + config: JackhmmerConfig | NhmmerConfig + chain_poly_type: str + crop_size: int | None + + def __post_init__(self): + if self.crop_size is not None and self.crop_size < 2: + raise ValueError( + f'crop_size must be None or >= 2: {self.crop_size}') + + _validate_chain_poly_type(self.chain_poly_type) + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class HmmsearchConfig: + """Configuration for a hmmsearch.""" + + hmmsearch_binary_path: str + hmmbuild_binary_path: str + + e_value: float + inc_e: float + dom_e: float + incdom_e: float + alphabet: str = 'amino' + filter_f1: float | None = None + filter_f2: float | None = None + filter_f3: float | None = None + filter_max: bool = False + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class TemplateToolConfig: + """Configuration for a template tool.""" + + database_path: str + chain_poly_type: str + hmmsearch_config: HmmsearchConfig + max_a3m_query_sequences: int | None = 300 + + def __post_init__(self): + _validate_chain_poly_type(self.chain_poly_type) + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class TemplateFilterConfig: + """Configuration for a template filter.""" + + max_subsequence_ratio: float | None + min_align_ratio: float | None + min_hit_length: int | None + deduplicate_sequences: bool + max_hits: int | None + max_template_date: datetime.date + + @classmethod + def no_op_filter(cls) -> Self: + """Returns a config for filter that keeps everything.""" + return cls( + max_subsequence_ratio=None, + min_align_ratio=None, + min_hit_length=None, + deduplicate_sequences=False, + max_hits=None, + # Very far in the future. + max_template_date=datetime.date(3000, 1, 1), + ) + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class TemplatesConfig: + """Configuration for the template search pipeline.""" + + template_tool_config: TemplateToolConfig + filter_config: TemplateFilterConfig diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_features.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_features.py new file mode 100644 index 000000000..7c6fff3f5 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_features.py @@ -0,0 +1,204 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Utilities for computing MSA features.""" + +from collections.abc import Sequence +import re +from alphafold3.constants import mmcif_names +import numpy as np + +_PROTEIN_TO_ID = { + 'A': 0, + 'B': 3, # Same as D. + 'C': 4, + 'D': 3, + 'E': 6, + 'F': 13, + 'G': 7, + 'H': 8, + 'I': 9, + 'J': 20, # Same as unknown (X). + 'K': 11, + 'L': 10, + 'M': 12, + 'N': 2, + 'O': 20, # Same as unknown (X). + 'P': 14, + 'Q': 5, + 'R': 1, + 'S': 15, + 'T': 16, + 'U': 4, # Same as C. + 'V': 19, + 'W': 17, + 'X': 20, + 'Y': 18, + 'Z': 6, # Same as E. + '-': 21, +} + +_RNA_TO_ID = { + # Map non-standard residues to UNK_NUCLEIC (N) -> 30 + **{chr(i): 30 for i in range(ord('A'), ord('Z') + 1)}, + # Continue the RNA indices from where Protein indices left off. + '-': 21, + 'A': 22, + 'G': 23, + 'C': 24, + 'U': 25, +} + +_DNA_TO_ID = { + # Map non-standard residues to UNK_NUCLEIC (N) -> 30 + **{chr(i): 30 for i in range(ord('A'), ord('Z') + 1)}, + # Continue the DNA indices from where DNA indices left off. + '-': 21, + 'A': 26, + 'G': 27, + 'C': 28, + 'T': 29, +} + + +def extract_msa_features( + msa_sequences: Sequence[str], chain_poly_type: str +) -> tuple[np.ndarray, np.ndarray]: + """Extracts MSA features. + + Example: + The input raw MSA is: `[["AAAAAA"], ["Ai-CiDiiiEFa"]]` + The output MSA will be: `[["AAAAAA"], ["A-CDEF"]]` + The deletions will be: `[[0, 0, 0, 0, 0, 0], [0, 1, 0, 1, 3, 0]]` + + Args: + msa_sequences: A list of strings, each string with one MSA sequence. Each + string must have the same, constant number of non-lowercase (matching) + residues. + chain_poly_type: Either 'polypeptide(L)' (protein), 'polyribonucleotide' + (RNA), or 'polydeoxyribonucleotide' (DNA). Use the appropriate string + constant from mmcif_names.py. + + Returns: + A tuple with: + * MSA array of shape (num_seq, num_res) that contains only the uppercase + characters or gaps (-) from the original MSA. + * Deletions array of shape (num_seq, num_res) that contains the number + of deletions (lowercase letters in the MSA) to the left from each + non-deleted residue (uppercase letters in the MSA). + + Raises: + ValueError if any of the preconditions are not met. + """ + + # Select the appropriate character map based on the chain type. + if chain_poly_type == mmcif_names.RNA_CHAIN: + char_map = _RNA_TO_ID + elif chain_poly_type == mmcif_names.DNA_CHAIN: + char_map = _DNA_TO_ID + elif chain_poly_type == mmcif_names.PROTEIN_CHAIN: + char_map = _PROTEIN_TO_ID + else: + raise ValueError(f'{chain_poly_type=} invalid.') + + # Handle empty MSA. + if not msa_sequences: + empty_msa = np.array([], dtype=np.int32).reshape((0, 0)) + empty_deletions = np.array([], dtype=np.int32).reshape((0, 0)) + return empty_msa, empty_deletions + + # Get the number of rows and columns in the MSA. + num_rows = len(msa_sequences) + num_cols = sum(1 for c in msa_sequences[0] if c in char_map) + + # Initialize the output arrays. + msa_arr = np.zeros((num_rows, num_cols), dtype=np.int32) + deletions_arr = np.zeros((num_rows, num_cols), dtype=np.int32) + + # Populate the output arrays. + for problem_row, msa_sequence in enumerate(msa_sequences): + deletion_count = 0 + upper_count = 0 + problem_col = 0 + problems = [] + for current in msa_sequence: + msa_id = char_map.get(current, -1) + if msa_id == -1: + if not current.islower(): + problems.append( + f'({problem_row}, {problem_col}):{current}') + deletion_count += 1 + else: + # Check the access is safe before writing to the array. + # We don't need to check problem_row since it's guaranteed to be within + # the array bounds, while upper_count is incremented in the loop. + if upper_count < deletions_arr.shape[1]: + deletions_arr[problem_row, upper_count] = deletion_count + msa_arr[problem_row, upper_count] = msa_id + deletion_count = 0 + upper_count += 1 + problem_col += 1 + if problems: + raise ValueError( + f"Unknown residues in MSA: {', '.join(problems)}. " + f'target_sequence: {msa_sequences[0]}' + ) + if upper_count != num_cols: + raise ValueError( + 'Invalid shape all strings must have the same number ' + 'of non-lowercase characters; First string has ' + f"{num_cols} non-lowercase characters but '{msa_sequence}' has " + f'{upper_count}. target_sequence: {msa_sequences[0]}' + ) + + return msa_arr, deletions_arr + + +# UniProtKB SwissProt/TrEMBL dbs have the following description format: +# `db|UniqueIdentifier|EntryName`, e.g. `sp|P0C2L1|A3X1_LOXLA` or +# `tr|A0A146SKV9|A0A146SKV9_FUNHE`. +_UNIPROT_ENTRY_NAME_REGEX = re.compile( + # UniProtKB TrEMBL or SwissProt database. + r'(?:tr|sp)\|' + # A primary accession number of the UniProtKB entry. + r'(?:[A-Z0-9]{6,10})' + # Occasionally there is an isoform suffix (e.g. _1 or _10) which we ignore. + r'(?:_\d+)?\|' + # TrEMBL: Same as AccessionId (6-10 characters). + # SwissProt: A mnemonic protein identification code (1-5 characters). + r'(?:[A-Z0-9]{1,10}_)' + # A mnemonic species identification code. + r'(?P[A-Z0-9]{1,5})' +) + + +def extract_species_ids(msa_descriptions: Sequence[str]) -> Sequence[str]: + """Extracts species ID from MSA UniProtKB sequence identifiers. + + Args: + msa_descriptions: The descriptions (the FASTA/A3M comment line) for each of + the sequences. + + Returns: + Extracted UniProtKB species IDs if there is a regex match for each + description line, blank if the regex doesn't match. + """ + species_ids = [] + for msa_description in msa_descriptions: + msa_description = msa_description.strip() + match = _UNIPROT_ENTRY_NAME_REGEX.match(msa_description) + if match: + species_ids.append(match.group('SpeciesId')) + else: + # Handle cases where the regex doesn't match + # (e.g., append None or raise an error depending on your needs) + species_ids.append('') + return species_ids diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_identifiers.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_identifiers.py new file mode 100644 index 000000000..0296080bb --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_identifiers.py @@ -0,0 +1,86 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Utilities for extracting identifiers from MSA sequence descriptions.""" + +import dataclasses +import re + + +# Sequences coming from UniProtKB database come in the +# `db|UniqueIdentifier|EntryName` format, e.g. `tr|A0A146SKV9|A0A146SKV9_FUNHE` +# or `sp|P0C2L1|A3X1_LOXLA` (for TREMBL/Swiss-Prot respectively). +_UNIPROT_PATTERN = re.compile( + r""" + ^ + # UniProtKB/TrEMBL or UniProtKB/Swiss-Prot + (?:tr|sp) + \| + # A primary accession number of the UniProtKB entry. + (?P[A-Za-z0-9]{6,10}) + # Occasionally there is a _0 or _1 isoform suffix, which we ignore. + (?:_\d)? + \| + # TREMBL repeats the accession ID here. Swiss-Prot has a mnemonic + # protein ID code. + (?:[A-Za-z0-9]+) + _ + # A mnemonic species identification code. + (?P([A-Za-z0-9]){1,5}) + # Small BFD uses a final value after an underscore, which we ignore. + (?:_\d+)? + $ + """, + re.VERBOSE, +) + + +@dataclasses.dataclass(frozen=True) +class Identifiers: + species_id: str = '' + + +def _parse_sequence_identifier(msa_sequence_identifier: str) -> Identifiers: + """Gets species from an msa sequence identifier. + + The sequence identifier has the format specified by + _UNIPROT_TREMBL_ENTRY_NAME_PATTERN or _UNIPROT_SWISSPROT_ENTRY_NAME_PATTERN. + An example of a sequence identifier: `tr|A0A146SKV9|A0A146SKV9_FUNHE` + + Args: + msa_sequence_identifier: a sequence identifier. + + Returns: + An `Identifiers` instance with species_id. These + can be empty in the case where no identifier was found. + """ + matches = re.search(_UNIPROT_PATTERN, msa_sequence_identifier.strip()) + if matches: + return Identifiers(species_id=matches.group('SpeciesIdentifier')) + return Identifiers() + + +def _extract_sequence_identifier(description: str) -> str | None: + """Extracts sequence identifier from description. Returns None if no match.""" + split_description = description.split() + if split_description: + return split_description[0].partition('/')[0] + else: + return None + + +def get_identifiers(description: str) -> Identifiers: + """Computes extra MSA features from the description.""" + sequence_identifier = _extract_sequence_identifier(description) + if sequence_identifier is None: + return Identifiers() + else: + return _parse_sequence_identifier(sequence_identifier) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_store.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_store.py new file mode 100644 index 000000000..cda58e4c9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_store.py @@ -0,0 +1,67 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Interface and implementations for fetching MSA data.""" + +from collections.abc import Sequence +from typing_extensions import Protocol, TypeAlias + +from alphafold3.data import msa +from alphafold3.data import msa_config + + +MsaErrors: TypeAlias = Sequence[tuple[msa_config.RunConfig, str]] + + +class MsaProvider(Protocol): + """Interface for providing Multiple Sequence Alignments.""" + + def __call__( + self, + query_sequence: str, + chain_polymer_type: str, + ) -> tuple[msa.Msa, MsaErrors]: + """Retrieve MSA for the given polymer query_sequence. + + Args: + query_sequence: The residue sequence of the polymer to search for. + chain_polymer_type: The polymer type of the query_sequence. This must + match the chain_polymer_type of the provider. + + Returns: + A tuple containing the MSA and MsaErrors. MsaErrors is a Sequence + containing a tuple for each msa_query that failed. Each tuple contains + the failing query and the associated error message. + """ + + +class EmptyMsaProvider: + """MSA provider that returns just the query sequence, useful for testing.""" + + def __init__(self, chain_polymer_type: str): + self._chain_polymer_type = chain_polymer_type + + def __call__( + self, query_sequence: str, chain_polymer_type: str + ) -> tuple[msa.Msa, MsaErrors]: + """Returns an MSA containing just the query sequence, never errors.""" + if chain_polymer_type != self._chain_polymer_type: + raise ValueError( + f'EmptyMsaProvider of type {self._chain_polymer_type} called with ' + f'sequence of {chain_polymer_type=}, {query_sequence=}.' + ) + return ( + msa.Msa.from_empty( + query_sequence=query_sequence, + chain_poly_type=self._chain_polymer_type, + ), + (), + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/parsers.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/parsers.py new file mode 100644 index 000000000..608c9aaf0 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/parsers.py @@ -0,0 +1,181 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ + +"""Functions for parsing various file formats.""" + +from collections.abc import Iterable, Sequence +from typing import IO, TypeAlias + +from alphafold3.cpp import fasta_iterator +from alphafold3.cpp import msa_conversion + + +DeletionMatrix: TypeAlias = Sequence[Sequence[int]] + + +def lazy_parse_fasta_string(fasta_string: str) -> Iterable[tuple[str, str]]: + """Lazily parses a FASTA/A3M string and yields (sequence, description) tuples. + + This implementation is more memory friendly than `fasta_sequence` while + offering comparable performance. The underlying implementation is in C++ and + is therefore faster than a pure Python implementation. + + Use this method when parsing FASTA files where you already have the FASTA + string, but need to control how far you iterate through its sequences. + + Arguments: + fasta_string: A string with the contents of FASTA/A3M file. + + Returns: + Iterator of (sequence, description). In the description, the leading ">" is + stripped. + + Raises: + ValueError if the FASTA/A3M file is invalid, e.g. empty. + """ + + # The lifetime of the FastaStringIterator is tied to the lifetime of + # fasta_string - fasta_string must be kept while the iterator is in use. + return fasta_iterator.FastaStringIterator(fasta_string) + + +def parse_fasta(fasta_string: str) -> tuple[Sequence[str], Sequence[str]]: + """Parses FASTA string and returns list of strings with amino-acid sequences. + + Arguments: + fasta_string: The string contents of a FASTA file. + + Returns: + A tuple of two lists: + * A list of sequences. + * A list of sequence descriptions taken from the comment lines. In the + same order as the sequences. + """ + return fasta_iterator.parse_fasta_include_descriptions(fasta_string) + + +def convert_a3m_to_stockholm(a3m: str, max_seqs: int | None = None) -> str: + """Converts MSA in the A3M format to the Stockholm format.""" + sequences, descriptions = parse_fasta(a3m) + if max_seqs is not None: + sequences = sequences[:max_seqs] + descriptions = descriptions[:max_seqs] + + stockholm = ['# STOCKHOLM 1.0', ''] + + # Add the Stockholm header with the sequence metadata. + names = [] + for i, description in enumerate(descriptions): + name, _, rest = description.partition(' ') + # Ensure that the names are unique - stockholm format requires that + # the sequence names are unique. + name = f'{name}_{i}' + names.append(name) + # Avoid zero-length description due to historic hmmbuild parsing bug. + desc = rest.strip() or '' + stockholm.append(f'#=GS {name.strip()} DE {desc}') + stockholm.append('') + + # Convert insertions in a sequence into gaps in all other sequences that don't + # have an insertion in that column as well. + sequences = msa_conversion.convert_a3m_to_stockholm(sequences) + + # Add the MSA data. + max_name_width = max(len(name) for name in names) + for name, sequence in zip(names, sequences, strict=True): + # Align the names to the left and pad with spaces to the maximum length. + stockholm.append(f'{name:<{max_name_width}s} {sequence}') + + # Add the reference annotation for the query (the first sequence). + ref_annotation = ''.join('.' if c == '-' else 'x' for c in sequences[0]) + stockholm.append(f'{"#=GC RF":<{max_name_width}s} {ref_annotation}') + stockholm.append('//') + + return '\n'.join(stockholm) + + +def convert_stockholm_to_a3m( + stockholm: IO[str], + max_sequences: int | None = None, + remove_first_row_gaps: bool = True, + linewidth: int | None = None, +) -> str: + """Converts MSA in Stockholm format to the A3M format.""" + descriptions = {} + sequences = {} + reached_max_sequences = False + + if linewidth is not None and linewidth <= 0: + raise ValueError('linewidth must be > 0 or None') + + for line in stockholm: + reached_max_sequences = max_sequences and len( + sequences) >= max_sequences + line = line.strip() + # Ignore blank lines, markup and end symbols - remainder are alignment + # sequence parts. + if not line or line.startswith(('#', '//')): + continue + seqname, aligned_seq = line.split(maxsplit=1) + if seqname not in sequences: + if reached_max_sequences: + continue + sequences[seqname] = '' + sequences[seqname] += aligned_seq + + stockholm.seek(0) + for line in stockholm: + line = line.strip() + if line[:4] == '#=GS': + # Description row - example format is: + # #=GS UniRef90_Q9H5Z4/4-78 DE [subseq from] cDNA: FLJ22755 ... + columns = line.split(maxsplit=3) + seqname, feature = columns[1:3] + value = columns[3] if len(columns) == 4 else '' + if feature != 'DE': + continue + if reached_max_sequences and seqname not in sequences: + continue + descriptions[seqname] = value + if len(descriptions) == len(sequences): + break + + assert len(descriptions) <= len(sequences) + + # Convert sto format to a3m line by line + a3m_sequences = {} + # query_sequence is assumed to be the first sequence + query_sequence = next(iter(sequences.values())) + for seqname, sto_sequence in sequences.items(): + if remove_first_row_gaps: + a3m_sequences[seqname] = msa_conversion.align_sequence_to_gapless_query( + sequence=sto_sequence, query_sequence=query_sequence + ).replace('.', '') + else: + a3m_sequences[seqname] = sto_sequence.replace('.', '') + + fasta_chunks = [] + + for seqname, seq in a3m_sequences.items(): + fasta_chunks.append(f'>{seqname} {descriptions.get(seqname, "")}') + + if linewidth: + fasta_chunks.extend( + seq[i: linewidth + i] for i in range(0, len(seq), linewidth) + ) + else: + fasta_chunks.append(seq) + + return '\n'.join(fasta_chunks) + '\n' # Include terminating newline. diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py new file mode 100644 index 000000000..89ae3dff3 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py @@ -0,0 +1,543 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Functions for running the MSA and template tools for the AlphaFold model.""" + +from concurrent import futures +import dataclasses +import datetime +import functools +import logging +import time + +from alphafold3.common import folding_input +from alphafold3.constants import mmcif_names +from alphafold3.data import msa +from alphafold3.data import msa_config +from alphafold3.data import structure_stores +from alphafold3.data import templates as templates_lib + + +# Cache to avoid re-running template search for the same sequence in homomers. +@functools.cache +def _get_protein_templates( + sequence: str, + input_msa_a3m: str, + run_template_search: bool, + templates_config: msa_config.TemplatesConfig, + pdb_database_path: str, +) -> templates_lib.Templates: + """Searches for templates for a single protein chain.""" + if run_template_search: + templates_start_time = time.time() + logging.info('Getting protein templates for sequence %s', sequence) + protein_templates = templates_lib.Templates.from_seq_and_a3m( + query_sequence=sequence, + msa_a3m=input_msa_a3m, + max_template_date=templates_config.filter_config.max_template_date, + database_path=templates_config.template_tool_config.database_path, + hmmsearch_config=templates_config.template_tool_config.hmmsearch_config, + max_a3m_query_sequences=None, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + structure_store=structure_stores.StructureStore(pdb_database_path), + filter_config=templates_config.filter_config, + ) + logging.info( + 'Getting protein templates took %.2f seconds for sequence %s', + time.time() - templates_start_time, + sequence, + ) + else: + logging.info('Skipping template search for sequence %s', sequence) + protein_templates = templates_lib.Templates( + query_sequence=sequence, + hits=[], + max_template_date=templates_config.filter_config.max_template_date, + structure_store=structure_stores.StructureStore(pdb_database_path), + ) + return protein_templates + + +# Cache to avoid re-running the MSA tools for the same sequence in homomers. +@functools.cache +def _get_protein_msa_and_templates( + sequence: str, + run_template_search: bool, + uniref90_msa_config: msa_config.RunConfig, + mgnify_msa_config: msa_config.RunConfig, + small_bfd_msa_config: msa_config.RunConfig, + uniprot_msa_config: msa_config.RunConfig, + templates_config: msa_config.TemplatesConfig, + pdb_database_path: str, +) -> tuple[msa.Msa, msa.Msa, templates_lib.Templates]: + """Processes a single protein chain.""" + logging.info('Getting protein MSAs for sequence %s', sequence) + msa_start_time = time.time() + # Run various MSA tools in parallel. Use a ThreadPoolExecutor because + # they're not blocked by the GIL, as they're sub-shelled out. + with futures.ThreadPoolExecutor(max_workers=4) as executor: + uniref90_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=uniref90_msa_config, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + mgnify_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=mgnify_msa_config, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + small_bfd_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=small_bfd_msa_config, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + uniprot_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=uniprot_msa_config, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + uniref90_msa = uniref90_msa_future.result() + mgnify_msa = mgnify_msa_future.result() + small_bfd_msa = small_bfd_msa_future.result() + uniprot_msa = uniprot_msa_future.result() + logging.info( + 'Getting protein MSAs took %.2f seconds for sequence %s', + time.time() - msa_start_time, + sequence, + ) + + logging.info('Deduplicating MSAs for sequence %s', sequence) + msa_dedupe_start_time = time.time() + with futures.ThreadPoolExecutor() as executor: + unpaired_protein_msa_future = executor.submit( + msa.Msa.from_multiple_msas, + msas=[uniref90_msa, small_bfd_msa, mgnify_msa], + deduplicate=True, + ) + paired_protein_msa_future = executor.submit( + msa.Msa.from_multiple_msas, msas=[uniprot_msa], deduplicate=False + ) + unpaired_protein_msa = unpaired_protein_msa_future.result() + paired_protein_msa = paired_protein_msa_future.result() + logging.info( + 'Deduplicating MSAs took %.2f seconds for sequence %s', + time.time() - msa_dedupe_start_time, + sequence, + ) + + protein_templates = _get_protein_templates( + sequence=sequence, + input_msa_a3m=unpaired_protein_msa.to_a3m(), + run_template_search=run_template_search, + templates_config=templates_config, + pdb_database_path=pdb_database_path, + ) + + return unpaired_protein_msa, paired_protein_msa, protein_templates + + +# Cache to avoid re-running the Nhmmer for the same sequence in homomers. +@functools.cache +def _get_rna_msa( + sequence: str, + nt_rna_msa_config: msa_config.NhmmerConfig, + rfam_msa_config: msa_config.NhmmerConfig, + rnacentral_msa_config: msa_config.NhmmerConfig, +) -> msa.Msa: + """Processes a single RNA chain.""" + logging.info('Getting RNA MSAs for sequence %s', sequence) + rna_msa_start_time = time.time() + # Run various MSA tools in parallel. Use a ThreadPoolExecutor because + # they're not blocked by the GIL, as they're sub-shelled out. + with futures.ThreadPoolExecutor() as executor: + nt_rna_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=nt_rna_msa_config, + chain_poly_type=mmcif_names.RNA_CHAIN, + ) + rfam_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=rfam_msa_config, + chain_poly_type=mmcif_names.RNA_CHAIN, + ) + rnacentral_msa_future = executor.submit( + msa.get_msa, + target_sequence=sequence, + run_config=rnacentral_msa_config, + chain_poly_type=mmcif_names.RNA_CHAIN, + ) + nt_rna_msa = nt_rna_msa_future.result() + rfam_msa = rfam_msa_future.result() + rnacentral_msa = rnacentral_msa_future.result() + logging.info( + 'Getting RNA MSAs took %.2f seconds for sequence %s', + time.time() - rna_msa_start_time, + sequence, + ) + + return msa.Msa.from_multiple_msas( + msas=[rfam_msa, rnacentral_msa, nt_rna_msa], + deduplicate=True, + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class DataPipelineConfig: + """The configuration for the data pipeline. + + Attributes: + jackhmmer_binary_path: Jackhmmer binary path, used for protein MSA search. + nhmmer_binary_path: Nhmmer binary path, used for RNA MSA search. + hmmalign_binary_path: Hmmalign binary path, used to align hits to the query + profile. + hmmsearch_binary_path: Hmmsearch binary path, used for template search. + hmmbuild_binary_path: Hmmbuild binary path, used to build HMM profile from + raw MSA in template search. + small_bfd_database_path: Small BFD database path, used for protein MSA + search. + mgnify_database_path: Mgnify database path, used for protein MSA search. + uniprot_cluster_annot_database_path: Uniprot database path, used for protein + paired MSA search. + uniref90_database_path: UniRef90 database path, used for MSA search, and the + MSA obtained by searching it is used to construct the profile for template + search. + ntrna_database_path: NT-RNA database path, used for RNA MSA search. + rfam_database_path: Rfam database path, used for RNA MSA search. + rna_central_database_path: RNAcentral database path, used for RNA MSA + search. + seqres_database_path: PDB sequence database path, used for template search. + pdb_database_path: PDB database directory with mmCIF files path, used for + template search. + jackhmmer_n_cpu: Number of CPUs to use for Jackhmmer. + nhmmer_n_cpu: Number of CPUs to use for Nhmmer. + max_template_date: The latest date of templates to use. + """ + + # Binary paths. + jackhmmer_binary_path: str + nhmmer_binary_path: str + hmmalign_binary_path: str + hmmsearch_binary_path: str + hmmbuild_binary_path: str + + # Jackhmmer databases. + small_bfd_database_path: str + mgnify_database_path: str + uniprot_cluster_annot_database_path: str + uniref90_database_path: str + # Nhmmer databases. + ntrna_database_path: str + rfam_database_path: str + rna_central_database_path: str + # Template search databases. + seqres_database_path: str + pdb_database_path: str + + # Optional configuration for MSA tools. + jackhmmer_n_cpu: int = 8 + nhmmer_n_cpu: int = 8 + + max_template_date: datetime.date + + +class DataPipeline: + """Runs the alignment tools and assembles the input features.""" + + def __init__(self, data_pipeline_config: DataPipelineConfig): + """Initializes the data pipeline with default configurations.""" + self._uniref90_msa_config = msa_config.RunConfig( + config=msa_config.JackhmmerConfig( + binary_path=data_pipeline_config.jackhmmer_binary_path, + database_config=msa_config.DatabaseConfig( + name='uniref90', + path=data_pipeline_config.uniref90_database_path, + ), + n_cpu=data_pipeline_config.jackhmmer_n_cpu, + n_iter=1, + e_value=1e-4, + z_value=None, + max_sequences=10_000, + ), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + crop_size=None, + ) + self._mgnify_msa_config = msa_config.RunConfig( + config=msa_config.JackhmmerConfig( + binary_path=data_pipeline_config.jackhmmer_binary_path, + database_config=msa_config.DatabaseConfig( + name='mgnify', + path=data_pipeline_config.mgnify_database_path, + ), + n_cpu=data_pipeline_config.jackhmmer_n_cpu, + n_iter=1, + e_value=1e-4, + z_value=None, + max_sequences=5_000, + ), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + crop_size=None, + ) + self._small_bfd_msa_config = msa_config.RunConfig( + config=msa_config.JackhmmerConfig( + binary_path=data_pipeline_config.jackhmmer_binary_path, + database_config=msa_config.DatabaseConfig( + name='small_bfd', + path=data_pipeline_config.small_bfd_database_path, + ), + n_cpu=data_pipeline_config.jackhmmer_n_cpu, + n_iter=1, + e_value=1e-4, + # Set z_value=138_515_945 to match the z_value used in the paper. + # In practice, this has minimal impact on predicted structures. + z_value=None, + max_sequences=5_000, + ), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + crop_size=None, + ) + self._uniprot_msa_config = msa_config.RunConfig( + config=msa_config.JackhmmerConfig( + binary_path=data_pipeline_config.jackhmmer_binary_path, + database_config=msa_config.DatabaseConfig( + name='uniprot_cluster_annot', + path=data_pipeline_config.uniprot_cluster_annot_database_path, + ), + n_cpu=data_pipeline_config.jackhmmer_n_cpu, + n_iter=1, + e_value=1e-4, + z_value=None, + max_sequences=50_000, + ), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + crop_size=None, + ) + self._nt_rna_msa_config = msa_config.RunConfig( + config=msa_config.NhmmerConfig( + binary_path=data_pipeline_config.nhmmer_binary_path, + hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, + hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, + database_config=msa_config.DatabaseConfig( + name='nt_rna', + path=data_pipeline_config.ntrna_database_path, + ), + n_cpu=data_pipeline_config.nhmmer_n_cpu, + e_value=1e-3, + alphabet='rna', + max_sequences=10_000, + ), + chain_poly_type=mmcif_names.RNA_CHAIN, + crop_size=None, + ) + self._rfam_msa_config = msa_config.RunConfig( + config=msa_config.NhmmerConfig( + binary_path=data_pipeline_config.nhmmer_binary_path, + hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, + hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, + database_config=msa_config.DatabaseConfig( + name='rfam_rna', + path=data_pipeline_config.rfam_database_path, + ), + n_cpu=data_pipeline_config.nhmmer_n_cpu, + e_value=1e-3, + alphabet='rna', + max_sequences=10_000, + ), + chain_poly_type=mmcif_names.RNA_CHAIN, + crop_size=None, + ) + self._rnacentral_msa_config = msa_config.RunConfig( + config=msa_config.NhmmerConfig( + binary_path=data_pipeline_config.nhmmer_binary_path, + hmmalign_binary_path=data_pipeline_config.hmmalign_binary_path, + hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, + database_config=msa_config.DatabaseConfig( + name='rna_central_rna', + path=data_pipeline_config.rna_central_database_path, + ), + n_cpu=data_pipeline_config.nhmmer_n_cpu, + e_value=1e-3, + alphabet='rna', + max_sequences=10_000, + ), + chain_poly_type=mmcif_names.RNA_CHAIN, + crop_size=None, + ) + + self._templates_config = msa_config.TemplatesConfig( + template_tool_config=msa_config.TemplateToolConfig( + database_path=data_pipeline_config.seqres_database_path, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + hmmsearch_config=msa_config.HmmsearchConfig( + hmmsearch_binary_path=data_pipeline_config.hmmsearch_binary_path, + hmmbuild_binary_path=data_pipeline_config.hmmbuild_binary_path, + filter_f1=0.1, + filter_f2=0.1, + filter_f3=0.1, + e_value=100, + inc_e=100, + dom_e=100, + incdom_e=100, + alphabet='amino', + ), + ), + filter_config=msa_config.TemplateFilterConfig( + max_subsequence_ratio=0.95, + min_align_ratio=0.1, + min_hit_length=10, + deduplicate_sequences=True, + max_hits=4, + max_template_date=data_pipeline_config.max_template_date, + ), + ) + self._pdb_database_path = data_pipeline_config.pdb_database_path + + def process_protein_chain( + self, chain: folding_input.ProteinChain + ) -> folding_input.ProteinChain: + """Processes a single protein chain.""" + has_unpaired_msa = chain.unpaired_msa is not None + has_paired_msa = chain.paired_msa is not None + has_templates = chain.templates is not None + + if not has_unpaired_msa and not has_paired_msa and not chain.templates: + # MSA None - search. Templates either [] - don't search, or None - search. + unpaired_msa, paired_msa, template_hits = _get_protein_msa_and_templates( + sequence=chain.sequence, + # Skip template search if []. + run_template_search=not has_templates, + uniref90_msa_config=self._uniref90_msa_config, + mgnify_msa_config=self._mgnify_msa_config, + small_bfd_msa_config=self._small_bfd_msa_config, + uniprot_msa_config=self._uniprot_msa_config, + templates_config=self._templates_config, + pdb_database_path=self._pdb_database_path, + ) + unpaired_msa = unpaired_msa.to_a3m() + paired_msa = paired_msa.to_a3m() + templates = [ + folding_input.Template( + mmcif=struct.to_mmcif(), + query_to_template_map=hit.query_to_hit_mapping, + ) + for hit, struct in template_hits.get_hits_with_structures() + ] + elif has_unpaired_msa and has_paired_msa and not has_templates: + # Has MSA, but doesn't have templates. Search for templates only. + empty_msa = msa.Msa.from_empty( + query_sequence=chain.sequence, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ).to_a3m() + unpaired_msa = chain.unpaired_msa or empty_msa + paired_msa = chain.paired_msa or empty_msa + template_hits = _get_protein_templates( + sequence=chain.sequence, + input_msa_a3m=unpaired_msa, + run_template_search=True, + templates_config=self._templates_config, + pdb_database_path=self._pdb_database_path, + ) + templates = [ + folding_input.Template( + mmcif=struct.to_mmcif(), + query_to_template_map=hit.query_to_hit_mapping, + ) + for hit, struct in template_hits.get_hits_with_structures() + ] + else: + # Has MSA and templates, don't search for anything. + if not has_unpaired_msa or not has_paired_msa or not has_templates: + raise ValueError( + f'Protein chain {chain.id} has unpaired MSA, paired MSA, or' + ' templates set only partially. If you want to run the pipeline' + ' with custom MSA/templates, you need to set all of them. You can' + ' set MSA to empty string and templates to empty list to signify' + ' that they should not be used and searched for.' + ) + logging.info( + 'Skipping MSA and template search for protein chain %s because it ' + 'already has MSAs and templates.', + chain.id, + ) + if not chain.unpaired_msa: + logging.info( + 'Using empty unpaired MSA for protein chain %s', chain.id) + if not chain.paired_msa: + logging.info( + 'Using empty paired MSA for protein chain %s', chain.id) + if not chain.templates: + logging.info( + 'Using no templates for protein chain %s', chain.id) + empty_msa = msa.Msa.from_empty( + query_sequence=chain.sequence, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ).to_a3m() + unpaired_msa = chain.unpaired_msa or empty_msa + paired_msa = chain.paired_msa or empty_msa + templates = chain.templates + + return dataclasses.replace( + chain, + unpaired_msa=unpaired_msa, + paired_msa=paired_msa, + templates=templates, + ) + + def process_rna_chain( + self, chain: folding_input.RnaChain + ) -> folding_input.RnaChain: + """Processes a single RNA chain.""" + if chain.unpaired_msa is not None: + # Don't run MSA tools if the chain already has an MSA. + logging.info( + 'Skipping MSA search for RNA chain %s because it already has MSA.', + chain.id, + ) + if not chain.unpaired_msa: + logging.info( + 'Using empty unpaired MSA for RNA chain %s', chain.id) + empty_msa = msa.Msa.from_empty( + query_sequence=chain.sequence, chain_poly_type=mmcif_names.RNA_CHAIN + ).to_a3m() + unpaired_msa = chain.unpaired_msa or empty_msa + else: + unpaired_msa = _get_rna_msa( + sequence=chain.sequence, + nt_rna_msa_config=self._nt_rna_msa_config, + rfam_msa_config=self._rfam_msa_config, + rnacentral_msa_config=self._rnacentral_msa_config, + ).to_a3m() + return dataclasses.replace(chain, unpaired_msa=unpaired_msa) + + def process(self, fold_input: folding_input.Input) -> folding_input.Input: + """Runs MSA and template tools and returns a new Input with the results.""" + processed_chains = [] + for chain in fold_input.chains: + print(f'Processing chain {chain.id}') + process_chain_start_time = time.time() + match chain: + case folding_input.ProteinChain(): + processed_chains.append(self.process_protein_chain(chain)) + case folding_input.RnaChain(): + processed_chains.append(self.process_rna_chain(chain)) + case _: + processed_chains.append(chain) + print( + f'Processing chain {chain.id} took' + f' {time.time() - process_chain_start_time:.2f} seconds', + ) + + return dataclasses.replace(fold_input, chains=processed_chains) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/structure_stores.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/structure_stores.py new file mode 100644 index 000000000..afaa10d32 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/structure_stores.py @@ -0,0 +1,102 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Library for loading structure data from various sources.""" + +from collections.abc import Mapping, Sequence +import functools +import os +import pathlib +import tarfile + + +class NotFoundError(KeyError): + """Raised when the structure store doesn't contain the requested target.""" + + +class StructureStore: + """Handles the retrieval of mmCIF files from a filesystem.""" + + def __init__( + self, + structures: str | os.PathLike[str] | Mapping[str, str], + ): + """Initialises the instance. + + Args: + structures: Path of the directory where the mmCIF files are or a Mapping + from target name to mmCIF string. + """ + if isinstance(structures, Mapping): + self._structure_mapping = structures + self._structure_path = None + self._structure_tar = None + else: + self._structure_mapping = None + path_str = os.fspath(structures) + if path_str.endswith('.tar'): + self._structure_tar = tarfile.open(path_str, 'r') + self._structure_path = None + else: + self._structure_path = pathlib.Path(structures) + self._structure_tar = None + + @functools.cached_property + def _tar_members(self) -> Mapping[str, tarfile.TarInfo]: + assert self._structure_tar is not None + return { + path.stem: tarinfo + for tarinfo in self._structure_tar.getmembers() + if tarinfo.isfile() + and (path := pathlib.Path(tarinfo.path.lower())).suffix == '.cif' + } + + def get_mmcif_str(self, target_name: str) -> str: + """Returns an mmCIF for a given `target_name`. + + Args: + target_name: Name specifying the target mmCIF. + + Raises: + NotFoundError: If the target is not found. + """ + if self._structure_mapping is not None: + try: + return self._structure_mapping[target_name] + except KeyError as e: + raise NotFoundError(f'{target_name=} not found') from e + + if self._structure_tar is not None: + try: + member = self._tar_members[target_name] + if struct_file := self._structure_tar.extractfile(member): + return struct_file.read().decode() + else: + raise NotFoundError(f'{target_name=} not found') + except KeyError: + raise NotFoundError(f'{target_name=} not found') from None + + filepath = self._structure_path / f'{target_name}.cif' + try: + return filepath.read_text() + except FileNotFoundError as e: + raise NotFoundError( + f'{target_name=} not found at {filepath=}') from e + + def target_names(self) -> Sequence[str]: + """Returns all targets in the store.""" + if self._structure_mapping is not None: + return [*self._structure_mapping.keys()] + elif self._structure_tar is not None: + return sorted(self._tar_members.keys()) + elif self._structure_path is not None: + return sorted([path.stem for path in self._structure_path.glob('*.cif')]) + return () diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_realign.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_realign.py new file mode 100644 index 000000000..6b4d0215d --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_realign.py @@ -0,0 +1,170 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Realign sequences found in PDB seqres to the actual CIF sequences.""" + +from collections.abc import Mapping + + +class AlignmentError(Exception): + """Failed alignment between the hit sequence and the actual mmCIF sequence.""" + + +def realign_hit_to_structure( + *, + hit_sequence: str, + hit_start_index: int, + hit_end_index: int, + full_length: int, + structure_sequence: str, + query_to_hit_mapping: Mapping[int, int], +) -> Mapping[int, int]: + """Realigns the hit sequence to the Structure sequence. + + For example, for the given input: + query_sequence : ABCDEFGHIJKL + hit_sequence : ---DEFGHIJK- + struc_sequence : XDEFGHKL + the mapping is {3: 0, 4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7}. However, the + actual Structure sequence has an extra X at the start as well as no IJ. So the + alignment from the query to the Structure sequence will be: + hit_sequence : ---DEFGHIJK- + struc_aligned : --XDEFGH--KL + and the new mapping will therefore be: {3: 1, 4: 2, 5: 3, 6: 4, 7: 5, 10: 6}. + + Args: + hit_sequence: The PDB seqres hit sequence obtained from Hmmsearch, but + without any gaps. This is not the full PDB seqres template sequence but + rather just its subsequence from hit_start_index to hit_end_index. + hit_start_index: The start index of the hit sequence in the full PDB seqres + template sequence (inclusive). + hit_end_index: The end index of the hit sequence in the full PDB seqres + template sequence (exclusive). + full_length: The length of the full PDB seqres template sequence. + structure_sequence: The actual sequence extracted from the Structure + corresponding to this template. In vast majority of cases this is the same + as the PDB seqres sequence, but this function handles the cases when not. + query_to_hit_mapping: The mapping from the query sequence to the + hit_sequence. + + Raises: + AlignmentError: if the alignment between the sequence returned by Hmmsearch + differs from the actual sequence found in the mmCIF and can't be aligned + using the simple alignment algorithm. + + Returns: + A mapping from the query sequence to the actual Structure sequence. + """ + max_num_gaps = full_length - len(structure_sequence) + if max_num_gaps < 0: + raise AlignmentError( + f'The Structure sequence ({len(structure_sequence)}) ' + f'must be shorter than the PDB seqres sequence ({full_length}):\n' + f'Structure sequence : {structure_sequence}\n' + f'PDB seqres sequence: {hit_sequence}' + ) + + if len(hit_sequence) != hit_end_index - hit_start_index: + raise AlignmentError( + f'The difference of {hit_end_index=} and {hit_start_index=} does not ' + f'equal to the length of the {hit_sequence}: {len(hit_sequence)}' + ) + + best_score = -1 + best_start = 0 + best_query_to_hit_mapping = query_to_hit_mapping + max_num_gaps_before_subseq = min(hit_start_index, max_num_gaps) + # It is possible the gaps needed to align the PDB seqres subsequence and + # the Structure subsequence need to be inserted before the match region. + # Try and pick the alignment with the best number of aligned residues. + for num_gaps_before_subseq in range(0, max_num_gaps_before_subseq + 1): + start = hit_start_index - num_gaps_before_subseq + end = hit_end_index - num_gaps_before_subseq + structure_subseq = structure_sequence[start:end] + + new_query_to_hit_mapping, score = _remap_to_struc_seq( + hit_seq=hit_sequence, + struc_seq=structure_subseq, + max_num_gaps=max_num_gaps - num_gaps_before_subseq, + mapping=query_to_hit_mapping, + ) + if score >= best_score: + # Use >= to prefer matches with larger number of gaps before. + best_score = score + best_start = start + best_query_to_hit_mapping = new_query_to_hit_mapping + + return {q: h + best_start for q, h in best_query_to_hit_mapping.items()} + + +def _remap_to_struc_seq( + *, + hit_seq: str, + struc_seq: str, + max_num_gaps: int, + mapping: Mapping[int, int], +) -> tuple[Mapping[int, int], int]: + """Remaps the query -> hit mapping to match the actual Structure sequence. + + Args: + hit_seq: The hit sequence - a subsequence of the PDB seqres sequence without + any Hmmsearch modifications like inserted gaps or lowercased residues. + struc_seq: The actual sequence obtained from the corresponding Structure. + max_num_gaps: The maximum number of gaps that can be inserted in the + Structure sequence. In practice, this is the length difference between the + PDB seqres sequence and the actual Structure sequence. + mapping: The mapping from the query residues to the hit residues. This will + be remapped to point to the actual Structure sequence using a simple + realignment algorithm. + + Returns: + A tuple of (mapping, score): + * Mapping from the query to the actual Structure sequence. + * Score which is the number of matching aligned residues. + + Raises: + ValueError if the structure sequence isn't shorter than the seqres sequence. + ValueError if the alignment fails. + """ + hit_seq_idx = 0 + struc_seq_idx = 0 + hit_to_struc_seq_mapping = {} + score = 0 + + # This while loop is guaranteed to terminate since we increase both + # struc_seq_idx and hit_seq_idx by at least 1 in each iteration. + remaining_num_gaps = max_num_gaps + while hit_seq_idx < len(hit_seq) and struc_seq_idx < len(struc_seq): + if hit_seq[hit_seq_idx] != struc_seq[struc_seq_idx]: + # Explore which alignment aligns the next residue (if present). + best_shift = 0 + for shift in range(0, remaining_num_gaps + 1): + next_hit_res = hit_seq[hit_seq_idx + + shift: hit_seq_idx + shift + 1] + next_struc_res = struc_seq[struc_seq_idx: struc_seq_idx + 1] + if next_hit_res == next_struc_res: + best_shift = shift + break + hit_seq_idx += best_shift + remaining_num_gaps -= best_shift + + hit_to_struc_seq_mapping[hit_seq_idx] = struc_seq_idx + score += hit_seq[hit_seq_idx] == struc_seq[struc_seq_idx] + hit_seq_idx += 1 + struc_seq_idx += 1 + + fixed_mapping = {} + for query_idx, original_hit_idx in mapping.items(): + fixed_hit_idx = hit_to_struc_seq_mapping.get(original_hit_idx) + if fixed_hit_idx is not None: + fixed_mapping[query_idx] = fixed_hit_idx + + return fixed_mapping, score diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_store.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_store.py new file mode 100644 index 000000000..004443960 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_store.py @@ -0,0 +1,47 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Interface and implementations for fetching templates data.""" + +from collections.abc import Mapping +import datetime +from typing import Any, Protocol, TypeAlias + + +TemplateFeatures: TypeAlias = Mapping[str, Any] + + +class TemplateFeatureProvider(Protocol): + """Interface for providing Template Features.""" + + def __call__( + self, + sequence: str, + release_date: datetime.date | None, + include_ligand_features: bool = True, + ) -> TemplateFeatures: + """Retrieve template features for the given sequence and release_date. + + Args: + sequence: The residue sequence of the query. + release_date: The release_date of the template query, this is used to + filter templates for training, ensuring that they do not leak structure + information from the future. + include_ligand_features: Whether to include ligand features. + + Returns: + Template features: A mapping of template feature labels to features, which + may be numpy arrays, bytes objects, or for the special case of label + `ligand_features`, a nested feature map of labels to numpy arrays. + + Raises: + TemplateRetrievalError if the template features were not found. + """ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/templates.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/templates.py new file mode 100644 index 000000000..060dc7b83 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/templates.py @@ -0,0 +1,974 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""API for retrieving and manipulating template search results.""" + +from collections.abc import Iterable, Iterator, Mapping, Sequence +import dataclasses +import datetime +import functools +import os +import re +from typing import Any, Final, Self, TypeAlias +import numpy as np +from absl import logging +from alphafold3 import structure +from alphafold3.common import resources +from alphafold3.constants import atom_types +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.data import msa_config +from alphafold3.data import parsers +from alphafold3.data import structure_stores +from alphafold3.data import template_realign +from alphafold3.data.tools import hmmsearch +from alphafold3.structure import mmcif + + +_POLYMER_FEATURES: Final[Mapping[str, np.float64 | np.int32 | object]] = { + 'template_aatype': np.int32, + 'template_all_atom_masks': np.float64, + 'template_all_atom_positions': np.float64, + 'template_domain_names': object, + 'template_release_date': object, + 'template_sequence': object, +} + +_LIGAND_FEATURES: Final[Mapping[str, Any]] = { + 'ligand_features': Mapping[str, Any] +} + + +TemplateFeatures: TypeAlias = Mapping[ + str, np.ndarray | bytes | Mapping[str, np.ndarray | bytes] +] +_REQUIRED_METADATA_COLUMNS: Final[Sequence[str]] = ( + 'seq_release_date', + 'seq_unresolved_res_num', + 'seq_author_chain_id', + 'seq_sequence', +) + + +@dataclasses.dataclass(frozen=True, kw_only=True, slots=True) +class _Polymer: + """Container for alphabet specific (dna, rna, protein) atom information.""" + + min_atoms: int + num_atom_types: int + atom_order: Mapping[str, int] + + +_POLYMERS = { + mmcif_names.PROTEIN_CHAIN: _Polymer( + min_atoms=5, + num_atom_types=atom_types.ATOM37_NUM, + atom_order=atom_types.ATOM37_ORDER, + ), + mmcif_names.DNA_CHAIN: _Polymer( + min_atoms=21, + num_atom_types=atom_types.ATOM29_NUM, + atom_order=atom_types.ATOM29_ORDER, + ), + mmcif_names.RNA_CHAIN: _Polymer( + min_atoms=20, + num_atom_types=atom_types.ATOM29_NUM, + atom_order=atom_types.ATOM29_ORDER, + ), +} + + +def _encode_restype( + chain_poly_type: str, + sequence: str, +) -> Sequence[int]: + """Encodes a sequence of residue names as a sequence of ints. + + Args: + chain_poly_type: Polymer chain type to determine sequence encoding. + sequence: Polymer residues. Protein encoded by single letters. RNA and DNA + encoded by multi-letter CCD codes. + + Returns: + A sequence of integers encoding amino acid types for the given chain type. + """ + if chain_poly_type == mmcif_names.PROTEIN_CHAIN: + return [ + residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP_TO_INT[ + _STANDARDIZED_AA.get(res, res) + ] + for res in sequence + ] + + unk_nucleic = residue_names.UNK_NUCLEIC_ONE_LETTER + unk_nucleic_idx = residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP[ + unk_nucleic + ] + if chain_poly_type == mmcif_names.RNA_CHAIN: + return [ + residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP.get( + res, unk_nucleic_idx + ) + for res in sequence + ] + elif chain_poly_type == mmcif_names.DNA_CHAIN: + # Map UNK DNA to the generic nucleic UNK (N), which happens to also be the + # same as the RNA UNK. + return [ + residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP.get( + residue_names.DNA_COMMON_ONE_TO_TWO.get(res, unk_nucleic), + unk_nucleic_idx, + ) + for res in sequence + ] + + raise NotImplementedError(f'"{chain_poly_type}" unsupported.') + + +_DAYS_BEFORE_QUERY_DATE: Final[int] = 60 +_HIT_DESCRIPTION_REGEX = re.compile( + r'(?P[a-z0-9]{4,})_(?P\w+)/(?P\d+)-(?P\d+) ' + r'.* length:(?P\d+)\b.*' +) + +_STANDARDIZED_AA = {'B': 'D', 'J': 'X', 'O': 'X', 'U': 'C', 'Z': 'E'} + + +class Error(Exception): + """Base class for exceptions.""" + + +class HitDateError(Error): + """An error indicating that invalid release date was detected.""" + + +class InvalidTemplateError(Error): + """An error indicating that template is invalid.""" + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Hit: + """Template hit metrics derived from the MSA for filtering and featurising. + + Attributes: + pdb_id: The PDB ID of the hit. + auth_chain_id: The author chain ID of the hit. + hmmsearch_sequence: Hit sequence as given in hmmsearch a3m output. + structure_sequence: Hit sequence as given in PDB structure. + unresolved_res_indices: Indices of unresolved residues in the structure + sequence. 0-based. + query_sequence: The query nucleotide/amino acid sequence. + start_index: The start index of the sequence relative to the full PDB seqres + sequence. Inclusive and uses 0-based indexing. + end_index: The end index of the sequence relative to the full PDB seqres + sequence. Exclusive and uses 0-based indexing. + full_length: Length of the full PDB seqres sequence. This can be different + from the length from the actual sequence we get from the mmCIF and we use + this to detect whether we need to realign or not. + release_date: The release date of the PDB corresponding to this hit. + chain_poly_type: The polymer type of the selected hit structure. + """ + + pdb_id: str + auth_chain_id: str + hmmsearch_sequence: str + structure_sequence: str + unresolved_res_indices: Sequence[int] | None + query_sequence: str + start_index: int + end_index: int + full_length: int + release_date: datetime.date + chain_poly_type: str + + @functools.cached_property + def query_to_hit_mapping(self) -> Mapping[int, int]: + """0-based query index to hit index mapping.""" + query_to_hit_mapping = {} + hit_index = 0 + query_index = 0 + for residue in self.hmmsearch_sequence: + # Gap inserted in the template + if residue == '-': + query_index += 1 + # Deleted residue in the template (would be a gap in the query). + elif residue.islower(): + hit_index += 1 + # Normal aligned residue, in both query and template. Add to mapping. + elif residue.isupper(): + query_to_hit_mapping[query_index] = hit_index + query_index += 1 + hit_index += 1 + + structure_subseq = self.structure_sequence[ + self.start_index: self.end_index + ] + if self.matching_sequence != structure_subseq: + # The seqres sequence doesn't match the structure sequence. Two cases: + # 1. The sequences have the same length. The sequences are different + # because our 3->1 residue code mapping is different from the one PDB + # uses. We don't do anything in this case as both sequences have the + # same length, so the original query to hit mapping stays valid. + # 2. The sequences don't have the same length, the one in structure is + # shorter. In this case we change the mapping to match the actual + # structure sequence using a simple realignment algorithm. + # This procedure was validated on all PDB seqres (2023_01_12) sequences + # and handles all cases that can happen. + if self.full_length != len(self.structure_sequence): + return template_realign.realign_hit_to_structure( + hit_sequence=self.matching_sequence, + hit_start_index=self.start_index, + hit_end_index=self.end_index, + full_length=self.full_length, + structure_sequence=self.structure_sequence, + query_to_hit_mapping=query_to_hit_mapping, + ) + + # Hmmsearch returns a subsequence and so far indices have been relative to + # the subsequence. Add an offset to index relative to the full structure + # sequence. + return {q: h + self.start_index for q, h in query_to_hit_mapping.items()} + + @property + def matching_sequence(self) -> str: + """Returns the matching hit sequence including insertions. + + Make deleted residues uppercase and remove gaps ("-"). + """ + return self.hmmsearch_sequence.upper().replace('-', '') + + @functools.cached_property + def output_templates_sequence(self) -> str: + """Returns the final template sequence.""" + result_seq = ['-'] * len(self.query_sequence) + for query_index, template_index in self.query_to_hit_mapping.items(): + result_seq[query_index] = self.structure_sequence[template_index] + return ''.join(result_seq) + + @property + def length_ratio(self) -> float: + """Ratio of the length of the hit sequence to the query.""" + return len(self.matching_sequence) / len(self.query_sequence) + + @property + def align_ratio(self) -> float: + """Ratio of the number of aligned residues to the query length.""" + return len(self.query_to_hit_mapping) / len(self.query_sequence) + + @functools.cached_property + def is_valid(self) -> bool: + """Whether hit can be used as a template.""" + if self.unresolved_res_indices is None: + return False + + return bool( + set(self.query_to_hit_mapping.values()) + - set(self.unresolved_res_indices) + ) + + @property + def full_name(self) -> str: + """A full name of the hit.""" + return f'{self.pdb_id}_{self.auth_chain_id}' + + def __post_init__(self): + if not self.pdb_id.islower() and not self.pdb_id.isdigit(): + raise ValueError(f'pdb_id must be lowercase {self.pdb_id}') + + if not (0 <= self.start_index <= self.end_index): + raise ValueError( + 'Start must be non-negative and less than or equal to end index. ' + f'Range: {self.start_index}-{self.end_index}' + ) + + if len(self.matching_sequence) != (self.end_index - self.start_index): + raise ValueError( + 'Sequence length must be equal to end_index - start_index. ' + f'{len(self.matching_sequence)} != {self.end_index} - ' + f'{self.start_index}' + ) + + if self.full_length < 0: + raise ValueError( + f'Full length must be non-negative: {self.full_length}') + + def keep( + self, + *, + release_date_cutoff: datetime.date | None, + max_subsequence_ratio: float | None, + min_hit_length: int | None, + min_align_ratio: float | None, + ) -> bool: + """Returns whether the hit should be kept. + + In addition to filtering on all of the provided parameters, this method also + excludes hits with unresolved residues. + + Args: + release_date_cutoff: Maximum release date of the template. + max_subsequence_ratio: If set, excludes hits which are an exact + subsequence of the query sequence, and longer than this ratio. Useful to + avoid ground truth leakage. + min_hit_length: If set, excludes hits which have fewer residues than this. + min_align_ratio: If set, excludes hits where the number of residues + aligned to the query is less than this proportion of the template + length. + """ + # Exclude hits which are too recent. + if ( + release_date_cutoff is not None + and self.release_date > release_date_cutoff + ): + return False + + # Exclude hits which are large duplicates of the query_sequence. + if ( + max_subsequence_ratio is not None + and self.length_ratio > max_subsequence_ratio + ): + if self.matching_sequence in self.query_sequence: + return False + + # Exclude hits which are too short. + if ( + min_hit_length is not None + and len(self.matching_sequence) < min_hit_length + ): + return False + + # Exclude hits with unresolved residues. + if not self.is_valid: + return False + + # Exclude hits with too few alignments. + try: + if min_align_ratio is not None and self.align_ratio <= min_align_ratio: + return False + except template_realign.AlignmentError as e: + logging.warning('Failed to align %s: %s', self, str(e)) + return False + + return True + + +def _filter_hits( + hits: Iterable[Hit], + release_date_cutoff: datetime.date, + max_subsequence_ratio: float | None, + min_align_ratio: float | None, + min_hit_length: int | None, + deduplicate_sequences: bool, + max_hits: int | None, +) -> Sequence[Hit]: + """Filters hits based on the filter config.""" + filtered_hits = [] + seen_before = set() + for hit in hits: + if not hit.keep( + max_subsequence_ratio=max_subsequence_ratio, + min_align_ratio=min_align_ratio, + min_hit_length=min_hit_length, + release_date_cutoff=release_date_cutoff, + ): + continue + + # Remove duplicate templates, keeping the first. + if deduplicate_sequences: + if hit.output_templates_sequence in seen_before: + continue + seen_before.add(hit.output_templates_sequence) + + filtered_hits.append(hit) + if max_hits and len(filtered_hits) == max_hits: + break + + return filtered_hits + + +@dataclasses.dataclass(init=False) +class Templates: + """A container for templates that were found for the given query sequence. + + The structure_store is constructed from the config by default. Callers can + optionally supply a structure_store to the constructor to avoid the cost of + construction and metadata loading. + """ + + def __init__( + self, + *, + query_sequence: str, + hits: Sequence[Hit], + max_template_date: datetime.date, + structure_store: structure_stores.StructureStore, + query_release_date: datetime.date | None = None, + ): + self._query_sequence = query_sequence + self._hits = tuple(hits) + self._max_template_date = max_template_date + self._query_release_date = query_release_date + self._hit_structures = {} + self._structure_store = structure_store + + if any(h.query_sequence != self._query_sequence for h in self.hits): + raise ValueError('All hits must match the query sequence.') + + if self._hits: + chain_poly_type = self._hits[0].chain_poly_type + if any(h.chain_poly_type != chain_poly_type for h in self.hits): + raise ValueError( + 'All hits must have the same chain_poly_type.') + + @classmethod + def from_seq_and_a3m( + cls, + *, + query_sequence: str, + msa_a3m: str, + max_template_date: datetime.date, + database_path: os.PathLike[str] | str, + hmmsearch_config: msa_config.HmmsearchConfig, + max_a3m_query_sequences: int | None, + structure_store: structure_stores.StructureStore, + filter_config: msa_config.TemplateFilterConfig | None = None, + query_release_date: datetime.date | None = None, + chain_poly_type: str = mmcif_names.PROTEIN_CHAIN, + ) -> Self: + """Creates templates from a run of hmmsearch tool against a custom a3m. + + Args: + query_sequence: The polymer sequence of the target query. + msa_a3m: An a3m of related polymers aligned to the query sequence, this is + used to create an HMM for the hmmsearch run. + max_template_date: This is used to filter templates for training, ensuring + that they do not leak ground truth information used in testing sets. + database_path: A path to the sequence database to search for templates. + hmmsearch_config: Config with Hmmsearch settings. + max_a3m_query_sequences: The maximum number of input MSA sequences to use + to construct the profile which is then used to search for templates. + structure_store: Structure store to fetch template structures from. + filter_config: Optional config that controls which and how many hits to + keep. More performant than constructing and then filtering. If not + provided, no filtering is done. + query_release_date: The release_date of the template query, this is used + to filter templates for training, ensuring that they do not leak + structure information from the future. + chain_poly_type: The polymer type of the templates. + + Returns: + Templates object containing a list of Hits initialised from the + structure_store metadata and a3m alignments. + """ + hmmsearch_a3m = run_hmmsearch_with_a3m( + database_path=database_path, + hmmsearch_config=hmmsearch_config, + max_a3m_query_sequences=max_a3m_query_sequences, + a3m=msa_a3m, + ) + return cls.from_hmmsearch_a3m( + query_sequence=query_sequence, + a3m=hmmsearch_a3m, + max_template_date=max_template_date, + query_release_date=query_release_date, + chain_poly_type=chain_poly_type, + structure_store=structure_store, + filter_config=filter_config, + ) + + @classmethod + def from_hmmsearch_a3m( + cls, + *, + query_sequence: str, + a3m: str, + max_template_date: datetime.date, + structure_store: structure_stores.StructureStore, + filter_config: msa_config.TemplateFilterConfig | None = None, + query_release_date: datetime.date | None = None, + chain_poly_type: str = mmcif_names.PROTEIN_CHAIN, + ) -> Self: + """Creates Templates from a Hmmsearch A3M. + + Args: + query_sequence: The polymer sequence of the target query. + a3m: Results of Hmmsearch in A3M format. This provides a list of potential + template alignments and pdb codes. + max_template_date: This is used to filter templates for training, ensuring + that they do not leak ground truth information used in testing sets. + structure_store: Structure store to fetch template structures from. + filter_config: Optional config that controls which and how many hits to + keep. More performant than constructing and then filtering. If not + provided, no filtering is done. + query_release_date: The release_date of the template query, this is used + to filter templates for training, ensuring that they do not leak + structure information from the future. + chain_poly_type: The polymer type of the templates. + + Returns: + Templates object containing a list of Hits initialised from the + structure_store metadata and a3m alignments. + """ + + def hit_generator(a3m: str): + for hit_seq, hit_desc in parsers.lazy_parse_fasta_string(a3m): + pdb_id, auth_chain_id, start, end, full_length = _parse_hit_description( + hit_desc + ) + + release_date, sequence, unresolved_res_ids = _parse_hit_metadata( + structure_store, pdb_id, auth_chain_id + ) + if unresolved_res_ids is None: + continue + + # seq_unresolved_res_num are 1-based, setting to 0-based indices. + unresolved_indices = [i - 1 for i in unresolved_res_ids] + + yield Hit( + pdb_id=pdb_id, + auth_chain_id=auth_chain_id, + hmmsearch_sequence=hit_seq, + structure_sequence=sequence, + query_sequence=query_sequence, + unresolved_res_indices=unresolved_indices, + # Raw value is residue number, not index. + start_index=start - 1, + end_index=end, + full_length=full_length, + release_date=datetime.date.fromisoformat(release_date), + chain_poly_type=chain_poly_type, + ) + + if filter_config is None: + hits = tuple(hit_generator(a3m)) + else: + hits = _filter_hits( + hit_generator(a3m), + release_date_cutoff=filter_config.max_template_date, + max_subsequence_ratio=filter_config.max_subsequence_ratio, + min_align_ratio=filter_config.min_align_ratio, + min_hit_length=filter_config.min_hit_length, + deduplicate_sequences=filter_config.deduplicate_sequences, + max_hits=filter_config.max_hits, + ) + + return Templates( + query_sequence=query_sequence, + query_release_date=query_release_date, + hits=hits, + max_template_date=max_template_date, + structure_store=structure_store, + ) + + @property + def query_sequence(self) -> str: + return self._query_sequence + + @property + def hits(self) -> tuple[Hit, ...]: + return self._hits + + @property + def query_release_date(self) -> datetime.date | None: + return self._query_release_date + + @property + def num_hits(self) -> int: + return len(self._hits) + + @functools.cached_property + def release_date_cutoff(self) -> datetime.date: + if self.query_release_date is None: + return self._max_template_date + return min( + self._max_template_date, + self.query_release_date + - datetime.timedelta(days=_DAYS_BEFORE_QUERY_DATE), + ) + + def __repr__(self) -> str: + return f'Templates({self.num_hits} hits)' + + def filter( + self, + *, + max_subsequence_ratio: float | None, + min_align_ratio: float | None, + min_hit_length: int | None, + deduplicate_sequences: bool, + max_hits: int | None, + ) -> Self: + """Returns a new Templates object with only the hits that pass all filters. + + This also filters on query_release_date and max_template_date. + + Args: + max_subsequence_ratio: If set, excludes hits which are an exact + subsequence of the query sequence, and longer than this ratio. Useful to + avoid ground truth leakage. + min_align_ratio: If set, excludes hits where the number of residues + aligned to the query is less than this proportion of the template + length. + min_hit_length: If set, excludes hits which have fewer residues than this. + deduplicate_sequences: Whether to exclude duplicate template sequences, + keeping only the first. This can be useful in increasing the diversity + of hits especially in the case of homomer hits. + max_hits: If set, excludes any hits which exceed this count. + """ + filtered_hits = _filter_hits( + hits=self._hits, + release_date_cutoff=self.release_date_cutoff, + max_subsequence_ratio=max_subsequence_ratio, + min_align_ratio=min_align_ratio, + min_hit_length=min_hit_length, + deduplicate_sequences=deduplicate_sequences, + max_hits=max_hits, + ) + return Templates( + query_sequence=self.query_sequence, + query_release_date=self.query_release_date, + hits=filtered_hits, + max_template_date=self._max_template_date, + structure_store=self._structure_store, + ) + + def get_hits_with_structures( + self, + ) -> Sequence[tuple[Hit, structure.Structure]]: + """Returns hits + Structures, Structures filtered to the hit's chain.""" + results = [] + structures = {struct.name.lower(): struct for struct in self.structures} + for hit in self.hits: + if not hit.is_valid: + raise InvalidTemplateError( + 'Hits must be filtered before calling get_hits_with_structures.' + ) + struct = structures[hit.pdb_id] + label_chain_id = struct.polymer_auth_asym_id_to_label_asym_id().get( + hit.auth_chain_id + ) + results.append((hit, struct.filter(chain_id=label_chain_id))) + return results + + def featurize( + self, + include_ligand_features: bool = True, + ) -> TemplateFeatures: + """Featurises the templates and returns a map of feature names to features. + + NB: If you don't do any prefiltering, this method might be slow to run + as it has to fetch many CIFs and featurize them all. + + Args: + include_ligand_features: Whether to compute ligand features. + + Returns: + Template features: A mapping of template feature labels to features, which + may be numpy arrays, bytes objects, or for the special case of label + `ligand_features` (if `include_ligand_features` is True), a nested + feature map of labels to numpy arrays. + + Raises: + InvalidTemplateError: If hits haven't been filtered before featurization. + """ + hits_by_pdb_id = {} + for idx, hit in enumerate(self.hits): + if not hit.is_valid: + raise InvalidTemplateError( + f'Hits must be filtered before featurizing, got unprocessed {hit=}' + ) + hits_by_pdb_id.setdefault(hit.pdb_id, []).append((idx, hit)) + + unsorted_features = [] + for struct in self.structures: + pdb_id = str(struct.name).lower() + for idx, hit in hits_by_pdb_id[pdb_id]: + try: + label_chain_id = struct.polymer_auth_asym_id_to_label_asym_id()[ + hit.auth_chain_id + ] + hit_features = { + **get_polymer_features( + chain=struct.filter(chain_id=label_chain_id), + chain_poly_type=hit.chain_poly_type, + query_sequence_length=len(hit.query_sequence), + query_to_hit_mapping=hit.query_to_hit_mapping, + ), + } + if include_ligand_features: + hit_features['ligand_features'] = _get_ligand_features( + struct) + unsorted_features.append((idx, hit_features)) + except Error as e: + raise type(e)(f'Failed to featurise {hit=}') from e + + sorted_features = sorted(unsorted_features, key=lambda x: x[0]) + sorted_features = [feat for _, feat in sorted_features] + return package_template_features( + hit_features=sorted_features, + include_ligand_features=include_ligand_features, + ) + + @property + def structures(self) -> Iterator[structure.Structure]: + """Yields template structures for each unique PDB ID among hits. + + If there are multiple hits in the same Structure, the Structure will be + included only once by this method. + + Yields: + A Structure object for each unique PDB ID among hits. + + Raises: + HitDateError: If template's release date exceeds max cutoff date. + """ + + for hit in self.hits: + if hit.release_date > self.release_date_cutoff: # pylint: disable=comparison-with-callable + raise HitDateError( + f'Invalid release date for hit {hit.pdb_id=}, when release date ' + f'cutoff is {self.release_date_cutoff}.' + ) + + # Get the set of pdbs to load. In particular, remove duplicate PDB IDs. + targets_to_load = tuple({hit.pdb_id for hit in self.hits}) + + for target_name in targets_to_load: + yield structure.from_mmcif( + mmcif_string=self._structure_store.get_mmcif_str(target_name), + fix_mse_residues=True, + fix_arginines=True, + include_water=False, + include_bonds=False, + include_other=True, # For non-standard polymer chains. + ) + + +def _parse_hit_description(description: str) -> tuple[str, str, int, int, int]: + """Parses the hmmsearch A3M sequence description line.""" + # Example lines (protein, nucleic, no description): + # >4pqx_A/2-217 [subseq from] mol:protein length:217 Free text + # >4pqx_A/2-217 [subseq from] mol:na length:217 Free text + # >5g3r_A/1-55 [subseq from] mol:protein length:352 + if match := re.fullmatch(_HIT_DESCRIPTION_REGEX, description): + return ( + match['pdb_id'], + match['chain_id'], + int(match['start']), + int(match['end']), + int(match['length']), + ) + else: + raise ValueError(f'Could not parse description "{description}"') + + +def _parse_hit_metadata( + structure_store: structure_stores.StructureStore, + pdb_id: str, + auth_chain_id: str, +) -> tuple[Any, str | None, Sequence[int] | None]: + """Parse hit metadata by parsing mmCIF from structure store.""" + try: + cif = mmcif.from_string(structure_store.get_mmcif_str(pdb_id)) + except structure_stores.NotFoundError: + logging.warning('Failed to get mmCIF for %s.', pdb_id) + return None, None, None + release_date = mmcif.get_release_date(cif) + + try: + struct = structure.from_parsed_mmcif( + cif, + model_id=structure.ModelID.ALL, + include_water=True, + include_other=True, + include_bonds=False, + ) + except ValueError: + struct = structure.from_parsed_mmcif( + cif, + model_id=structure.ModelID.FIRST, + include_water=True, + include_other=True, + include_bonds=False, + ) + + sequence = struct.polymer_author_chain_single_letter_sequence( + include_missing_residues=True, + protein=True, + dna=True, + rna=True, + other=True, + )[auth_chain_id] + + unresolved_res_ids = struct.filter( + chain_auth_asym_id=auth_chain_id + ).unresolved_residues.id + + return release_date, sequence, unresolved_res_ids + + +def get_polymer_features( + *, + chain: structure.Structure, + chain_poly_type: str, + query_sequence_length: int, + query_to_hit_mapping: Mapping[int, int], +) -> Mapping[str, Any]: + """Returns features for this polymer chain. + + Args: + chain: Structure object representing the template. Must be already filtered + to a single chain. + chain_poly_type: The chain polymer type (protein, DNA, RNA). + query_sequence_length: The length of the query sequence. + query_to_hit_mapping: 0-based query index to hit index mapping. + + Returns: + A dictionary with polymer features for template_chain_id in the struct. + + Raises: + ValueError: If the input structure contains more than just a single chain. + """ + if len(chain.polymer_auth_asym_id_to_label_asym_id()) != 1: + raise ValueError('The structure must be filtered to a single chain.') + + if chain.name is None: + raise ValueError('The structure must have a name.') + + if chain.release_date is None: + raise ValueError('The structure must have a release date.') + + auth_chain_id, label_chain_id = next( + iter(chain.polymer_auth_asym_id_to_label_asym_id().items()) + ) + chain_sequence = chain.chain_single_letter_sequence()[label_chain_id] + + polymer = _POLYMERS[chain_poly_type] + positions, positions_mask = chain.to_res_arrays( + include_missing_residues=True, atom_order=polymer.atom_order + ) + template_all_atom_positions = np.zeros( + (query_sequence_length, polymer.num_atom_types, 3), dtype=np.float64 + ) + template_all_atom_masks = np.zeros( + (query_sequence_length, polymer.num_atom_types), dtype=np.int64 + ) + + template_sequence = ['-'] * query_sequence_length + for query_index, template_index in query_to_hit_mapping.items(): + template_all_atom_positions[query_index] = positions[template_index] + template_all_atom_masks[query_index] = positions_mask[template_index] + template_sequence[query_index] = chain_sequence[template_index] + + template_sequence = ''.join(template_sequence) + template_aatype = _encode_restype(chain_poly_type, template_sequence) + template_name = f'{chain.name.lower()}_{auth_chain_id}' + release_date = chain.release_date.strftime('%Y-%m-%d') + return { + 'template_all_atom_positions': template_all_atom_positions, + 'template_all_atom_masks': template_all_atom_masks, + 'template_sequence': template_sequence.encode(), + 'template_aatype': np.array(template_aatype, dtype=np.int32), + 'template_domain_names': np.array(template_name.encode(), dtype=object), + 'template_release_date': np.array(release_date.encode(), dtype=object), + } + + +def _get_ligand_features( + struct: structure.Structure, +) -> Mapping[str, Mapping[str, np.ndarray | bytes]]: + """Returns features for the ligands in this structure.""" + ligand_struct = struct.filter_to_entity_type(ligand=True) + assert ligand_struct.coords is not None + assert ligand_struct.atom_name is not None + assert ligand_struct.atom_occupancy is not None + + ligand_features = {} + for ligand_chain_id in ligand_struct.chains: + idxs = np.where(ligand_struct.chain_id == ligand_chain_id)[0] + if idxs.shape[0]: + ligand_features[ligand_chain_id] = { + 'ligand_atom_positions': ligand_struct.coords[idxs, :].astype( + np.float32 + ), + 'ligand_atom_names': ligand_struct.atom_name[idxs].astype(object), + 'ligand_atom_occupancies': ligand_struct.atom_occupancy[idxs].astype( + np.float32 + ), + 'ccd_id': ligand_struct.res_name[idxs][0].encode(), + } + return ligand_features + + +def package_template_features( + *, + hit_features: Sequence[Mapping[str, Any]], + include_ligand_features: bool, +) -> Mapping[str, Any]: + """Stacks polymer features, adds empty and keeps ligand features unstacked.""" + + features_to_include = set(_POLYMER_FEATURES) + if include_ligand_features: + features_to_include.update(_LIGAND_FEATURES) + + features = { + feat: [single_hit_features[feat] + for single_hit_features in hit_features] + for feat in features_to_include + } + + stacked_features = {} + for k, v in features.items(): + if k in _POLYMER_FEATURES: + v = np.stack(v, axis=0) if v else np.array( + [], dtype=_POLYMER_FEATURES[k]) + stacked_features[k] = v + + return stacked_features + + +def _resolve_path(path: os.PathLike[str] | str) -> str: + """Resolves path for data dep paths, stringifies otherwise.""" + # Data dependency paths: db baked into the binary. + resolved_path = resources.filename(path) + if os.path.exists(resolved_path): + return resolved_path + else: + # Other paths, e.g. local. + return str(path) + + +def run_hmmsearch_with_a3m( + *, + database_path: os.PathLike[str] | str, + hmmsearch_config: msa_config.HmmsearchConfig, + max_a3m_query_sequences: int | None, + a3m: str | None, +) -> str: + """Runs Hmmsearch to get a3m string of hits.""" + searcher = hmmsearch.Hmmsearch( + binary_path=hmmsearch_config.hmmsearch_binary_path, + hmmbuild_binary_path=hmmsearch_config.hmmbuild_binary_path, + database_path=_resolve_path(database_path), + e_value=hmmsearch_config.e_value, + inc_e=hmmsearch_config.inc_e, + dom_e=hmmsearch_config.dom_e, + incdom_e=hmmsearch_config.incdom_e, + alphabet=hmmsearch_config.alphabet, + filter_f1=hmmsearch_config.filter_f1, + filter_f2=hmmsearch_config.filter_f2, + filter_f3=hmmsearch_config.filter_f3, + filter_max=hmmsearch_config.filter_max, + ) + # STO enables us to annotate query non-gap columns as reference columns. + sto = parsers.convert_a3m_to_stockholm(a3m, max_a3m_query_sequences) + return searcher.query_with_sto(sto, model_construction='hand') diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmalign.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmalign.py new file mode 100644 index 000000000..f36967338 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmalign.py @@ -0,0 +1,144 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""A Python wrapper for hmmalign from the HMMER Suite.""" + +from collections.abc import Mapping, Sequence +import os +import tempfile + +from alphafold3.data import parsers +from alphafold3.data.tools import subprocess_utils + + +def _to_a3m(sequences: Sequence[str], name_prefix: str = 'sequence') -> str: + a3m = '' + for i, sequence in enumerate(sequences, 1): + a3m += f'> {name_prefix} {i}\n{sequence}\n' + return a3m + + +class Hmmalign: + """Python wrapper of the hmmalign binary.""" + + def __init__(self, binary_path: str): + """Initializes the Python hmmalign wrapper. + + Args: + binary_path: Path to the hmmalign binary. + + Raises: + RuntimeError: If hmmalign binary not found within the path. + """ + self.binary_path = binary_path + + subprocess_utils.check_binary_exists( + path=self.binary_path, name='hmmalign') + + def align_sequences( + self, + sequences: Sequence[str], + profile: str, + extra_flags: Mapping[str, str] | None = None, + ) -> str: + """Aligns sequence list to the profile and returns the alignment in A3M.""" + return self.align( + a3m_str=_to_a3m(sequences, name_prefix='query'), + profile=profile, + extra_flags=extra_flags, + ) + + def align( + self, + a3m_str: str, + profile: str, + extra_flags: Mapping[str, str] | None = None, + ) -> str: + """Aligns sequences in A3M to the profile and returns the alignment in A3M. + + Args: + a3m_str: A list of sequence strings. + profile: A hmm file with the hmm profile to align the sequences to. + extra_flags: Dictionary with extra flags, flag_name: flag_value, that are + added to hmmalign. + + Returns: + An A3M string with the aligned sequences. + + Raises: + RuntimeError: If hmmalign fails. + """ + with tempfile.TemporaryDirectory() as query_tmp_dir: + input_profile = os.path.join(query_tmp_dir, 'profile.hmm') + input_sequences = os.path.join(query_tmp_dir, 'sequences.a3m') + output_a3m_path = os.path.join(query_tmp_dir, 'output.a3m') + + with open(input_profile, 'w') as f: + f.write(profile) + + with open(input_sequences, 'w') as f: + f.write(a3m_str) + + cmd = [ + self.binary_path, + *('-o', output_a3m_path), + *('--outformat', 'A2M'), # A2M is A3M in the HMMER suite. + ] + if extra_flags: + for flag_name, flag_value in extra_flags.items(): + cmd.extend([flag_name, flag_value]) + cmd.extend([input_profile, input_sequences]) + + subprocess_utils.run( + cmd=cmd, + cmd_name='hmmalign', + log_stdout=False, + log_stderr=True, + log_on_process_error=True, + ) + + with open(output_a3m_path, encoding='utf-8') as f: + a3m = f.read() + + return a3m + + def align_sequences_to_profile(self, profile: str, sequences_a3m: str) -> str: + """Aligns the sequences to profile and returns the alignment in A3M string. + + Uses hmmalign to align the sequences to the profile, then outputs the + sequence concatenated at the beginning of the sequences in the A3M format. + As the sequences are represented by an alignment with possible gaps ('-') + and insertions (lowercase characters), the method first removes the gaps, + then uppercases the insertions to prepare the sequences for realignment. + Sequences with gaps cannot be aligned, as '-'s are not a valid symbol to + align; lowercase characters must be uppercased to preserve the original + sequences before realignment. + + Args: + profile: The Hmmbuild profile to align the sequences to. + sequences_a3m: Sequences in A3M format to align to the profile. + + Returns: + An A3M string with the aligned sequences. + + Raises: + RuntimeError: If hmmalign fails. + """ + deletion_table = str.maketrans('', '', '-') + sequences_no_gaps_a3m = [] + for seq, desc in parsers.lazy_parse_fasta_string(sequences_a3m): + sequences_no_gaps_a3m.append(f'>{desc}') + sequences_no_gaps_a3m.append(seq.translate(deletion_table)) + sequences_no_gaps_a3m = '\n'.join(sequences_no_gaps_a3m) + + aligned_sequences = self.align(sequences_no_gaps_a3m, profile) + + return aligned_sequences diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmbuild.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmbuild.py new file mode 100644 index 000000000..8d1f798ba --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmbuild.py @@ -0,0 +1,148 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""A Python wrapper for hmmbuild - construct HMM profiles from MSA.""" + +import os +import re +import tempfile +from typing import Literal + +from alphafold3.data import parsers +from alphafold3.data.tools import subprocess_utils + + +class Hmmbuild(object): + """Python wrapper of the hmmbuild binary.""" + + def __init__( + self, + *, + binary_path: str, + singlemx: bool = False, + alphabet: str | None = None, + ): + """Initializes the Python hmmbuild wrapper. + + Args: + binary_path: The path to the hmmbuild executable. + singlemx: Whether to use --singlemx flag. If True, it forces HMMBuild to + just use a common substitution score matrix. + alphabet: The alphabet to assert when building a profile. Useful when + hmmbuild cannot guess the alphabet. If None, no alphabet is asserted. + + Raises: + RuntimeError: If hmmbuild binary not found within the path. + """ + self.binary_path = binary_path + self.singlemx = singlemx + self.alphabet = alphabet + + subprocess_utils.check_binary_exists( + path=self.binary_path, name='hmmbuild') + + def build_profile_from_sto(self, sto: str, model_construction='fast') -> str: + """Builds a HHM for the aligned sequences given as an A3M string. + + Args: + sto: A string with the aligned sequences in the Stockholm format. + model_construction: Whether to use reference annotation in the msa to + determine consensus columns ('hand') or default ('fast'). + + Returns: + A string with the profile in the HMM format. + + Raises: + RuntimeError: If hmmbuild fails. + """ + return self._build_profile( + sto, informat='stockholm', model_construction=model_construction + ) + + def build_profile_from_a3m(self, a3m: str) -> str: + """Builds a HHM for the aligned sequences given as an A3M string. + + Args: + a3m: A string with the aligned sequences in the A3M format. + + Returns: + A string with the profile in the HMM format. + + Raises: + RuntimeError: If hmmbuild fails. + """ + lines = [] + for sequence, description in parsers.lazy_parse_fasta_string(a3m): + # Remove inserted residues. + sequence = re.sub('[a-z]+', '', sequence) + lines.append(f'>{description}\n{sequence}\n') + msa = ''.join(lines) + return self._build_profile(msa, informat='afa') + + def _build_profile( + self, + msa: str, + informat: Literal['afa', 'stockholm'], + model_construction: str = 'fast', + ) -> str: + """Builds a HMM for the aligned sequences given as an MSA string. + + Args: + msa: A string with the aligned sequences, in A3M or STO format. + informat: One of 'afa' (aligned FASTA) or 'sto' (Stockholm). + model_construction: Whether to use reference annotation in the msa to + determine consensus columns ('hand') or default ('fast'). + + Returns: + A string with the profile in the HMM format. + + Raises: + RuntimeError: If hmmbuild fails. + ValueError: If unspecified arguments are provided. + """ + if model_construction not in {'hand', 'fast'}: + raise ValueError( + f'Bad {model_construction=}. Only hand or fast allowed.') + + with tempfile.TemporaryDirectory() as query_tmp_dir: + input_msa_path = os.path.join(query_tmp_dir, 'query.msa') + output_hmm_path = os.path.join(query_tmp_dir, 'output.hmm') + + with open(input_msa_path, 'w') as f: + f.write(msa) + + # Specify the format as we don't specify the input file extension. See + # https://github.com/EddyRivasLab/hmmer/issues/321 for more details. + cmd_flags = ['--informat', informat] + # If adding flags, we have to do so before the output and input: + if model_construction == 'hand': + cmd_flags.append(f'--{model_construction}') + if self.singlemx: + cmd_flags.append('--singlemx') + if self.alphabet: + cmd_flags.append(f'--{self.alphabet}') + + cmd_flags.extend([output_hmm_path, input_msa_path]) + + cmd = [self.binary_path, *cmd_flags] + + subprocess_utils.run( + cmd=cmd, + cmd_name='Hmmbuild', + log_stdout=False, + log_stderr=True, + log_on_process_error=True, + ) + + with open(output_hmm_path) as f: + hmm = f.read() + + return hmm diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py new file mode 100644 index 000000000..1b4854e5d --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py @@ -0,0 +1,152 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""A Python wrapper for hmmsearch - search profile against a sequence db.""" + +import os +import tempfile + +from absl import logging +from alphafold3.data import parsers +from alphafold3.data.tools import hmmbuild +from alphafold3.data.tools import subprocess_utils + + +class Hmmsearch(object): + """Python wrapper of the hmmsearch binary.""" + + def __init__( + self, + *, + binary_path: str, + hmmbuild_binary_path: str, + database_path: str, + alphabet: str = 'amino', + filter_f1: float | None = None, + filter_f2: float | None = None, + filter_f3: float | None = None, + e_value: float | None = None, + inc_e: float | None = None, + dom_e: float | None = None, + incdom_e: float | None = None, + filter_max: bool = False, + ): + """Initializes the Python hmmsearch wrapper. + + Args: + binary_path: The path to the hmmsearch executable. + hmmbuild_binary_path: The path to the hmmbuild executable. Used to build + an hmm from an input a3m. + database_path: The path to the hmmsearch database (FASTA format). + alphabet: Chain type e.g. amino, rna, dna. + filter_f1: MSV and biased composition pre-filter, set to >1.0 to turn off. + filter_f2: Viterbi pre-filter, set to >1.0 to turn off. + filter_f3: Forward pre-filter, set to >1.0 to turn off. + e_value: E-value criteria for inclusion in tblout. + inc_e: E-value criteria for inclusion in MSA/next round. + dom_e: Domain e-value criteria for inclusion in tblout. + incdom_e: Domain e-value criteria for inclusion of domains in MSA/next + round. + filter_max: Remove all filters, will ignore all filter_f* settings. + + Raises: + RuntimeError: If hmmsearch binary not found within the path. + """ + self.binary_path = binary_path + self.hmmbuild_runner = hmmbuild.Hmmbuild( + alphabet=alphabet, binary_path=hmmbuild_binary_path + ) + self.database_path = database_path + flags = [] + if filter_max: + flags.append('--max') + else: + if filter_f1 is not None: + flags.extend(('--F1', filter_f1)) + if filter_f2 is not None: + flags.extend(('--F2', filter_f2)) + if filter_f3 is not None: + flags.extend(('--F3', filter_f3)) + + if e_value is not None: + flags.extend(('-E', e_value)) + if inc_e is not None: + flags.extend(('--incE', inc_e)) + if dom_e is not None: + flags.extend(('--domE', dom_e)) + if incdom_e is not None: + flags.extend(('--incdomE', incdom_e)) + + self.flags = tuple(map(str, flags)) + + subprocess_utils.check_binary_exists( + path=self.binary_path, name='hmmsearch' + ) + + if not os.path.exists(self.database_path): + logging.error( + 'Could not find hmmsearch database %s', database_path) + raise ValueError( + f'Could not find hmmsearch database {database_path}') + + def query_with_hmm(self, hmm: str) -> str: + """Queries the database using hmmsearch using a given hmm.""" + with tempfile.TemporaryDirectory() as query_tmp_dir: + hmm_input_path = os.path.join(query_tmp_dir, 'query.hmm') + sto_out_path = os.path.join(query_tmp_dir, 'output.sto') + with open(hmm_input_path, 'w') as f: + f.write(hmm) + + cmd = [ + self.binary_path, + '--noali', # Don't include the alignment in stdout. + *('--cpu', '8'), + ] + # If adding flags, we have to do so before the output and input: + if self.flags: + cmd.extend(self.flags) + cmd.extend([ + *('-A', sto_out_path), + hmm_input_path, + self.database_path, + ]) + + subprocess_utils.run( + cmd=cmd, + cmd_name='Hmmsearch', + log_stdout=False, + log_stderr=True, + log_on_process_error=True, + ) + + with open(sto_out_path) as f: + a3m_out = parsers.convert_stockholm_to_a3m( + f, remove_first_row_gaps=False, linewidth=60 + ) + + return a3m_out + + def query_with_a3m(self, a3m_in: str) -> str: + """Query the database using hmmsearch using a given a3m.""" + + # Only the "fast" model construction makes sense with A3M, as it doesn't + # have any way to annotate reference columns. + hmm = self.hmmbuild_runner.build_profile_from_a3m(a3m_in) + return self.query_with_hmm(hmm) + + def query_with_sto( + self, msa_sto: str, model_construction: str = 'fast' + ) -> str: + """Queries the database using hmmsearch using a given stockholm msa.""" + hmm = self.hmmbuild_runner.build_profile_from_sto( + msa_sto, model_construction=model_construction + ) + return self.query_with_hmm(hmm) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/jackhmmer.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/jackhmmer.py new file mode 100644 index 000000000..4ac8c8010 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/jackhmmer.py @@ -0,0 +1,137 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Library to run Jackhmmer from Python.""" + +import os +import tempfile + +from absl import logging +from alphafold3.data import parsers +from alphafold3.data.tools import msa_tool +from alphafold3.data.tools import subprocess_utils + + +class Jackhmmer(msa_tool.MsaTool): + """Python wrapper of the Jackhmmer binary.""" + + def __init__( + self, + *, + binary_path: str, + database_path: str, + n_cpu: int = 8, + n_iter: int = 3, + e_value: float | None = 1e-3, + z_value: float | int | None = None, + max_sequences: int = 5000, + filter_f1: float = 5e-4, + filter_f2: float = 5e-5, + filter_f3: float = 5e-7, + ): + """Initializes the Python Jackhmmer wrapper. + + Args: + binary_path: The path to the jackhmmer executable. + database_path: The path to the jackhmmer database (FASTA format). + n_cpu: The number of CPUs to give Jackhmmer. + n_iter: The number of Jackhmmer iterations. + e_value: The E-value, see Jackhmmer docs for more details. + z_value: The Z-value representing the number of comparisons done (i.e + correct database size) for E-value calculation. + max_sequences: Maximum number of sequences to return in the MSA. + filter_f1: MSV and biased composition pre-filter, set to >1.0 to turn off. + filter_f2: Viterbi pre-filter, set to >1.0 to turn off. + filter_f3: Forward pre-filter, set to >1.0 to turn off. + + Raises: + RuntimeError: If Jackhmmer binary not found within the path. + """ + self.binary_path = binary_path + self.database_path = database_path + + subprocess_utils.check_binary_exists( + path=self.binary_path, name='Jackhmmer' + ) + + if not os.path.exists(self.database_path): + raise ValueError( + f'Could not find Jackhmmer database {database_path}') + + self.n_cpu = n_cpu + self.n_iter = n_iter + self.e_value = e_value + self.z_value = z_value + self.max_sequences = max_sequences + self.filter_f1 = filter_f1 + self.filter_f2 = filter_f2 + self.filter_f3 = filter_f3 + + def query(self, target_sequence: str) -> msa_tool.MsaToolResult: + """Queries the database using Jackhmmer.""" + logging.info('Query sequence: %s', target_sequence) + with tempfile.TemporaryDirectory() as query_tmp_dir: + input_fasta_path = os.path.join(query_tmp_dir, 'query.fasta') + subprocess_utils.create_query_fasta_file( + sequence=target_sequence, path=input_fasta_path + ) + + output_sto_path = os.path.join(query_tmp_dir, 'output.sto') + + # The F1/F2/F3 are the expected proportion to pass each of the filtering + # stages (which get progressively more expensive), reducing these + # speeds up the pipeline at the expensive of sensitivity. They are + # currently set very low to make querying Mgnify run in a reasonable + # amount of time. + cmd_flags = [ + # Don't pollute stdout with Jackhmmer output. + *('-o', '/dev/null'), + *('-A', output_sto_path), + '--noali', + *('--F1', str(self.filter_f1)), + *('--F2', str(self.filter_f2)), + *('--F3', str(self.filter_f3)), + *('--cpu', str(self.n_cpu)), + *('-N', str(self.n_iter)), + ] + + # Report only sequences with E-values <= x in per-sequence output. + if self.e_value is not None: + cmd_flags.extend(['-E', str(self.e_value)]) + + # Use the same value as the reporting e-value (`-E` flag). + cmd_flags.extend(['--incE', str(self.e_value)]) + + if self.z_value is not None: + cmd_flags.extend(['-Z', str(self.z_value)]) + + cmd = ( + [self.binary_path] + + cmd_flags + + [input_fasta_path, self.database_path] + ) + + subprocess_utils.run( + cmd=cmd, + cmd_name='Jackhmmer', + log_stdout=False, + log_stderr=True, + log_on_process_error=True, + ) + + with open(output_sto_path) as f: + a3m = parsers.convert_stockholm_to_a3m( + f, max_sequences=self.max_sequences + ) + + return msa_tool.MsaToolResult( + target_sequence=target_sequence, a3m=a3m, e_value=self.e_value + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/msa_tool.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/msa_tool.py new file mode 100644 index 000000000..0c8bd1894 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/msa_tool.py @@ -0,0 +1,31 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Defines protocol for MSA tools.""" + +import dataclasses +from typing import Protocol + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class MsaToolResult: + """The result of a MSA tool query.""" + + target_sequence: str + e_value: float + a3m: str + + +class MsaTool(Protocol): + """Interface for MSA tools.""" + + def query(self, target_sequence: str) -> MsaToolResult: + """Runs the MSA tool on the target sequence.""" diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/nhmmer.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/nhmmer.py new file mode 100644 index 000000000..70bc06149 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/nhmmer.py @@ -0,0 +1,175 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Library to run Nhmmer from Python.""" + +import os +import pathlib +import tempfile +from typing import Final + +from absl import logging +from alphafold3.data import parsers +from alphafold3.data.tools import hmmalign +from alphafold3.data.tools import hmmbuild +from alphafold3.data.tools import msa_tool +from alphafold3.data.tools import subprocess_utils + +_SHORT_SEQUENCE_CUTOFF: Final[int] = 50 + + +class Nhmmer(msa_tool.MsaTool): + """Python wrapper of the Nhmmer binary.""" + + def __init__( + self, + binary_path: str, + hmmalign_binary_path: str, + hmmbuild_binary_path: str, + database_path: str, + n_cpu: int = 8, + e_value: float = 1e-3, + max_sequences: int = 5000, + filter_f3: float = 1e-5, + alphabet: str | None = None, + strand: str | None = None, + ): + """Initializes the Python Nhmmer wrapper. + + Args: + binary_path: Path to the Nhmmer binary. + hmmalign_binary_path: Path to the Hmmalign binary. + hmmbuild_binary_path: Path to the Hmmbuild binary. + database_path: MSA database path to search against. This can be either a + FASTA (slow) or HMMERDB produced from the FASTA using the makehmmerdb + binary. The HMMERDB is ~10x faster but experimental. + n_cpu: The number of CPUs to give Nhmmer. + e_value: The E-value, see Nhmmer docs for more details. Will be + overwritten if bit_score is set. + max_sequences: Maximum number of sequences to return in the MSA. + filter_f3: Forward pre-filter, set to >1.0 to turn off. + alphabet: The alphabet to assert when building a profile with hmmbuild. + This must be 'rna', 'dna', or None. + strand: "watson" searches query sequence, "crick" searches + reverse-compliment and default is None which means searching for both. + + Raises: + RuntimeError: If Nhmmer binary not found within the path. + """ + self._binary_path = binary_path + self._hmmalign_binary_path = hmmalign_binary_path + self._hmmbuild_binary_path = hmmbuild_binary_path + self._db_path = database_path + + subprocess_utils.check_binary_exists( + path=self._binary_path, name='Nhmmer') + + if strand and strand not in {'watson', 'crick'}: + raise ValueError( + f'Invalid {strand=}. only "watson" or "crick" supported') + + if alphabet and alphabet not in {'rna', 'dna'}: + raise ValueError( + f'Invalid {alphabet=}, only "rna" or "dna" supported') + + self._e_value = e_value + self._n_cpu = n_cpu + self._max_sequences = max_sequences + self._filter_f3 = filter_f3 + self._alphabet = alphabet + self._strand = strand + + def query(self, target_sequence: str) -> msa_tool.MsaToolResult: + """Query the database using Nhmmer.""" + logging.info('Query sequence: %s', target_sequence) + + with tempfile.TemporaryDirectory() as query_tmp_dir: + input_a3m_path = os.path.join(query_tmp_dir, 'query.a3m') + output_sto_path = os.path.join(query_tmp_dir, 'output.sto') + pathlib.Path(output_sto_path).touch() + subprocess_utils.create_query_fasta_file( + sequence=target_sequence, path=input_a3m_path + ) + + cmd_flags = [ + # Don't pollute stdout with nhmmer output. + *('-o', '/dev/null'), + '--noali', # Don't include the alignment in stdout. + *('--cpu', str(self._n_cpu)), + ] + + cmd_flags.extend(['-E', str(self._e_value)]) + + if self._alphabet: + cmd_flags.extend([f'--{self._alphabet}']) + + if self._strand is not None: + cmd_flags.extend([f'--{self._strand}']) + + cmd_flags.extend(['-A', output_sto_path]) + # As recommend by RNAcentral for short sequences. + if ( + self._alphabet == 'rna' + and len(target_sequence) < _SHORT_SEQUENCE_CUTOFF + ): + cmd_flags.extend(['--F3', str(0.02)]) + else: + cmd_flags.extend(['--F3', str(self._filter_f3)]) + + # The input A3M and the db are the last two arguments. + cmd_flags.extend((input_a3m_path, self._db_path)) + + cmd = [self._binary_path, *cmd_flags] + + subprocess_utils.run( + cmd=cmd, + cmd_name='Nhmmer', + log_stdout=False, + log_stderr=True, + log_on_process_error=True, + ) + + if os.path.getsize(output_sto_path) > 0: + with open(output_sto_path) as f: + a3m_out = parsers.convert_stockholm_to_a3m( + # Query not included. + f, max_sequences=self._max_sequences - 1 + ) + # Nhmmer hits are generally shorter than the query sequence. To get MSA + # of width equal to the query sequence, align hits to the query profile. + logging.info( + 'Aligning output a3m of size %d bytes', len(a3m_out)) + + aligner = hmmalign.Hmmalign(self._hmmalign_binary_path) + target_sequence_fasta = f'>query\n{target_sequence}\n' + profile_builder = hmmbuild.Hmmbuild( + binary_path=self._hmmbuild_binary_path, alphabet=self._alphabet + ) + profile = profile_builder.build_profile_from_a3m( + target_sequence_fasta) + a3m_out = aligner.align_sequences_to_profile( + profile=profile, sequences_a3m=a3m_out + ) + a3m_out = ''.join([target_sequence_fasta, a3m_out]) + + # Parse the output a3m to remove line breaks. + a3m = '\n'.join( + [f'>{n}\n{s}' for s, + n in parsers.lazy_parse_fasta_string(a3m_out)] + ) + else: + # Nhmmer returns an empty file if there are no hits. + # In this case return only the query sequence. + a3m = f'>query\n{target_sequence}' + + return msa_tool.MsaToolResult( + target_sequence=target_sequence, e_value=self._e_value, a3m=a3m + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/rdkit_utils.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/rdkit_utils.py new file mode 100644 index 000000000..eed668822 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/rdkit_utils.py @@ -0,0 +1,526 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Tools for calculating features for ligands.""" + +import collections +from collections.abc import Mapping, Sequence + +from absl import logging +from alphafold3.cpp import cif_dict +import numpy as np +import rdkit.Chem as rd_chem + + +_RDKIT_MMCIF_TO_BOND_TYPE: Mapping[str, rd_chem.BondType] = { + 'SING': rd_chem.BondType.SINGLE, + 'DOUB': rd_chem.BondType.DOUBLE, + 'TRIP': rd_chem.BondType.TRIPLE, +} + +_RDKIT_BOND_TYPE_TO_MMCIF: Mapping[rd_chem.BondType, str] = { + v: k for k, v in _RDKIT_MMCIF_TO_BOND_TYPE.items() +} + +_RDKIT_BOND_STEREO_TO_MMCIF: Mapping[rd_chem.BondStereo, str] = { + rd_chem.BondStereo.STEREONONE: 'N', + rd_chem.BondStereo.STEREOE: 'E', + rd_chem.BondStereo.STEREOZ: 'Z', + rd_chem.BondStereo.STEREOCIS: 'Z', + rd_chem.BondStereo.STEREOTRANS: 'E', +} + + +class MolFromMmcifError(Exception): + """Raised when conversion from mmCIF to RDKit Mol fails.""" + + +class UnsupportedMolBondError(Exception): + """Raised when we try to handle unsupported RDKit bonds.""" + + +def _populate_atoms_in_mol( + mol: rd_chem.Mol, + atom_names: Sequence[str], + atom_types: Sequence[str], + atom_charges: Sequence[int], + implicit_hydrogens: bool, + ligand_name: str, + atom_leaving_flags: Sequence[str], +): + """Populate the atoms of a Mol given atom features. + + Args: + mol: Mol object. + atom_names: Names of the atoms. + atom_types: Types of the atoms. + atom_charges: Charges of the atoms. + implicit_hydrogens: Whether to mark the atoms to allow implicit Hs. + ligand_name: Name of the ligand which the atoms are in. + atom_leaving_flags: Whether the atom is possibly a leaving atom. Values from + the CCD column `_chem_comp_atom.pdbx_leaving_atom_flag`. The expected + values are 'Y' (yes), 'N' (no), '?' (unknown/unset, interpreted as no). + + Raises: + ValueError: If atom type is invalid. + """ + # Map atom names to the position they will take in the rdkit molecule. + atom_name_to_idx = {name: i for i, name in enumerate(atom_names)} + + for atom_name, atom_type, atom_charge, atom_leaving_flag in zip( + atom_names, atom_types, atom_charges, atom_leaving_flags, strict=True + ): + try: + if atom_type == 'X': + atom_type = '*' + atom = rd_chem.Atom(atom_type) + except RuntimeError as e: + raise ValueError(f'Failed to use atom type: {str(e)}') from e + + if not implicit_hydrogens: + atom.SetNoImplicit(True) + + atom.SetProp('atom_name', atom_name) + atom.SetProp('atom_leaving_flag', atom_leaving_flag) + atom.SetFormalCharge(atom_charge) + residue_info = rd_chem.AtomPDBResidueInfo() + residue_info.SetName(_format_atom_name(atom_name, atom_type)) + residue_info.SetIsHeteroAtom(True) + residue_info.SetResidueName(ligand_name) + residue_info.SetResidueNumber(1) + atom.SetPDBResidueInfo(residue_info) + atom_index = mol.AddAtom(atom) + assert atom_index == atom_name_to_idx[atom_name] + + +def _populate_bonds_in_mol( + mol: rd_chem.Mol, + atom_names: Sequence[str], + bond_begins: Sequence[str], + bond_ends: Sequence[str], + bond_orders: Sequence[str], + bond_is_aromatics: Sequence[bool], +): + """Populate the bonds of a Mol given bond features. + + Args: + mol: Mol object. + atom_names: Names of atoms in the molecule. + bond_begins: Names of atoms at the beginning of the bond. + bond_ends: Names of atoms at the end of the bond. + bond_orders: What order the bonds are. + bond_is_aromatics: Whether the bonds are aromatic. + """ + atom_name_to_idx = {name: i for i, name in enumerate(atom_names)} + for begin, end, bond_type, is_aromatic in zip( + bond_begins, bond_ends, bond_orders, bond_is_aromatics, strict=True + ): + begin_name, end_name = atom_name_to_idx[begin], atom_name_to_idx[end] + bond_idx = mol.AddBond(begin_name, end_name, bond_type) + mol.GetBondWithIdx(bond_idx - 1).SetIsAromatic(is_aromatic) + + +def _sanitize_mol(mol, sort_alphabetically, remove_hydrogens) -> rd_chem.Mol: + # https://www.rdkit.org/docs/source/rdkit.Chem.rdmolops.html#rdkit.Chem.rdmolops.SanitizeMol + # Kekulize, check valencies, set aromaticity, conjugation and hybridization. + # This can repair e.g. incorrect aromatic flags. + rd_chem.SanitizeMol(mol) + if sort_alphabetically: + mol = sort_atoms_by_name(mol) + if remove_hydrogens: + mol = rd_chem.RemoveHs(mol) + return mol + + +def _add_conformer_to_mol(mol, conformer, force_parse) -> rd_chem.Mol: + # Create conformer and use it to assign stereochemistry. + if conformer is not None: + try: + mol.AddConformer(conformer) + rd_chem.AssignStereochemistryFrom3D(mol) + except ValueError as e: + logging.warning('Failed to parse conformer: %s', e) + if not force_parse: + raise + + +def mol_from_ccd_cif( + mol_cif: cif_dict.CifDict, + *, + force_parse: bool = False, + sort_alphabetically: bool = True, + remove_hydrogens: bool = True, + implicit_hydrogens: bool = False, +) -> rd_chem.Mol: + """Creates an rdkit Mol object from a CCD mmcif data block. + + The atoms are renumbered so that their names are in alphabetical order and + these names are placed on the atoms under property 'atom_name'. + Only hydrogens which are not required to define the molecule are removed. + For example, hydrogens that define stereochemistry around a double bond are + retained. + See this link for more details. + https://www.rdkit.org/docs/source/rdkit.Chem.rdmolops.html#rdkit.Chem.rdmolops.RemoveHs + + Args: + mol_cif: An mmcif object representing a molecule. + force_parse: If True, assumes missing aromatic flags are false, substitutes + deuterium for hydrogen, assumes missing charges are 0 and ignores missing + conformer / stereochemistry information. + sort_alphabetically: True: sort atom alphabetically; False: keep CCD order + remove_hydrogens: if True, remove non-important hydrogens + implicit_hydrogens: Sets a marker on the atom that allows implicit Hs. + + Returns: + An rdkit molecule, with the atoms sorted by name. + + Raises: + MolToMmcifError: If conversion from mmcif to rdkit Mol fails. More detailed + error is available as this error's cause. + """ + # Read data fields. + try: + atom_names, atom_types, atom_charges, atom_leaving_flags = parse_atom_data( + mol_cif, force_parse + ) + bond_begins, bond_ends, bond_orders, bond_is_aromatics = parse_bond_data( + mol_cif, force_parse + ) + lig_name = mol_cif['_chem_comp.id'][0].rjust(3) + except (KeyError, ValueError) as e: + raise MolFromMmcifError from e + + # Build Rdkit molecule. + mol = rd_chem.RWMol() + + # Per atom features. + try: + _populate_atoms_in_mol( + mol=mol, + atom_names=atom_names, + atom_types=atom_types, + atom_charges=atom_charges, + implicit_hydrogens=implicit_hydrogens, + ligand_name=lig_name, + atom_leaving_flags=atom_leaving_flags, + ) + except (ValueError, RuntimeError) as e: + raise MolFromMmcifError from e + + _populate_bonds_in_mol( + mol, atom_names, bond_begins, bond_ends, bond_orders, bond_is_aromatics + ) + + try: + conformer = _parse_ideal_conformer(mol_cif) + except (KeyError, ValueError) as e: + logging.warning('Failed to parse ideal conformer: %s', e) + if not force_parse: + raise MolFromMmcifError from e + conformer = None + + mol.UpdatePropertyCache(strict=False) + + try: + _add_conformer_to_mol(mol, conformer, force_parse) + mol = _sanitize_mol(mol, sort_alphabetically, remove_hydrogens) + except ( + ValueError, + rd_chem.KekulizeException, + rd_chem.AtomValenceException, + ) as e: + raise MolFromMmcifError from e + + return mol + + +def mol_to_ccd_cif( + mol: rd_chem.Mol, + component_id: str, + pdbx_smiles: str | None = None, + include_hydrogens: bool = True, +) -> cif_dict.CifDict: + """Creates a CCD-like mmcif data block from an rdkit Mol object. + + Only a subset of associated mmcif fields is populated, but that is + sufficient for further usage, e.g. in featurization code. + + Atom names can be specified via `atom_name` property. For atoms with + unspecified value of that property, the name is assigned based on element type + and the order in the Mol object. + + If the Mol object has associated conformers, atom positions from the first of + them will be populated in the resulting mmcif file. + + Args: + mol: An rdkit molecule. + component_id: Name of the molecule to use in the resulting mmcif. That is + equivalent to CCD code. + pdbx_smiles: If specified, the value will be used to populate + `_chem_comp.pdbx_smiles`. + include_hydrogens: Whether to include atom and bond data involving + hydrogens. + + Returns: + An mmcif data block corresponding for the given rdkit molecule. + + Raises: + UnsupportedMolBond: When a molecule contains a bond that can't be + represented with mmcif. + """ + mol = rd_chem.Mol(mol) + if include_hydrogens: + mol = rd_chem.AddHs(mol) + rd_chem.Kekulize(mol) + + if mol.GetNumConformers() > 0: + ideal_conformer = mol.GetConformer(0).GetPositions() + ideal_conformer = np.vectorize(lambda x: f'{x:.3f}')(ideal_conformer) + else: + # No data will be populated in the resulting mmcif if the molecule doesn't + # have any conformers attached to it. + ideal_conformer = None + + mol_cif = collections.defaultdict(list) + mol_cif['data_'] = [component_id] + mol_cif['_chem_comp.id'] = [component_id] + if pdbx_smiles: + mol_cif['_chem_comp.pdbx_smiles'] = [pdbx_smiles] + + mol = assign_atom_names_from_graph(mol, keep_existing_names=True) + + for atom_idx, atom in enumerate(mol.GetAtoms()): + element = atom.GetSymbol() + if not include_hydrogens and element in ('H', 'D'): + continue + + mol_cif['_chem_comp_atom.comp_id'].append(component_id) + mol_cif['_chem_comp_atom.atom_id'].append(atom.GetProp('atom_name')) + mol_cif['_chem_comp_atom.type_symbol'].append(atom.GetSymbol().upper()) + mol_cif['_chem_comp_atom.charge'].append(str(atom.GetFormalCharge())) + if ideal_conformer is not None: + coords = ideal_conformer[atom_idx] + mol_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'].append( + coords[0]) + mol_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'].append( + coords[1]) + mol_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'].append( + coords[2]) + + for bond in mol.GetBonds(): + atom1 = bond.GetBeginAtom() + atom2 = bond.GetEndAtom() + if not include_hydrogens and ( + atom1.GetSymbol() in ('H', 'D') or atom2.GetSymbol() in ('H', 'D') + ): + continue + mol_cif['_chem_comp_bond.comp_id'].append(component_id) + mol_cif['_chem_comp_bond.atom_id_1'].append( + bond.GetBeginAtom().GetProp('atom_name') + ) + mol_cif['_chem_comp_bond.atom_id_2'].append( + bond.GetEndAtom().GetProp('atom_name') + ) + try: + bond_type = bond.GetBondType() + # Older versions of RDKit did not have a DATIVE bond type. Convert it to + # SINGLE to match the AF3 training setup. + if bond_type == rd_chem.BondType.DATIVE: + bond_type = rd_chem.BondType.SINGLE + mol_cif['_chem_comp_bond.value_order'].append( + _RDKIT_BOND_TYPE_TO_MMCIF[bond_type] + ) + mol_cif['_chem_comp_bond.pdbx_stereo_config'].append( + _RDKIT_BOND_STEREO_TO_MMCIF[bond.GetStereo()] + ) + except KeyError as e: + raise UnsupportedMolBondError from e + mol_cif['_chem_comp_bond.pdbx_aromatic_flag'].append( + 'Y' if bond.GetIsAromatic() else 'N' + ) + + return cif_dict.CifDict(mol_cif) + + +def _format_atom_name(atom_name: str, atom_type: str) -> str: + """Formats an atom name to fit in the four characters specified in PDB. + + See for example the following note on atom name formatting in PDB files: + https://www.cgl.ucsf.edu/chimera/docs/UsersGuide/tutorials/pdbintro.html#note1 + + Args: + atom_name: The unformatted atom name. + atom_type: The atom element symbol. + + Returns: + formatted_atom_name: The formatted 4-character atom name. + """ + atom_name = atom_name.strip() + atom_type = atom_type.strip().upper() + if len(atom_name) == 1: + return atom_name.rjust(2).ljust(4) + elif len(atom_name) == 2: + if atom_name == atom_type: + return atom_name.ljust(4) + return atom_name.center(4) + elif len(atom_name) == 3: + if atom_name[:2] == atom_type: + return atom_name.ljust(4) + return atom_name.rjust(4) + elif len(atom_name) == 4: + return atom_name + else: + raise ValueError( + f'Atom name `{atom_name}` has more than four characters ' + 'or is an empty string.' + ) + + +def parse_atom_data( + mol_cif: cif_dict.CifDict | Mapping[str, Sequence[str]], force_parse: bool +) -> tuple[Sequence[str], Sequence[str], Sequence[int], Sequence[str]]: + """Parses atoms. If force_parse is True, fix deuterium and missing charge.""" + atom_types = [t.capitalize() + for t in mol_cif['_chem_comp_atom.type_symbol']] + atom_names = mol_cif['_chem_comp_atom.atom_id'] + atom_charges = mol_cif['_chem_comp_atom.charge'] + atom_leaving_flags = ['?'] * len(atom_names) + if '_chem_comp_atom.pdbx_leaving_atom_flag' in mol_cif: + atom_leaving_flags = mol_cif['_chem_comp_atom.pdbx_leaving_atom_flag'] + + if force_parse: + # Replace missing charges with 0. + atom_charges = [charge if charge != + '?' else '0' for charge in atom_charges] + # Deuterium for hydrogen. + atom_types = [type_ if type_ != 'D' else 'H' for type_ in atom_types] + + atom_charges = [int(atom_charge) for atom_charge in atom_charges] + return atom_names, atom_types, atom_charges, atom_leaving_flags + + +def parse_bond_data( + mol_cif: cif_dict.CifDict | Mapping[str, Sequence[str]], force_parse: bool +) -> tuple[ + Sequence[str], Sequence[str], Sequence[rd_chem.BondType], Sequence[bool] +]: + """Parses bond data. If force_parse is True, ignore missing aromatic flags.""" + # The bond table isn't present if there are no bonds. Use [] in that case. + begin_atoms = mol_cif.get('_chem_comp_bond.atom_id_1', []) + end_atoms = mol_cif.get('_chem_comp_bond.atom_id_2', []) + orders = mol_cif.get('_chem_comp_bond.value_order', []) + bond_types = [_RDKIT_MMCIF_TO_BOND_TYPE[order] for order in orders] + + try: + aromatic_flags = mol_cif.get('_chem_comp_bond.pdbx_aromatic_flag', []) + is_aromatic = [{'Y': True, 'N': False}[flag] + for flag in aromatic_flags] + except KeyError: + if force_parse: + # Set them all to not aromatic. + is_aromatic = [False for _ in begin_atoms] + else: + raise + + return begin_atoms, end_atoms, bond_types, is_aromatic + + +def _parse_ideal_conformer(mol_cif: cif_dict.CifDict) -> rd_chem.Conformer: + """Builds a conformer containing the ideal coordinates from the CCD. + + Args: + mol_cif: An mmcif object representing a molecule. + + Returns: + An rdkit conformer filled with the ideal positions from the mmcif. + + Raises: + ValueError: if the positions can't be interpreted. + """ + atom_x = [ + float(x) for x in mol_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'] + ] + atom_y = [ + float(y) for y in mol_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'] + ] + atom_z = [ + float(z) for z in mol_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'] + ] + atom_positions = zip(atom_x, atom_y, atom_z, strict=True) + + conformer = rd_chem.Conformer(len(atom_x)) + for atom_index, atom_position in enumerate(atom_positions): + conformer.SetAtomPosition(atom_index, atom_position) + + return conformer + + +def sort_atoms_by_name(mol: rd_chem.Mol) -> rd_chem.Mol: + """Sorts the atoms in the molecule by their names.""" + atom_names = { + atom.GetProp('atom_name'): atom.GetIdx() for atom in mol.GetAtoms() + } + + # Sort the name, int tuples by the names. + sorted_atom_names = sorted(atom_names.items()) + + # Zip these tuples back together to the sorted indices. + _, new_order = zip(*sorted_atom_names, strict=True) + + # Reorder the molecule. + # new_order is effectively an argsort of the names. + return rd_chem.RenumberAtoms(mol, new_order) + + +def assign_atom_names_from_graph( + mol: rd_chem.Mol, + keep_existing_names: bool = False, +) -> rd_chem.Mol: + """Assigns atom names from the molecular graph. + + The atom name is stored as an atom property 'atom_name', accessible + with atom.GetProp('atom_name'). If the property is already specified, and + keep_existing_names is True we keep the original name. + + We traverse the graph in the order of the rdkit atom index and give each atom + a name equal to '{ELEMENT_TYPE}_{INDEX}'. E.g. C5 is the name for the fifth + unnamed carbon encountered. + + NOTE: A new mol is returned, the original is not changed in place. + + Args: + mol: + keep_existing_names: If True, atoms that already have the atom_name property + will keep their assigned names. + + Returns: + A new mol, with potentially new 'atom_name' properties. + """ + mol = rd_chem.Mol(mol) + + specified_atom_names = { + atom.GetProp('atom_name') + for atom in mol.GetAtoms() + if atom.HasProp('atom_name') and keep_existing_names + } + + element_counts = collections.Counter() + for atom in mol.GetAtoms(): + if not atom.HasProp('atom_name') or not keep_existing_names: + element = atom.GetSymbol() + while True: + element_counts[element] += 1 + new_name = f'{element}{element_counts[element]}' + if new_name not in specified_atom_names: + break + atom.SetProp('atom_name', new_name) + + return mol diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/subprocess_utils.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/subprocess_utils.py new file mode 100644 index 000000000..15aa61ba5 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/subprocess_utils.py @@ -0,0 +1,108 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Helper functions for launching external tools.""" + +from collections.abc import Sequence +import os +import subprocess +import time +from typing import Any + +from absl import logging + + +def create_query_fasta_file(sequence: str, path: str, linewidth: int = 80): + """Creates a fasta file with the sequence with line width limit.""" + with open(path, 'w') as f: + f.write('>query\n') + + i = 0 + while i < len(sequence): + f.write(f'{sequence[i:(i + linewidth)]}\n') + i += linewidth + + +def check_binary_exists(path: str, name: str) -> None: + """Checks if a binary exists on the given path and raises otherwise.""" + if not os.path.exists(path): + raise RuntimeError(f'{name} binary not found at {path}') + + +def run( + cmd: Sequence[str], + cmd_name: str, + log_on_process_error: bool = False, + log_stderr: bool = False, + log_stdout: bool = False, + max_out_streams_len: int | None = 500_000, + **run_kwargs, +) -> subprocess.CompletedProcess[Any]: + """Launches a subprocess, times it, and checks for errors. + + Args: + cmd: Command to launch. + cmd_name: Human-readable command name to be used in logs. + log_on_process_error: Whether to use `logging.error` to log the process' + stderr on failure. + log_stderr: Whether to log the stderr of the command. + log_stdout: Whether to log the stdout of the command. + max_out_streams_len: Max length of prefix of stdout and stderr included in + the exception message. Set to `None` to disable truncation. + **run_kwargs: Any other kwargs for `subprocess.run`. + + Returns: + The completed process object. + + Raises: + RuntimeError: if the process completes with a non-zero return code. + """ + + logging.info('Launching subprocess "%s"', ' '.join(cmd)) + + start_time = time.time() + try: + completed_process = subprocess.run( + cmd, + check=True, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE, + text=True, + **run_kwargs, + ) + except subprocess.CalledProcessError as e: + if log_on_process_error: + # Logs have a 15k character limit, so log the error line by line. + logging.error('%s failed. %s stderr begin:', cmd_name, cmd_name) + for error_line in e.stderr.splitlines(): + if stripped_error_line := error_line.strip(): + logging.error(stripped_error_line) + logging.error('%s stderr end.', cmd_name) + + error_msg = ( + f'{cmd_name} failed' + f'\nstdout:\n{e.stdout[:max_out_streams_len]}\n' + f'\nstderr:\n{e.stderr[:max_out_streams_len]}' + ) + raise RuntimeError(error_msg) from e + end_time = time.time() + + logging.info('Finished %s in %.3f seconds', + cmd_name, end_time - start_time) + stdout, stderr = completed_process.stdout, completed_process.stderr + + if log_stdout and stdout: + logging.info('%s stdout:\n%s', cmd_name, stdout) + + if log_stderr and stderr: + logging.info('%s stderr:\n%s', cmd_name, stderr) + + return completed_process diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/atom_layout/atom_layout.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/atom_layout/atom_layout.py new file mode 100644 index 000000000..4bb172eec --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/atom_layout/atom_layout.py @@ -0,0 +1,1194 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Helper functions for different atom layouts and conversion between them.""" + +import collections +from collections.abc import Mapping, Sequence +import math +import dataclasses +import types +from typing import Any, TypeAlias + +import numpy as np +import mindspore as ms +from mindspore import ops +from rdkit import Chem + +from alphafold3 import structure +from alphafold3.constants import atom_types +from alphafold3.constants import chemical_component_sets +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.structure import chemical_components as struc_chem_comps + + +xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name +NumpyIndex: TypeAlias = Any + + +def _assign_atom_names_from_graph(mol: Chem.Mol) -> Chem.Mol: + """Assigns atom names from the molecular graph. + + The atom name is stored as an atom property 'atom_name', accessible with + atom.GetProp('atom_name'). If the property is already specified, we keep the + original name. + + We traverse the graph in the order of the rdkit atom index and give each atom + a name equal to '{ELEMENT_TYPE}_{INDEX}'. E.g. C5 is the name for the fifth + unnamed carbon encountered. + + NOTE: A new mol is returned, the original is not changed in place. + + Args: + mol: RDKit molecule. + + Returns: + A new mol, with potentially new 'atom_name' properties. + """ + mol = Chem.Mol(mol) + + specified_atom_names = { + a.GetProp('atom_name') for a in mol.GetAtoms() if a.HasProp('atom_name') + } + + element_counts = collections.Counter() + for atom in mol.GetAtoms(): + if not atom.HasProp('atom_name'): + element = atom.GetSymbol() + while True: + element_counts[element] += 1 + new_name = f'{element}{element_counts[element]}' + if new_name not in specified_atom_names: + break + atom.SetProp('atom_name', new_name) + + return mol + + +@dataclasses.dataclass(frozen=True) +class AtomLayout: + """Atom layout in a fixed shape (usually 1-dim or 2-dim). + + Examples for atom layouts are atom37, atom14, and similar. + All members are np.ndarrays with the same shape, e.g. + - [num_atoms] + - [num_residues, max_atoms_per_residue] + - [num_fragments, max_fragments_per_residue] + All string arrays should have dtype=object to avoid pitfalls with Numpy's + fixed-size strings + + Attributes: + atom_name: np.ndarray of str: atom names (e.g. 'CA', 'NE2'), padding + elements have an empty string (''), None or any other value, that maps to + False for .astype(bool). mmCIF field: _atom_site.label_atom_id. + res_id: np.ndarray of int: residue index (usually starting from 1) padding + elements can have an arbitrary value. mmCIF field: + _atom_site.label_seq_id. + chain_id: np.ndarray of str: chain names (e.g. 'A', 'B') padding elements + can have an arbitrary value. mmCIF field: _atom_site.label_seq_id. + atom_element: np.ndarray of str: atom elements (e.g. 'C', 'N', 'O'), padding + elements have an empty string (''), None or any other value, that maps to + False for .astype(bool). mmCIF field: _atom_site.type_symbol. + res_name: np.ndarray of str: residue names (e.g. 'ARG', 'TRP') padding + elements can have an arbitrary value. mmCIF field: + _atom_site.label_comp_id. + chain_type: np.ndarray of str: chain types (e.g. 'polypeptide(L)'). padding + elements can have an arbitrary value. mmCIF field: _entity_poly.type OR + _entity.type (for non-polymers). + shape: shape of the layout (just returns atom_name.shape) + """ + + atom_name: np.ndarray + res_id: np.ndarray + chain_id: np.ndarray + atom_element: np.ndarray | None = None + res_name: np.ndarray | None = None + chain_type: np.ndarray | None = None + + def __post_init__(self): + """Assert all arrays have the same shape.""" + attribute_names = ( + 'atom_name', + 'atom_element', + 'res_name', + 'res_id', + 'chain_id', + 'chain_type', + ) + _assert_all_arrays_have_same_shape( + obj=self, + expected_shape=self.atom_name.shape, + attribute_names=attribute_names, + ) + # atom_name must have dtype object, such that we can convert it to bool to + # obtain the mask + if self.atom_name.dtype != object: + raise ValueError( + 'atom_name must have dtype object, such that it can ' + 'be converted converted to bool to obtain the mask' + ) + + def __getitem__(self, key: NumpyIndex) -> 'AtomLayout': + return AtomLayout( + atom_name=self.atom_name[key], + res_id=self.res_id[key], + chain_id=self.chain_id[key], + atom_element=( + self.atom_element[key] if self.atom_element is not None else None + ), + res_name=(self.res_name[key] + if self.res_name is not None else None), + chain_type=( + self.chain_type[key] if self.chain_type is not None else None + ), + ) + + def __eq__(self, other: 'AtomLayout') -> bool: + if not np.array_equal(self.atom_name, other.atom_name): + return False + + mask = self.atom_name.astype(bool) + # Check essential fields. + for field in ('res_id', 'chain_id'): + my_arr = getattr(self, field) + other_arr = getattr(other, field) + if not np.array_equal(my_arr[mask], other_arr[mask]): + return False + + # Check optional fields. + for field in ('atom_element', 'res_name', 'chain_type'): + my_arr = getattr(self, field) + other_arr = getattr(other, field) + if ( + my_arr is not None + and other_arr is not None + and not np.array_equal(my_arr[mask], other_arr[mask]) + ): + return False + + return True + + def copy_and_pad_to(self, shape: tuple[int, ...]) -> 'AtomLayout': + """Copies and pads the layout to the requested shape. + + Args: + shape: new shape for the atom layout + + Returns: + a copy of the atom layout padded to the requested shape + + Raises: + ValueError: incompatible shapes. + """ + if len(shape) != len(self.atom_name.shape): + raise ValueError( + f'Incompatible shape {shape}. Current layout has shape {self.shape}.' + ) + if any(new < old for old, new in zip(self.atom_name.shape, shape)): + raise ValueError( + "Can't pad to a smaller shape. Current layout has shape " + f'{self.shape} and you requested shape {shape}.' + ) + pad_width = [ + (0, new - old) for old, new in zip(self.atom_name.shape, shape) + ] + pad_val = np.array('', dtype=object) + return AtomLayout( + atom_name=np.pad(self.atom_name, pad_width, + constant_values=pad_val), + res_id=np.pad(self.res_id, pad_width, constant_values=0), + chain_id=np.pad(self.chain_id, pad_width, constant_values=pad_val), + atom_element=( + np.pad(self.atom_element, pad_width, constant_values=pad_val) + if self.atom_element is not None + else None + ), + res_name=( + np.pad(self.res_name, pad_width, constant_values=pad_val) + if self.res_name is not None + else None + ), + chain_type=( + np.pad(self.chain_type, pad_width, constant_values=pad_val) + if self.chain_type is not None + else None + ), + ) + + def to_array(self) -> np.ndarray: + """Stacks the fields to a numpy array with shape (6, ). + + Creates a pure numpy array of type `object` by stacking the 6 fields of the + AtomLayout, i.e. (atom_name, atom_element, res_name, res_id, chain_id, + chain_type). This method together with from_array() provides an easy way to + apply pure numpy methods like np.concatenate() to `AtomLayout`s. + + Returns: + np.ndarray of object with shape (6, ), e.g. + array([['N', 'CA', 'C', ..., 'CB', 'CG', 'CD'], + ['N', 'C', 'C', ..., 'C', 'C', 'C'], + ['LEU', 'LEU', 'LEU', ..., 'PRO', 'PRO', 'PRO'], + [1, 1, 1, ..., 403, 403, 403], + ['A', 'A', 'A', ..., 'D', 'D', 'D'], + ['polypeptide(L)', 'polypeptide(L)', ..., 'polypeptide(L)']], + dtype=object) + """ + if ( + self.atom_element is None + or self.res_name is None + or self.chain_type is None + ): + raise ValueError('All optional fields need to be present.') + + return np.stack(dataclasses.astuple(self), axis=0) + + @classmethod + def from_array(cls, arr: np.ndarray) -> 'AtomLayout': + """Creates an AtomLayout object from a numpy array with shape (6, ...). + + see also to_array() + Args: + arr: np.ndarray of object with shape (6, ) + + Returns: + AtomLayout object with shape () + """ + if arr.shape[0] != 6: + raise ValueError( + 'Given array must have shape (6, ...) to match the 6 fields of ' + 'AtomLayout (atom_name, atom_element, res_name, res_id, chain_id, ' + f'chain_type). Your array has {arr.shape=}' + ) + return cls(*arr) + + @property + def shape(self) -> tuple[int, ...]: + return self.atom_name.shape + + +@dataclasses.dataclass(frozen=True) +class Residues: + """List of residues with meta data. + + Attributes: + res_name: np.ndarray of str [num_res], e.g. 'ARG', 'TRP' + res_id: np.ndarray of int [num_res] + chain_id: np.ndarray of str [num_res], e.g. 'A', 'B' + chain_type: np.ndarray of str [num_res], e.g. 'polypeptide(L)' + is_start_terminus: np.ndarray of bool [num_res] + is_end_terminus: np.ndarray of bool [num_res] + deprotonation: (optional) np.ndarray of set() [num_res], e.g. {'HD1', 'HE2'} + smiles_string: (optional) np.ndarray of str [num_res], e.g. 'Cc1ccccc1' + shape: shape of the layout (just returns res_name.shape) + """ + + res_name: np.ndarray + res_id: np.ndarray + chain_id: np.ndarray + chain_type: np.ndarray + is_start_terminus: np.ndarray + is_end_terminus: np.ndarray + deprotonation: np.ndarray | None = None + smiles_string: np.ndarray | None = None + + def __post_init__(self): + """Assert all arrays are 1D have the same shape.""" + attribute_names = ( + 'res_name', + 'res_id', + 'chain_id', + 'chain_type', + 'is_start_terminus', + 'is_end_terminus', + 'deprotonation', + 'smiles_string', + ) + _assert_all_arrays_have_same_shape( + obj=self, + expected_shape=(self.res_name.shape[0],), + attribute_names=attribute_names, + ) + + def __getitem__(self, key: NumpyIndex) -> 'Residues': + return Residues( + res_name=self.res_name[key], + res_id=self.res_id[key], + chain_id=self.chain_id[key], + chain_type=self.chain_type[key], + is_start_terminus=self.is_start_terminus[key], + is_end_terminus=self.is_end_terminus[key], + deprotonation=( + self.deprotonation[key] if self.deprotonation is not None else None + ), + smiles_string=( + self.smiles_string[key] if self.smiles_string is not None else None + ), + ) + + def __eq__(self, other: 'Residues') -> bool: + return all( + np.array_equal(getattr(self, field.name), + getattr(other, field.name)) + for field in dataclasses.fields(self) + ) + + @property + def shape(self) -> tuple[int, ...]: + return self.res_name.shape + + +@dataclasses.dataclass # (frozen=True) +class GatherInfo: + """Gather indices to translate from one atom layout to another. + + All members are np or jnp ndarray (usually 1-dim or 2-dim) with the same + shape, e.g. + - [num_atoms] + - [num_residues, max_atoms_per_residue] + - [num_fragments, max_fragments_per_residue] + + Attributes: + gather_idxs: np or jnp ndarray of int: gather indices into a flattened array + gather_mask: np or jnp ndarray of bool: mask for resulting array + input_shape: np or jnp ndarray of int: the shape of the unflattened input + array + shape: output shape. Just returns gather_idxs.shape + """ + + gather_idxs: ms.Tensor + gather_mask: ms.Tensor + input_shape: ms.Tensor + + def __post_init__(self): + if self.gather_mask.shape != self.gather_idxs.shape: + raise ValueError( + 'All arrays must have the same shape. Got\n' + f'gather_idxs.shape = {self.gather_idxs.shape}\n' + f'gather_mask.shape = {self.gather_mask.shape}\n' + ) + + def __getitem__(self, key: NumpyIndex) -> 'GatherInfo': + return GatherInfo( + gather_idxs=self.gather_idxs[key], + gather_mask=self.gather_mask[key], + input_shape=self.input_shape, + ) + + @property + def shape(self) -> tuple[int, ...]: + return self.gather_idxs.shape + + def as_np_or_jnp(self, xnp: types.ModuleType) -> 'GatherInfo': + return GatherInfo( + gather_idxs=xnp.array(self.gather_idxs), + gather_mask=xnp.array(self.gather_mask), + input_shape=xnp.array(self.input_shape), + ) + + def as_dict( + self, + key_prefix: str | None = None, + ) -> dict[str, xnp_ndarray]: + prefix = f'{key_prefix}:' if key_prefix else '' + return { + prefix + 'gather_idxs': self.gather_idxs, + prefix + 'gather_mask': self.gather_mask, + prefix + 'input_shape': self.input_shape, + } + + @classmethod + def from_dict( + cls, + d: Mapping[str, xnp_ndarray], + key_prefix: str | None = None, + ) -> 'GatherInfo': + """Creates GatherInfo from a given dictionary.""" + prefix = f'{key_prefix}:' if key_prefix else '' + return cls( + gather_idxs=d[prefix + 'gather_idxs'], + gather_mask=d[prefix + 'gather_mask'], + input_shape=d[prefix + 'input_shape'], + ) + + +def fill_in_optional_fields( + minimal_atom_layout: AtomLayout, + reference_atoms: AtomLayout, +) -> AtomLayout: + """Fill in the optional fields (atom_element, res_name, chain_type). + + Extracts the optional fields (atom_element, res_name, chain_type) from a + flat reference layout and fills them into the fields from this layout. + + Args: + minimal_atom_layout: An AtomLayout that only contains the essential fields + (atom_name, res_id, chain_id). + reference_atoms: A flat layout that contains all fields for all atoms. + + Returns: + An AtomLayout that contains all fields. + + Raises: + ValueError: Reference atoms layout is not flat. + ValueError: Missing atoms in reference. + """ + if len(reference_atoms.shape) > 1: + raise ValueError('Only flat layouts are supported as reference.') + ref_to_self = compute_gather_idxs( + source_layout=reference_atoms, target_layout=minimal_atom_layout + ) + atom_mask = minimal_atom_layout.atom_name.astype(bool) + missing_atoms_mask = atom_mask & ~ref_to_self.gather_mask + if np.any(missing_atoms_mask): + raise ValueError( + f'{np.sum(missing_atoms_mask)} missing atoms in reference: ' + f'{minimal_atom_layout[missing_atoms_mask]}' + ) + + def _convert_str_array(gather: GatherInfo, arr: np.ndarray): + output = arr[gather.gather_idxs] + output[~gather.gather_mask] = '' + return output + + return dataclasses.replace( + minimal_atom_layout, + atom_element=_convert_str_array( + ref_to_self, reference_atoms.atom_element + ), + res_name=_convert_str_array(ref_to_self, reference_atoms.res_name), + chain_type=_convert_str_array(ref_to_self, reference_atoms.chain_type), + ) + + +def guess_deprotonation(residues: Residues) -> Residues: + """Convenience function to create a plausible deprotonation field. + + Assumes a pH of 7 and always prefers HE2 over HD1 for HIS. + Args: + residues: a Residues object without a depronotation field + + Returns: + a Residues object with a depronotation field + """ + num_residues = residues.res_name.shape[0] + deprotonation = np.empty(num_residues, dtype=object) + deprotonation_at_ph7 = { + 'ASP': 'HD2', + 'GLU': 'HE2', + 'HIS': 'HD1', + } + for idx, res_name in enumerate(residues.res_name): + deprotonation[idx] = set() + if res_name in deprotonation_at_ph7: + deprotonation[idx].add(deprotonation_at_ph7[res_name]) + if residues.is_end_terminus[idx]: + deprotonation[idx].add('HXT') + + return dataclasses.replace(residues, deprotonation=deprotonation) + + +def atom_layout_from_structure( + struct: structure.Structure, + *, + fix_non_standard_polymer_res: bool = False, +) -> AtomLayout: + """Extract AtomLayout from a Structure.""" + + if not fix_non_standard_polymer_res: + return AtomLayout( + atom_name=np.array(struct.atom_name, dtype=object), + atom_element=np.array(struct.atom_element, dtype=object), + res_name=np.array(struct.res_name, dtype=object), + res_id=np.array(struct.res_id, dtype=int), + chain_id=np.array(struct.chain_id, dtype=object), + chain_type=np.array(struct.chain_type, dtype=object), + ) + + # Target lists. + target_atom_names = [] + target_atom_elements = [] + target_res_ids = [] + target_res_names = [] + target_chain_ids = [] + target_chain_types = [] + + for atom in struct.iter_atoms(): + target_atom_names.append(atom['atom_name']) + target_atom_elements.append(atom['atom_element']) + target_res_ids.append(atom['res_id']) + target_chain_ids.append(atom['chain_id']) + target_chain_types.append(atom['chain_type']) + if mmcif_names.is_standard_polymer_type(atom['chain_type']): + fixed_res_name = mmcif_names.fix_non_standard_polymer_res( + res_name=atom['res_name'], chain_type=atom['chain_type'] + ) + target_res_names.append(fixed_res_name) + else: + target_res_names.append(atom['res_name']) + + return AtomLayout( + atom_name=np.array(target_atom_names, dtype=object), + atom_element=np.array(target_atom_elements, dtype=object), + res_name=np.array(target_res_names, dtype=object), + res_id=np.array(target_res_ids, dtype=int), + chain_id=np.array(target_chain_ids, dtype=object), + chain_type=np.array(target_chain_types, dtype=object), + ) + + +def residues_from_structure( + struct: structure.Structure, + *, + include_missing_residues: bool = True, + fix_non_standard_polymer_res: bool = False, +) -> Residues: + """Create a Residues object from a Structure object.""" + + def _get_smiles(res_name): + """Get SMILES string from chemical components.""" + smiles = None + if ( + struct.chemical_components_data is not None + and struct.chemical_components_data.chem_comp is not None + and struct.chemical_components_data.chem_comp.get(res_name) + ): + smiles = struct.chemical_components_data.chem_comp[res_name].pdbx_smiles + return smiles + + res_names_per_chain = struct.chain_res_name_sequence( + include_missing_residues=include_missing_residues, + fix_non_standard_polymer_res=fix_non_standard_polymer_res, + ) + res_name = [] + res_id = [] + chain_id = [] + chain_type = [] + smiles = [] + is_start_terminus = [] + for c in struct.iter_chains(): + if include_missing_residues: + this_res_ids = [ + id for (_, id) in struct.all_residues[c['chain_id']]] + else: + this_res_ids = [ + r['res_id'] + for r in struct.iter_residues() + if r['chain_id'] == c['chain_id'] + ] + fixed_res_names = res_names_per_chain[c['chain_id']] + assert len(this_res_ids) == len( + fixed_res_names + ), f'{len(this_res_ids)} != {len(fixed_res_names)}' + this_start_res_id = min(min(this_res_ids), 1) + this_is_start_terminus = [r == this_start_res_id for r in this_res_ids] + smiles.extend([_get_smiles(res_name) for res_name in fixed_res_names]) + num_res = len(fixed_res_names) + res_name.extend(fixed_res_names) + res_id.extend(this_res_ids) + chain_id.extend([c['chain_id']] * num_res) + chain_type.extend([c['chain_type']] * num_res) + is_start_terminus.extend(this_is_start_terminus) + res_name = np.array(res_name, dtype=object) + res_id = np.array(res_id, dtype=int) + chain_id = np.array(chain_id, dtype=object) + chain_type = np.array(chain_type, dtype=object) + smiles = np.array(smiles, dtype=object) + is_start_terminus = np.array(is_start_terminus, dtype=bool) + + res_uid_to_idx = { + uid: idx for idx, uid in enumerate(zip(chain_id, res_id, strict=True)) + } + + # Start terminus indicates whether residue index is 1 and chain is polymer. + is_polymer = np.isin(chain_type, tuple(mmcif_names.POLYMER_CHAIN_TYPES)) + is_start_terminus = is_start_terminus & is_polymer + + # Start also indicates whether amino acid is attached to H2 or proline to H. + start_terminus_atom_index = np.nonzero( + (struct.chain_type == mmcif_names.PROTEIN_CHAIN) + & ( + (struct.atom_name == 'H2') + | ((struct.atom_name == 'H') & (struct.res_name == 'PRO')) + ) + )[0] + + # Translate atom idx to residue idx to assign start terminus. + for atom_idx in start_terminus_atom_index: + res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx]) + res_idx = res_uid_to_idx[res_uid] + is_start_terminus[res_idx] = True + + # Infer end terminus: Check for OXT, or in case of + # include_missing_residues==True for the last residue of the chain. + num_all_residues = res_name.shape[0] + is_end_terminus = np.zeros(num_all_residues, dtype=bool) + end_term_atom_idxs = np.nonzero(struct.atom_name == 'OXT')[0] + for atom_idx in end_term_atom_idxs: + res_uid = (struct.chain_id[atom_idx], struct.res_id[atom_idx]) + res_idx = res_uid_to_idx[res_uid] + is_end_terminus[res_idx] = True + + if include_missing_residues: + for idx in range(num_all_residues - 1): + if is_polymer[idx] and chain_id[idx] != chain_id[idx + 1]: + is_end_terminus[idx] = True + if (num_all_residues > 0) and is_polymer[-1]: + is_end_terminus[-1] = True + + # Infer (de-)protonation: Only if hydrogens are given. + num_hydrogens = np.sum( + (struct.atom_element == 'H') & (struct.chain_type == 'polypeptide(L)') + ) + if num_hydrogens > 0: + deprotonation = np.empty(num_all_residues, dtype=object) + all_atom_uids = set( + zip(struct.chain_id, struct.res_id, struct.atom_name, strict=True) + ) + for idx in range(num_all_residues): + deprotonation[idx] = set() + check_hydrogens = set() + if is_end_terminus[idx]: + check_hydrogens.add('HXT') + if res_name[idx] in atom_types.PROTONATION_HYDROGENS: + check_hydrogens.update( + atom_types.PROTONATION_HYDROGENS[res_name[idx]]) + for hydrogen in check_hydrogens: + if (chain_id[idx], res_id[idx], hydrogen) not in all_atom_uids: + deprotonation[idx].add(hydrogen) + else: + deprotonation = None + + return Residues( + res_name=res_name, + res_id=res_id, + chain_id=chain_id, + chain_type=chain_type, + is_start_terminus=is_start_terminus.astype(bool), + is_end_terminus=is_end_terminus, + deprotonation=deprotonation, + smiles_string=smiles, + ) + + +def get_link_drop_atoms( + res_name: str, + chain_type: str, + *, + is_start_terminus: bool, + is_end_terminus: bool, + bonded_atoms: set[str], + drop_ligand_leaving_atoms: bool = False, +) -> set[str]: + """Returns set of atoms that are dropped when this res_name gets linked. + + Args: + res_name: residue name, e.g. 'ARG' + chain_type: chain_type, e.g. 'polypeptide(L)' + is_start_terminus: whether the residue is the n-terminus + is_end_terminus: whether the residue is the c-terminus + bonded_atoms: Names of atoms coming off this residue. + drop_ligand_leaving_atoms: Flag to switch on/off leaving atoms for ligands. + + Returns: + Set of atoms that are dropped when this amino acid gets linked. + """ + drop_atoms = set() + if chain_type == mmcif_names.PROTEIN_CHAIN: + if res_name == 'PRO': + if not is_start_terminus: + drop_atoms.update({'H', 'H2', 'H3'}) + if not is_end_terminus: + drop_atoms.update({'OXT', 'HXT'}) + else: + if not is_start_terminus: + drop_atoms.update({'H2', 'H3'}) + if not is_end_terminus: + drop_atoms.update({'OXT', 'HXT'}) + elif chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: + if not is_start_terminus: + drop_atoms.update({'OP3'}) + elif ( + drop_ligand_leaving_atoms and chain_type in mmcif_names.LIGAND_CHAIN_TYPES + ): + if res_name in { + *chemical_component_sets.GLYCAN_OTHER_LIGANDS, + *chemical_component_sets.GLYCAN_LINKING_LIGANDS, + }: + if 'O1' not in bonded_atoms: + drop_atoms.update({'O1'}) + return drop_atoms + + +def get_bonded_atoms( + polymer_ligand_bonds: AtomLayout, + ligand_ligand_bonds: AtomLayout, + res_id: int, + chain_id: str, +) -> set[str]: + """Finds the res_name on the opposite end of the bond, if a bond exists. + + Args: + polymer_ligand_bonds: Bond information for polymer-ligand pairs. + ligand_ligand_bonds: Bond information for ligand-ligand pairs. + res_id: residue id in question. + chain_id: chain id of residue in question. + + Returns: + res_name of bonded atom. + """ + bonded_atoms = set() + if polymer_ligand_bonds: + # Filter before searching to speed this up. + bond_idx = np.logical_and( + polymer_ligand_bonds.res_id == res_id, + polymer_ligand_bonds.chain_id == chain_id, + ).any(axis=1) + relevant_polymer_bonds = polymer_ligand_bonds[bond_idx] + for atom_names, res_ids, chain_ids in zip( + relevant_polymer_bonds.atom_name, + relevant_polymer_bonds.res_id, + relevant_polymer_bonds.chain_id, + ): + if (res_ids[0], chain_ids[0]) == (res_id, chain_id): + bonded_atoms.add(atom_names[0]) + elif (res_ids[1], chain_ids[1]) == (res_id, chain_id): + bonded_atoms.add(atom_names[1]) + if ligand_ligand_bonds: + bond_idx = np.logical_and( + ligand_ligand_bonds.res_id == res_id, + ligand_ligand_bonds.chain_id == chain_id, + ).any(axis=1) + relevant_ligand_bonds = ligand_ligand_bonds[bond_idx] + for atom_names, res_ids, chain_ids in zip( + relevant_ligand_bonds.atom_name, + relevant_ligand_bonds.res_id, + relevant_ligand_bonds.chain_id, + ): + if (res_ids[0], chain_ids[0]) == (res_id, chain_id): + bonded_atoms.add(atom_names[0]) + elif (res_ids[1], chain_ids[1]) == (res_id, chain_id): + bonded_atoms.add(atom_names[1]) + return bonded_atoms + + +def make_flat_atom_layout( + residues: Residues, + ccd: chemical_components.Ccd, + polymer_ligand_bonds: AtomLayout | None = None, + ligand_ligand_bonds: AtomLayout | None = None, + *, + with_hydrogens: bool = False, + skip_unk_residues: bool = True, + drop_ligand_leaving_atoms: bool = False, +) -> AtomLayout: + """Make a flat atom layout for given residues. + + Create a flat layout from a `Residues` object. The required atoms for each + amino acid type are taken from the CCD, hydrogens and oxygens are dropped to + make the linked residues. Terminal OXT's and protonation state for the + hydrogens come from the `Residues` object. + + Args: + residues: a `Residues` object. + ccd: The chemical components dictionary. + polymer_ligand_bonds: Bond information for polymer-ligand pairs. + ligand_ligand_bonds: Bond information for ligand-ligand pairs. + with_hydrogens: whether to create hydrogens + skip_unk_residues: whether to skip 'UNK' resides -- default is True to be + compatible with the rest of AlphaFold that does not predict atoms for + unknown residues + drop_ligand_leaving_atoms: Flag to switch on/ off leaving atoms for ligands. + + Returns: + an `AtomLayout` object + """ + num_res = residues.res_name.shape[0] + + # Target lists. + target_atom_names = [] + target_atom_elements = [] + target_res_ids = [] + target_res_names = [] + target_chain_ids = [] + target_chain_types = [] + + for idx in range(num_res): + # skip 'UNK' residues if requested + if ( + skip_unk_residues + and residues.res_name[idx] in residue_names.UNKNOWN_TYPES + ): + continue + + # Get the atoms for this residue type from CCD. + if ccd.get(residues.res_name[idx]): + res_atoms = struc_chem_comps.get_all_atoms_in_entry( + ccd=ccd, res_name=residues.res_name[idx] + ) + atom_names_elements = list( + zip( + res_atoms['_chem_comp_atom.atom_id'], + res_atoms['_chem_comp_atom.type_symbol'], + strict=True, + ) + ) + elif residues.smiles_string[idx]: + # Get atoms from RDKit via SMILES. + mol = Chem.MolFromSmiles(residues.smiles_string[idx]) + mol = _assign_atom_names_from_graph(mol) + atom_names_elements = [ + (a.GetProp('atom_name'), a.GetSymbol()) for a in mol.GetAtoms() + ] + else: + raise ValueError( + f'{residues.res_name[idx]} not found in CCD and no SMILES string' + ) + + # Remove hydrogens if requested. + if not with_hydrogens: + atom_names_elements = [ + (n, e) for n, e in atom_names_elements if (e != 'H' and e != 'D') + ] + bonded_atoms = get_bonded_atoms( + polymer_ligand_bonds, + ligand_ligand_bonds, + residues.res_id[idx], + residues.chain_id[idx], + ) + # Connect the amino-acids, i.e. remove OXT, HXT and H2. + drop_atoms = get_link_drop_atoms( + res_name=residues.res_name[idx], + chain_type=residues.chain_type[idx], + is_start_terminus=residues.is_start_terminus[idx], + is_end_terminus=residues.is_end_terminus[idx], + bonded_atoms=bonded_atoms, + drop_ligand_leaving_atoms=drop_ligand_leaving_atoms, + ) + + # If deprotonation info is available, remove the specific atoms. + if residues.deprotonation is not None: + drop_atoms.update(residues.deprotonation[idx]) + + atom_names_elements = [ + (n, e) for n, e in atom_names_elements if n not in drop_atoms + ] + + # Append the found atoms to the target lists. + target_atom_names.extend([n for n, _ in atom_names_elements]) + target_atom_elements.extend([e for _, e in atom_names_elements]) + num_atoms = len(atom_names_elements) + target_res_names.extend([residues.res_name[idx]] * num_atoms) + target_res_ids.extend([residues.res_id[idx]] * num_atoms) + target_chain_ids.extend([residues.chain_id[idx]] * num_atoms) + target_chain_types.extend([residues.chain_type[idx]] * num_atoms) + + return AtomLayout( + atom_name=np.array(target_atom_names, dtype=object), + atom_element=np.array(target_atom_elements, dtype=object), + res_name=np.array(target_res_names, dtype=object), + res_id=np.array(target_res_ids, dtype=int), + chain_id=np.array(target_chain_ids, dtype=object), + chain_type=np.array(target_chain_types, dtype=object), + ) + + +def compute_gather_idxs( + *, + source_layout: AtomLayout, + target_layout: AtomLayout, + fill_value: int = 0, +) -> GatherInfo: + """Produce gather indices and mask to convert from source layout to target.""" + source_uid_to_idx = { + uid: idx + for idx, uid in enumerate( + zip( + source_layout.chain_id.ravel(), + source_layout.res_id.ravel(), + source_layout.atom_name.ravel(), + strict=True, + ) + ) + } + gather_idxs = [] + gather_mask = [] + for uid in zip( + target_layout.chain_id.ravel(), + target_layout.res_id.ravel(), + target_layout.atom_name.ravel(), + strict=True, + ): + if uid in source_uid_to_idx: + gather_idxs.append(source_uid_to_idx[uid]) + gather_mask.append(True) + else: + gather_idxs.append(fill_value) + gather_mask.append(False) + target_shape = target_layout.atom_name.shape + return GatherInfo( + gather_idxs=np.array(gather_idxs, dtype=int).reshape(target_shape), + gather_mask=np.array(gather_mask, dtype=bool).reshape(target_shape), + input_shape=np.array(source_layout.atom_name.shape), + ) + + +def convert( + gather_info: GatherInfo, + arr: xnp_ndarray, + *, + layout_axes: tuple[int, ...] = (0,), +) -> xnp_ndarray: + """Convert an array from one atom layout to another.""" + # Translate negative indices to the corresponding positives. + layout_axes = tuple(i if i >= 0 else i + arr.ndim for i in layout_axes) + + # Ensure that layout_axes are continuous. + layout_axes_begin = layout_axes[0] + layout_axes_end = layout_axes[-1] + 1 + + if layout_axes != tuple(range(layout_axes_begin, layout_axes_end)): + raise ValueError(f'layout_axes must be continuous. Got {layout_axes}.') + layout_shape = arr.shape[layout_axes_begin:layout_axes_end] + + # Ensure that the layout shape is compatible + # with the gather_info. I.e. the first axis size must be equal or greater + # than the gather_info.input_shape, and all subsequent axes sizes must match. + if (len(layout_shape) != gather_info.input_shape.size) or ( + isinstance(gather_info.input_shape, np.ndarray) + and ( + (layout_shape[0] < gather_info.input_shape[0]) + or (np.any(layout_shape[1:] != gather_info.input_shape[1:])) + ) + ): + raise ValueError( + 'Input array layout axes are incompatible. You specified layout ' + f'axes {layout_axes} with an input array of shape {arr.shape}, but ' + f'the gather info expects shape {gather_info.input_shape}. ' + 'Your first axis size must be equal or greater than the ' + 'gather_info.input_shape, and all subsequent axes sizes must ' + 'match.' + ) + + # Compute the shape of the input array with flattened layout. + batch_shape = arr.shape[:layout_axes_begin] + features_shape = arr.shape[layout_axes_end:] + arr_flattened_shape = batch_shape + \ + (int(np.prod(layout_shape)),) + features_shape + + # Flatten input array and perform the gather. + arr_flattened = arr.reshape(arr_flattened_shape) + if layout_axes_begin == 0: + out_arr = arr_flattened[gather_info.gather_idxs, ...] + elif layout_axes_begin == 1: + out_arr = arr_flattened[:, gather_info.gather_idxs, ...] + elif layout_axes_begin == 2: + out_arr = arr_flattened[:, :, gather_info.gather_idxs, ...] + elif layout_axes_begin == 3: + out_arr = arr_flattened[:, :, :, gather_info.gather_idxs, ...] + elif layout_axes_begin == 4: + out_arr = arr_flattened[:, :, :, :, gather_info.gather_idxs, ...] + else: + raise ValueError( + 'Only 4 batch axes supported. If you need more, the code ' + 'is easy to extend.' + ) + + # Broadcast the mask and apply it. + broadcasted_mask_shape = ( + (1,) * len(batch_shape) + + gather_info.gather_mask.shape + + (1,) * len(features_shape) + ) + out_arr *= gather_info.gather_mask.reshape(broadcasted_mask_shape) + return out_arr + + +def convert_ms( + gather_info: GatherInfo, + arr: ms.Tensor, + *, + layout_axes: tuple[int, ...] = (0,), +) -> ms.Tensor: + """Convert an array from one atom layout to another.""" + # Translate negative indices to the corresponding positives. + layout_axes = tuple(i if i >= 0 else i + arr.ndim for i in layout_axes) + + # Ensure that layout_axes are continuous. + layout_axes_begin = layout_axes[0] + layout_axes_end = layout_axes[-1] + 1 + + if layout_axes != tuple(range(layout_axes_begin, layout_axes_end)): + raise ValueError(f'layout_axes must be continuous. Got {layout_axes}.') + layout_shape = arr.shape[layout_axes_begin:layout_axes_end] + + # Ensure that the layout shape is compatible + # with the gather_info. I.e. the first axis size must be equal or greater + # than the gather_info.input_shape, and all subsequent axes sizes must match. + # if (len(layout_shape) != gather_info.input_shape.size) or ( + # isinstance(gather_info.input_shape, np.ndarray) + # and ( + # (layout_shape[0] < gather_info.input_shape[0]) + # or (np.any(layout_shape[1:] != gather_info.input_shape[1:])) + # ) + # ): + # raise ValueError( + # 'Input array layout axes are incompatible. You specified layout ' + # f'axes {layout_axes} with an input array of shape {arr.shape}, but ' + # f'the gather info expects shape {gather_info.input_shape}. ' + # 'Your first axis size must be equal or greater than the ' + # 'gather_info.input_shape, and all subsequent axes sizes must ' + # 'match.' + # ) + + # Compute the shape of the input array with flattened layout. + batch_shape = arr.shape[:layout_axes_begin] + features_shape = arr.shape[layout_axes_end:] + arr_flattened_shape = batch_shape + \ + (int(math.prod(layout_shape)),) + features_shape + + # Flatten input array and perform the gather. + arr_flattened = arr.reshape(arr_flattened_shape) + out_arr = ops.gather(arr_flattened, gather_info.gather_idxs, axis=layout_axes_begin) + + # Broadcast the mask and apply it. + broadcasted_mask_shape = ( + (1,) * len(batch_shape) + + gather_info.gather_mask.shape + + (1,) * len(features_shape) + ) + out_arr *= ms.Tensor(gather_info.gather_mask.reshape(broadcasted_mask_shape)) + return out_arr.astype(ms.float32) + + +def make_structure( + flat_layout: AtomLayout, + atom_coords: np.ndarray, + name: str, + *, + atom_b_factors: np.ndarray | None = None, + all_physical_residues: Residues | None = None, +) -> structure.Structure: + """Returns a Structure from a flat layout and atom coordinates. + + The provided flat_layout must be 1-dim and must not contain any padding + elements. The flat_layout.atom_name must conform to the OpenMM/CCD standard + and must not contain deuterium. + + Args: + flat_layout: flat 1-dim AtomLayout without padding elements + atom_coords: np.ndarray of float, shape (num_atoms, 3) + name: str: the name (usually PDB id), e.g. '1uao' + atom_b_factors: np.ndarray of float, shape (num_atoms,) or None. If None, + they will be set to all zeros. + all_physical_residues: a Residues object that contains all physically + existing residues, i.e. also those residues that have no resolved atoms. + This is common in experimental structures, but also appears in predicted + structures for 'UNK' or other non-standard residue types, where the model + does not predict coordinates. This will be used to create the + `all_residues` field of the structure object. + """ + + if flat_layout.atom_name.ndim != 1 or not np.all( + flat_layout.atom_name.astype(bool) + ): + raise ValueError( + 'flat_layout must be 1-dim and must not contain anypadding element' + ) + if ( + flat_layout.atom_element is None + or flat_layout.res_name is None + or flat_layout.chain_type is None + ): + raise ValueError('All optional fields must be present.') + + if atom_b_factors is None: + atom_b_factors = np.zeros(atom_coords.shape[:-1]) + + if all_physical_residues is not None: + # Create the all_residues field from a Residues object + # (unfortunately there is no central place to keep the chain_types in + # the structure class, so we drop it here) + all_residues = collections.defaultdict(list) + for chain_id, res_id, res_name in zip( + all_physical_residues.chain_id, + all_physical_residues.res_id, + all_physical_residues.res_name, + strict=True, + ): + all_residues[chain_id].append((res_name, res_id)) + else: + # Create the all_residues field from the flat_layout + all_residues = collections.defaultdict(list) + if flat_layout.chain_id.shape[0] > 0: + all_residues[flat_layout.chain_id[0]].append( + (flat_layout.res_name[0], flat_layout.res_id[0]) + ) + for i in range(1, flat_layout.shape[0]): + if ( + flat_layout.chain_id[i] != flat_layout.chain_id[i - 1] + or flat_layout.res_name[i] != flat_layout.res_name[i - 1] + or flat_layout.res_id[i] != flat_layout.res_id[i - 1] + ): + all_residues[flat_layout.chain_id[i]].append( + (flat_layout.res_name[i], flat_layout.res_id[i]) + ) + + return structure.from_atom_arrays( + name=name, + all_residues=dict(all_residues), + chain_id=flat_layout.chain_id, + chain_type=flat_layout.chain_type, + res_id=flat_layout.res_id.astype(np.int32), + res_name=flat_layout.res_name, + atom_name=flat_layout.atom_name, + atom_element=flat_layout.atom_element, + atom_x=atom_coords[..., 0], + atom_y=atom_coords[..., 1], + atom_z=atom_coords[..., 2], + atom_b_factor=atom_b_factors, + ) + + +def _assert_all_arrays_have_same_shape( + *, + obj: AtomLayout | Residues | GatherInfo, + expected_shape: tuple[int, ...], + attribute_names: Sequence[str], +) -> None: + """Checks that given attributes of the object have the expected shape.""" + attribute_shapes_description = [] + all_shapes_are_valid = True + + for attribute_name in attribute_names: + attribute = getattr(obj, attribute_name) + + if attribute is None: + attribute_shape = None + else: + attribute_shape = attribute.shape + + if attribute_shape is not None and expected_shape != attribute_shape: + all_shapes_are_valid = False + + attribute_shape_name = attribute_name + '.shape' + attribute_shapes_description.append( + f'{attribute_shape_name:25} = {attribute_shape}' + ) + + if not all_shapes_are_valid: + raise ValueError( + f'All arrays must have the same shape ({expected_shape=}). Got\n' + + '\n'.join(attribute_shapes_description) + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/base_config.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/base_config.py new file mode 100644 index 000000000..0d3a08b62 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/base_config.py @@ -0,0 +1,153 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Config for the protein folding model and experiment.""" + +from collections.abc import Mapping +import copy +import dataclasses +import types +import typing +from typing import Any, ClassVar, TypeVar + + +_T = TypeVar('_T') +_ConfigT = TypeVar('_ConfigT', bound='BaseConfig') + + +def _strip_optional(t: type[Any]) -> type[Any]: + """Transforms type annotations of the form `T | None` to `T`.""" + if typing.get_origin(t) in (typing.Union, types.UnionType): + args = set(typing.get_args(t)) - {types.NoneType} + if len(args) == 1: + return args.pop() + return t + + +_NO_UPDATE = object() + + +class _Autocreate: + + def __init__(self, **defaults: Any): + self.defaults = defaults + + +def autocreate(**defaults: Any) -> Any: + """Marks a field as having a default factory derived from its type.""" + return _Autocreate(**defaults) + + +def _clone_field( + field: dataclasses.Field[_T], new_default: _T +) -> dataclasses.Field[_T]: + if new_default is _NO_UPDATE: + return copy.copy(field) + return dataclasses.field( + default=new_default, + init=True, + kw_only=True, + repr=field.repr, + hash=field.hash, + compare=field.compare, + metadata=field.metadata, + ) + + +@typing.dataclass_transform() +class ConfigMeta(type): + """Metaclass that synthesizes a __post_init__ that coerces dicts to Config subclass instances.""" + + def __new__(mcs, name, bases, classdict): + cls = super().__new__(mcs, name, bases, classdict) + + def _coercable_fields(self) -> Mapping[str, tuple[ConfigMeta, Any]]: + type_hints = typing.get_type_hints(self.__class__) + fields = dataclasses.fields(self.__class__) + field_to_type_and_default = { + field.name: (_strip_optional( + type_hints[field.name]), field.default) + for field in fields + } + coercable_fields = { + f: t + for f, t in field_to_type_and_default.items() + if issubclass(type(t[0]), ConfigMeta) + } + return coercable_fields + + cls._coercable_fields = property(_coercable_fields) + + old_post_init = getattr(cls, '__post_init__', None) + + def _post_init(self) -> None: + # Use get_type_hints instead of Field.type to ensure that forward + # references are resolved. + for field_name, ( + field_type, + field_default, + ) in self._coercable_fields.items(): # pylint: disable=protected-access + field_value = getattr(self, field_name) + if field_value is None: + continue + try: + match field_value: + case _Autocreate(): + # Construct from field defaults. + setattr(self, field_name, field_type( + **field_value.defaults)) + case Mapping(): + # Field value is not yet a `Config` instance; Assume we can create + # one by splatting keys and values. + args = {} + # Apply default args first, if present. + if isinstance(field_default, _Autocreate): + args.update(field_default.defaults) + args.update(field_value) + setattr(self, field_name, field_type(**args)) + case _: + pass + except TypeError as e: + raise TypeError( + f'Failure while coercing field {field_name!r} of' + f' {self.__class__.__qualname__}' + ) from e + if old_post_init: + old_post_init(self) + + cls.__post_init__ = _post_init + + return dataclasses.dataclass(kw_only=True)(cls) + + +class BaseConfig(metaclass=ConfigMeta): + """Config base class. + + Subclassing Config automatically makes the subclass a kw_only dataclass with + a `__post_init__` that coerces Config-subclass field values from mappings to + instances of the right type. + """ + # Provided by dataclasses.make_dataclass + __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]] + + # Overridden by metaclass + @property + def _coercable_fields(self) -> Mapping[str, tuple[type['BaseConfig'], Any]]: + return {} + + def as_dict(self) -> Mapping[str, Any]: + result = dataclasses.asdict(self) + for field_name in self._coercable_fields: + field_value = getattr(self, field_name, None) + if isinstance(field_value, BaseConfig): + result[field_name] = field_value.as_dict() + return result diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_model.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_model.py new file mode 100644 index 000000000..5de365c85 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_model.py @@ -0,0 +1,51 @@ +"""Defines interface of a BaseModel.""" + +from collections.abc import Mapping +import dataclasses +from typing import Any, TypeAlias +from alphafold3 import structure +import numpy as np +import mindspore as ms + +ModelResult: TypeAlias = Mapping[str, Any] +ScalarNumberOrArray: TypeAlias = Mapping[str, float | int | np.ndarray] + +# Eval result will contain scalars (e.g. metrics or losses), selected from the +# forward pass outputs or computed in the online evaluation; np.ndarrays or +# jax.Arrays generated from the forward pass outputs (e.g. distogram expected +# distances) or batch inputs; protein structures (predicted and ground-truth). +EvalResultValue: TypeAlias = ( + float | int | np.ndarray | ms.Tensor | structure.Structure +) +# Eval result may be None for some metrics if they are not computable. +EvalResults: TypeAlias = Mapping[str, EvalResultValue | None] +# Interface metrics are all floats or None. +InterfaceMetrics: TypeAlias = Mapping[str, float | None] +# Interface results are a mapping from interface name to mappings from score +# type to metric value. +InterfaceResults: TypeAlias = Mapping[str, Mapping[str, InterfaceMetrics]] +# Eval output consists of full eval results and a dict of interface metrics. +EvalOutput: TypeAlias = tuple[EvalResults, InterfaceResults] + +# Signature for `apply` method of hk.transform_with_state called on a BaseModel. +# ForwardFn: TypeAlias = Callable[ +# [hk.Params, hk.State, jax.Array, features.BatchDict], +# tuple[ModelResult, hk.State], +# ] + + +@dataclasses.dataclass(frozen=True) +class InferenceResult: + """Postprocessed model result.""" + + # Predicted protein structure. + predicted_structure: structure.Structure = dataclasses.field() + # Useful numerical data (scalars or arrays) to be saved at inference time. + numerical_data: ScalarNumberOrArray = dataclasses.field( + default_factory=dict) + # Smaller numerical data (usually scalar) to be saved as inference metadata. + metadata: ScalarNumberOrArray = dataclasses.field(default_factory=dict) + # Additional dict for debugging, e.g. raw outputs of a model forward pass. + debug_outputs: ModelResult | None = dataclasses.field(default_factory=dict) + # Model identifier. + model_id: bytes = b'' diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py new file mode 100644 index 000000000..f53537382 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py @@ -0,0 +1,148 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Common modules.""" + +from collections.abc import Sequence +import contextlib +import numbers +from typing import TypeAlias + +import numpy as np +import mindspore as ms +from mindspore import nn, ops +from mindspore.common import initializer +from mindchemistry.e3.utils import Ncon + +# Useful for mocking in tests. +DEFAULT_PRECISION = None + +# Constant from scipy.stats.truncnorm.std(a=-2, b=2, loc=0., scale=1.) +TRUNCATED_NORMAL_STDDEV_FACTOR = np.asarray( + 0.87962566103423978, dtype=np.float32 +) + + +class LayerNorm(nn.Cell): + """LayerNorm module. + + Equivalent to ms.nn.LayerNorm. In most cases, it can be replaced by ms.nn.LayerNorm. + Here, gamma is scale, beta is shift or offset + Args: + normalized_shape (tuple | list): The shape of Tensor which need to LayerNorm. + name (str): Name of this layer. + begin_norm_axis(int): From which axis norm begin + begin_params_axis(int): From which axis params begin + gamma_init('str'): Initializer of gamma + beta_init('str'): Initializer of beta + epsilon(float): epsilon value + dtype(ms.type): Type of output + create_beta(bool): whether to create a trainable beta parameter + create_gamma(bool): whether to create a trainable gamma parameter + Inputs: + - **x** (Tensor) - Tensor of any shape + Outputs: + The shape of tensor is the same as x. + Supported Platforms: + ``Ascend`` + """ + + def __init__(self, normalized_shape, name=None, begin_norm_axis=-1, + begin_params_axis=-1, gamma_init='ones', + beta_init='zeros', epsilon=1e-5, dtype=ms.float32, + create_beta=True, create_gamma=True): + super().__init__() + if not create_beta: + beta_init = 'zeros' + if not create_gamma: + gamma_init = 'ones' + self.layernorm = nn.LayerNorm(normalized_shape[begin_norm_axis:], begin_norm_axis=begin_norm_axis, + begin_params_axis=begin_params_axis, gamma_init=gamma_init, + beta_init=beta_init, epsilon=epsilon, dtype=dtype) + if create_beta is False: + self.layernorm.beta.requires_grad = False + if create_gamma is False: + self.layernorm.gamma.requires_grad = False + self.dtype = dtype + + def construct(self, x): + out = self.layernorm(x.astype(ms.float32)).astype(x.dtype) + return out + + +class CustomDense(nn.Cell): + """ + Custom Linear Module. It can be apply to a high dimension Tensor, and can be used on more than 1D Matmul. + In Alphafold, they use Einsum to replace Matmul, here we use Ncon to replace Matmul. if in_shape and out_shape + are both int, this layer is equivalence to nn.Dense. + Args: + in_shape (Union(int, List, Tuple)): input shape, that need to be multiplied. + out_shape (Union(int, List, Tuple)): output shape, that need to be multiplied. + Inputs: + - **x** (Tensor) + Outputs: + + Supported Platforms: + ``Ascend`` + """ + + def __init__(self, in_shape, out_shape, weight_init="zeros", use_bias=False, \ + bias_init="zeros", ndim=None, dtype=ms.float32): + super().__init__() + if isinstance(in_shape, int): + in_shape = (in_shape,) + if isinstance(out_shape, int): + out_shape = (out_shape,) + self.num_output_dims = len(out_shape) + self.num_input_dims = len(in_shape) + if ndim is None: + ndim = len(in_shape) + 1 + if weight_init in ["relu", "linear"]: + self.weight = custom_initializer( + weight_init, in_shape + out_shape, dtype=dtype) + else: + self.weight = ms.Parameter(initializer.initializer( + weight_init, in_shape + out_shape, dtype=dtype)) + self.use_bias = use_bias + if self.use_bias: + self.bias = ms.Parameter( + initializer.initializer(bias_init, out_shape, dtype=dtype)) + ncon_list1 = [-i-1 for i in range(ndim - self.num_input_dims)] + [ + i+1 for i in range(len(in_shape))] + ncon_list2 = (ncon_list1[ndim - self.num_input_dims:]) + \ + [-i-ndim+self.num_input_dims-1 for i in range(len(out_shape))] + self.ncon = Ncon([ncon_list1, ncon_list2]) + + in_letters = 'abcde'[: self.num_input_dims] + out_letters = 'hijkl'[: self.num_output_dims] + self.equation = f'...{in_letters}, {in_letters}{out_letters}->...{out_letters}' + + def construct(self, x): + if self.use_bias: + output = self.ncon([x, self.weight]) + self.bias + else: + output = self.ncon([x, self.weight]) + return output + + +def custom_initializer(initializer_name, input_shape, dtype=ms.float32): + noise_scale = ms.Tensor(1.0) + for channel_dim in input_shape: + noise_scale /= channel_dim + if initializer_name == 'relu': + noise_scale *= 2 + stddev = ops.sqrt(noise_scale) + stddev = stddev / ms.Tensor(TRUNCATED_NORMAL_STDDEV_FACTOR) + param = ms.Parameter(initializer.initializer( + initializer.TruncatedNormal(stddev, 0), input_shape, dtype)) + return param diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py new file mode 100644 index 000000000..4d5f660be --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py @@ -0,0 +1,353 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Specialized mapping functions.""" + +from collections.abc import Callable, Sequence +import functools +from typing import Any +import mindspore as ms + + +Pytree = Any +PytreeJaxArray = Any + +partial = functools.partial +PROXY = object() + + +def _maybe_slice(array, i, slice_size, axis): + "modified to mindspore" + if axis is PROXY: + return array + start = [0]*array.ndim + start[axis] = i + size = list(array.shape) + size[axis] = slice_size + return ms.ops.slice(array, start, size) + + +def _maybe_get_size(array, axis): + "modified to mindspore" + if axis == PROXY: + return -1 + return array.shape[axis] + + +def tree_flatten(tree): + if isinstance(tree, (list, tuple)): + flat, structure = [], [] + for item in tree: + sub_flat, sub_struct = tree_flatten(item) + flat.extend(sub_flat) + structure.append(sub_struct) + return flat, structure + elif isinstance(tree, dict): + flat, structure = [], {} + for key, value in tree.items(): + sub_flat, sub_struct = tree_flatten(value) + flat.extend(sub_flat) + structure[key] = sub_struct + return flat, structure + else: + return [tree], None + + +def tree_unflatten(flat, structure): + if isinstance(structure, list): + result, idx = [], 0 + for sub_struct in structure: + sub_tree, idx = tree_unflatten(flat[idx:], sub_struct) + result.append(sub_tree) + return result, idx + elif isinstance(structure, dict): + result, idx = {}, 0 + for key, sub_struct in structure.items(): + sub_tree, idx = tree_unflatten(flat[idx:], sub_struct) + result[key] = sub_tree + return result, idx + else: + return flat[0], 1 + + +def _expand_axes(axes, values, name="sharded_apply"): + values_tree_def = tree_flatten(values)[1] + # flat_axes = tree_flatten(axes)[0] + flat_axes = [PROXY if axes is None else axes for _ in values_tree_def] + expanded_axes, _ = tree_unflatten(flat_axes, values_tree_def) + return expanded_axes + + +def tree_map(fn, *trees): + "Mindspore do not have the same function like Jax.tree.map, so try to write a mindspore version." + tree_types = {type(tree) for tree in trees} + tree_type = tree_types.pop() + if tree_type in (list,): + return tree_type(tree_map(fn, *subtrees) for subtrees in zip(*trees)) + if tree_type is dict: + keys = trees[0].keys() + if not all(tree.keys() == keys for tree in trees): + raise ValueError("All input dictionaries must have the same keys") + return {key: tree_map(fn, *(tree[key] for tree in trees)) for key in keys} + return fn(*trees) + + +def tree_leaves(tree): + "same as tree_map" + if isinstance(tree, (list, tuple)): + leaves = [] + for item in tree: + leaves.extend(tree_leaves(item)) + return leaves + if isinstance(tree, dict): + leaves = [] + for key in tree: + leaves.extend(tree_leaves(tree[key])) + return leaves + return [tree] + + +def eval_shape(fun, *args, **kwargs): + fake_inputs = [ms.ops.zeros(arg.shape, dtype=arg.dtype) if isinstance( + arg, ms.Tensor) else arg for arg in args] + output = fun(*fake_inputs, **kwargs) + return output + + +def sharded_apply( + fun: Callable[..., PytreeJaxArray], + shard_size: int | None = 1, + in_axes: int | Pytree = 0, + out_axes: int | Pytree = 0, + new_out_axes: bool = False, +) -> Callable[..., PytreeJaxArray]: + """Sharded apply. + + Applies `fun` over shards to axes, in a way similar to vmap, + but does so in shards of `shard_size`. Shards are stacked after. + This allows a smooth trade-off between + memory usage (as in a plain map) vs higher throughput (as in a vmap). + + Args: + fun: Function to apply smap transform to. + shard_size: Integer denoting shard size. + in_axes: Either integer or pytree describing which axis to map over for each + input to `fun`, None denotes broadcasting. + out_axes: Integer or pytree denoting to what axis in the output the mapped + over axis maps. + new_out_axes: Whether to stack outputs on new axes. This assumes that the + output sizes for each shard (including the possible remainder shard) are + the same. + + Returns: + Function with smap applied. + """ + docstr = ( + "Mapped version of {fun}. Takes similar arguments to {fun} " + "but with additional array axes over which {fun} is mapped." + ) + if new_out_axes: + raise NotImplementedError("New output axes not yet implemented.") + + # shard size None denotes no sharding + if shard_size is None: + return fun + + def mapped_fn(*args, **kwargs): + # Expand in axes and determine loop range. + in_axes_ = _expand_axes(ms.Tensor(in_axes), args) + + in_sizes = tree_map(_maybe_get_size, list(args), in_axes_) + in_size = max(tree_leaves(in_sizes)) + + num_extra_shards = (in_size - 1) // shard_size + + # Fix if necessary. + last_shard_size = in_size % shard_size + last_shard_size = shard_size if last_shard_size == 0 else last_shard_size + + def apply_fun_to_slice(slice_start, slice_size, args, in_axes_): + input_slice = tree_map( + lambda array, axis: _maybe_slice( + array, slice_start, slice_size, axis + ), + args, + in_axes_, + ) + return fun(input_slice, **kwargs) + + remainder_shape_dtype = eval_shape( + lambda array, axis: apply_fun_to_slice( + 0, last_shard_size, array, axis), + args, in_axes_ + ) + + out_shapes = tree_map(lambda x: x.shape, remainder_shape_dtype) + out_dtypes = tree_map(lambda x: x.dtype, remainder_shape_dtype) + out_axes_ = _expand_axes(out_axes, out_shapes) + + if num_extra_shards > 0: + regular_shard_shape_dtype = eval_shape( + lambda array, axis: apply_fun_to_slice( + 0, shard_size, array, axis), + args, in_axes_ + ) + shard_shapes = tree_map( + lambda x: x.shape, regular_shard_shape_dtype) + + def make_output_shape(axis, shard_shape, remainder_shape): + axis = axis if isinstance(axis, int) else int(axis[0]) + shard_shape = tuple(shard_shape) + remainder_shape = tuple(remainder_shape) + return ms.ops.stack( + shard_shape[:axis] + + (shard_shape[axis] * num_extra_shards + + remainder_shape[axis],) + + shard_shape[axis + 1:] + ) + + out_shapes = tree_map( + make_output_shape, out_axes_[0], ms.Tensor( + shard_shapes), ms.Tensor(out_shapes) + ) + + # Calls dynamic Update slice with different argument order. + # This is here since tree_map only works with positional arguments. + def dynamic_update_slice_in_dim(array, slice_size, axis, i): + start = [0]*array.ndim + start[axis] = int(i) + size = list(array.shape) + size[axis] = slice_size.shape[axis] + # return ms.ops.slice(array, start, size) + end = [x + y for x, y in zip(start, size)] + array[start[0]: end[0]] = slice_size + return array + + def compute_shard(outputs, slice_start, slice_size): + slice_out = (lambda array, axis: apply_fun_to_slice( + int(slice_start), shard_size, array, axis))(args, in_axes_) + update_slice = partial(dynamic_update_slice_in_dim, i=slice_start) + # slice_out = (slice_out,) if not isinstance(slice, (int, float)) else [int(x) for x in slice_out] + return tree_map(update_slice, outputs, slice_out, out_axes_[0]) + + def scan_iteration(outputs, i): + new_outputs = compute_shard(outputs, i, shard_size) + return new_outputs + + slice_starts = ms.ops.arange(0, in_size - shard_size + 1, shard_size) + + def allocate_buffer(dtype, shape): + return ms.ops.zeros(shape, dtype=dtype) + + outputs = tree_map(allocate_buffer, out_dtypes, out_shapes) + + if slice_starts.shape[0] > 0: + for slice_start in slice_starts: + outputs = scan_iteration(outputs, slice_start) + # scan_op = ms.ops.Scan() + # outputs, _ = scan_op(scan_iteration, outputs, slice_starts) + + if last_shard_size != shard_size: + remainder_start = in_size - last_shard_size + outputs = compute_shard(outputs, remainder_start, last_shard_size) + + return outputs + + return mapped_fn + + +def sharded_map(fun, shard_size=1, in_axes=0, out_axes=0): + vmapped_fun = ms.vmap(fun, int(in_axes), int(out_axes)) + return sharded_apply(vmapped_fun, shard_size, in_axes, out_axes) + + +def reshape_partitioned_inputs(batched_args, partitioned_dim, subbatch_size): + """Reshapes so subbatching doesn't happen on the partitioned dim.""" + subbatched_args = [] + for arg in batched_args: + shape = arg.shape + new_shape = ( + shape[:partitioned_dim] + + (subbatch_size, shape[partitioned_dim] // subbatch_size) + + shape[partitioned_dim + 1:] + ) + subbatched_args.append(arg.reshape(new_shape)) + return subbatched_args + + +def reshape_partitioned_output(output, output_subbatch_dim): + """Reshapes outputs as if reshape_partitioned_inputs were never applied.""" + out_shape = ( + output.shape[: output_subbatch_dim - 1] + + (-1,) + + output.shape[output_subbatch_dim + 1:] + ) + return output.reshape(out_shape) + + +def inference_subbatch(module, subbatch_size, batched_args, + nonbatched_args, input_subbatch_dim=0, output_subbatch_dim=None, + input_subbatch_dim_is_partitioned=False): + """Run through subbatches (like batch apply but with split and concat).""" + assert len(batched_args) > 0 + if output_subbatch_dim is None: + output_subbatch_dim = input_subbatch_dim + if input_subbatch_dim_is_partitioned: + # Subbatching along the partitioned axis would induce an all-gather that + # undoes the partitioning. So instead we reshape such that + # [..., partitioned_input_size, ...] becomes [..., subbatch_size, + # partitioned_input_size // subbatch_size, ...] and then actually subbatch + # along the partitioned_input_size // subbatch_size axis in slices of + # size 1. Partitioning is then preserved on the partitioned axis, except + # that dimension is now of size subbatch_size instead of + # partitioned_input_size. Note that the module itself still sees inputs of + # size [..., subbatch_size, ...], just as it would if this reshaping were + # not applied. + batched_args = reshape_partitioned_inputs( + batched_args, input_subbatch_dim, subbatch_size + ) + input_subbatch_dim += 1 + output_subbatch_dim += 1 + subbatch_size = 1 + + def run_module(*batched_args): + if input_subbatch_dim_is_partitioned: + # Squeeze off the singleton dimension (otherwise the module would see + # [..., subbatch_size, 1, ...]). + batched_args = [b.squeeze(axis=input_subbatch_dim) + for b in batched_args] + args = list(batched_args)[0] + list(nonbatched_args) + res = module(*args) + if input_subbatch_dim_is_partitioned: + # Add back in the singleton dimension so the outputs are stacked on the + # axis we are actually subbatching over (i.e stacked back to + # [..., subbatch_size, partitioned_input_size // subbatch_size, ...]), + # rather than on the partitioned axis, which would again induce an + # all-gather that breaks partitioning. + res = ms.ops.expand_dims(res, axis=output_subbatch_dim) + return res + sharded_module = sharded_apply( + run_module, + shard_size=subbatch_size, + in_axes=input_subbatch_dim, + out_axes=output_subbatch_dim, + ) + output = sharded_module(*batched_args) + if input_subbatch_dim_is_partitioned: + # The is of the same shape as the inputs [..., subbatch_size, + # partitioned_input_size // subbatch_size, ...]. Reshape to + # [..., partitioned_input_size, ...] as if the reshaping due to partitioning + # had never been applied. + output = reshape_partitioned_output(output, output_subbatch_dim) + + return output diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/utils.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/utils.py new file mode 100644 index 000000000..537ee0648 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/utils.py @@ -0,0 +1,65 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +from collections import abc +import numbers + +import numpy as np +import mindspore as ms + +VALID_DTYPES = [np.float32, np.float64, np.int8, np.int32, np.int32, bool] + + +def remove_invalidly_typed_feats(batch): + """Remove features of types we don't want to send to the TPU e.g. strings.""" + return { + k: v + for k, v in batch.items() + if hasattr(v, 'dtype') and v.dtype in VALID_DTYPES + } + + +def mask_mean(mask, value, axis=None, keepdims=False, eps=1e-10): + """Masked mean.""" + + mask_shape = mask.shape + value_shape = value.shape + + assert len(mask_shape) == len( + value_shape + ), 'Shapes are not compatible, shapes: {}, {}'.format(mask_shape, value_shape) + + if isinstance(axis, numbers.Integral): + axis = [axis] + elif axis is None: + axis = list(range(len(mask_shape))) + assert isinstance( + axis, abc.Iterable + ), 'axis needs to be either an iterable, integer or "None"' + + broadcast_factor = 1.0 + for axis_ in axis: + value_size = value_shape[axis_] + mask_size = mask_shape[axis_] + if mask_size == 1: + broadcast_factor *= value_size + else: + error = f'Shapes are not compatible, shapes: {mask_shape}, {value_shape}' + assert mask_size == value_size, error + + return ms.ops.sum(mask * value, keepdim=keepdims, dim=axis) / ( + ms.ops.maximum( + ms.ops.sum(mask, keepdim=keepdims, dim=axis) * + broadcast_factor, eps + ) + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidence_types.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidence_types.py new file mode 100644 index 000000000..3ceb926d6 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidence_types.py @@ -0,0 +1,310 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Confidence categories for predictions.""" + +import dataclasses +import enum +import json +from typing import Any, Self + +from absl import logging +import numpy as np +import mindspore as ms +from alphafold3.model.components import base_model +from alphafold3.model.components.mapping import tree_map + + +class StructureConfidenceFullEncoder(json.JSONEncoder): + """JSON encoder for serializing confidence types.""" + + def __init__(self, **kwargs): + super().__init__(**(kwargs | dict(separators=(',', ':')))) + + def encode(self, o: 'StructureConfidenceFull'): + # Cast to np.float64 before rounding, since casting to Python float will + # cast to a 64 bit float, potentially undoing np.float32 rounding. + atom_plddts = np.round( + np.clip(np.asarray(o.atom_plddts, dtype=np.float64), 0.0, 99.99), 2 + ).astype(float) + contact_probs = np.round( + np.clip(np.asarray(o.contact_probs, dtype=np.float64), 0.0, 1.0), 2 + ).astype(float) + pae = np.round( + np.clip(np.asarray(o.pae, dtype=np.float64), 0.0, 99.9), 1 + ).astype(float) + return """\ +{ + "atom_chain_ids": %s, + "atom_plddts": %s, + "contact_probs": %s, + "pae": %s, + "token_chain_ids": %s, + "token_res_ids": %s +}""" % ( + super().encode(o.atom_chain_ids), + super().encode(list(atom_plddts)).replace('NaN', 'null'), + super().encode([list(x) + for x in contact_probs]).replace('NaN', 'null'), + super().encode([list(x) for x in pae]).replace('NaN', 'null'), + super().encode(o.token_chain_ids), + super().encode(o.token_res_ids), + ) + + +def _dump_json(data: Any, indent: int | None = None) -> str: + """Dumps a json string with JSON compatible NaN representation.""" + json_str = json.dumps( + data, + sort_keys=True, + indent=indent, + separators=(',', ': '), + ) + return json_str.replace('NaN', 'null') + + +@enum.unique +class ConfidenceCategory(enum.Enum): + """Confidence categories for AlphaFold predictions.""" + + HIGH = 0 + MEDIUM = 1 + LOW = 2 + DISORDERED = 3 + + @classmethod + def from_char(cls, char: str) -> Self: + match char: + case 'H': + return cls.HIGH + case 'M': + return cls.MEDIUM + case 'L': + return cls.LOW + case 'D': + return cls.DISORDERED + case _: + raise ValueError( + f'Unknown character. Expected one of H, M, L or D; got: {char}' + ) + + def to_char(self) -> str: + match self: + case self.HIGH: + return 'H' + case self.MEDIUM: + return 'M' + case self.LOW: + return 'L' + case self.DISORDERED: + return 'D' + + @classmethod + def from_confidence_score(cls, confidence: float) -> Self: + if 90 <= confidence <= 100: + return cls.HIGH + if 70 <= confidence < 90: + return cls.MEDIUM + if 50 <= confidence < 70: + return cls.LOW + if 0 <= confidence < 50: + return cls.DISORDERED + raise ValueError( + f'Confidence score out of range [0, 100]: {confidence}') + + +@dataclasses.dataclass() +class AtomConfidence: + """Dataclass for 1D per-atom confidences from AlphaFold.""" + + chain_id: list[str] + atom_number: list[int] + confidence: list[float] + confidence_category: list[ConfidenceCategory] + + def __post_init__(self): + num_res = len(self.atom_number) + if not all( + len(v) == num_res + for v in [self.chain_id, self.confidence, self.confidence_category] + ): + raise ValueError( + 'All confidence fields must have the same length.') + + @classmethod + def from_inference_result( + cls, inference_result: base_model.InferenceResult + ) -> Self: + """Instantiates an AtomConfidence from a structure. + + Args: + inference_result: Inference result from AlphaFold. + + Returns: + Scores in AtomConfidence dataclass. + """ + struct = inference_result.predicted_structure + as_dict = { + 'chain_id': [], + 'atom_number': [], + 'confidence': [], + 'confidence_category': [], + } + for atom_number, atom in enumerate(struct.iter_atoms()): + this_confidence = float(struct.atom_b_factor[atom_number]) + as_dict['chain_id'].append(atom['chain_id']) + as_dict['atom_number'].append(atom_number) + as_dict['confidence'].append(round(this_confidence, 2)) + as_dict['confidence_category'].append( + ConfidenceCategory.from_confidence_score(this_confidence) + ) + return cls(**as_dict) + + @classmethod + def from_json(cls, json_string: str) -> Self: + """Instantiates a AtomConfidence from a json string.""" + input_dict = json.loads(json_string) + input_dict['confidence_category'] = [ + ConfidenceCategory.from_char(k) + for k in input_dict['confidence_category'] + ] + return cls(**input_dict) + + def to_json(self) -> str: + output = dataclasses.asdict(self) + output['confidence_category'] = [ + k.to_char() for k in output['confidence_category'] + ] + output['atom_number'] = [int(k) for k in output['atom_number']] + return _dump_json(output) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class StructureConfidenceSummary: + """Dataclass for the summary of structure scores from AlphaFold. + + Attributes: + ptm: Predicted TM global score. + iptm: Interface predicted TM global score. + ranking_score: Ranking score extracted from CIF metadata. + fraction_disordered: Fraction disordered, measured with RASA. + has_clash: Has significant clashing. + chain_pair_pae_min: [num_chains, num_chains] Minimum cross chain PAE. + chain_pair_iptm: [num_chains, num_chains] Chain pair ipTM. + chain_ptm: [num_chains] Chain pTM. + chain_iptm: [num_chains] Mean cross chain ipTM for a chain. + """ + + ptm: float + iptm: float + ranking_score: float + fraction_disordered: float + has_clash: float + chain_pair_pae_min: np.ndarray + chain_pair_iptm: np.ndarray + chain_ptm: np.ndarray + chain_iptm: np.ndarray + + @classmethod + def from_inference_result( + cls, inference_result: base_model.InferenceResult + ) -> Self: + """Returns a new instance based on a given inference result.""" + return cls( + ptm=float(inference_result.metadata['ptm']), + iptm=float(inference_result.metadata['iptm']), + ranking_score=float(inference_result.metadata['ranking_score']), + fraction_disordered=float( + inference_result.metadata['fraction_disordered'] + ), + has_clash=float(inference_result.metadata['has_clash']), + chain_pair_pae_min=inference_result.metadata['chain_pair_pae_min'], + chain_pair_iptm=inference_result.metadata['chain_pair_iptm'], + chain_ptm=inference_result.metadata['iptm_ichain'], + chain_iptm=inference_result.metadata['iptm_xchain'], + ) + + @classmethod + def from_json(cls, json_string: str) -> Self: + """Returns a new instance from a given json string.""" + return cls(**json.loads(json_string)) + + def to_json(self) -> str: + def convert(data): + if isinstance(data, np.ndarray): + # Cast to np.float64 before rounding, since casting to Python float will + # cast to a 64 bit float, potentially undoing np.float32 rounding. + rounded_data = np.round(data.astype( + np.float64), decimals=2).tolist() + else: + rounded_data = np.round(data, decimals=2) + return rounded_data + + return _dump_json(tree_map(convert, dataclasses.asdict(self)), indent=1) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class StructureConfidenceFull: + """Dataclass for full structure data from AlphaFold.""" + + pae: np.ndarray + token_chain_ids: list[str] + token_res_ids: list[int] + atom_plddts: list[float] + atom_chain_ids: list[str] + contact_probs: np.ndarray # [num_tokens, num_tokens] + + @classmethod + def from_inference_result( + cls, inference_result: base_model.InferenceResult + ) -> Self: + """Returns a new instance based on a given inference result.""" + + pae = inference_result.numerical_data['full_pae'] + if isinstance(pae, ms.Tensor): + pae = pae.asnumpy() + if not isinstance(pae, np.ndarray): + logging.info('%s', type(pae)) + raise TypeError('pae should be a numpy array.') + + contact_probs = inference_result.numerical_data['contact_probs'] + if isinstance(contact_probs, ms.Tensor): + contact_probs = contact_probs.asnumpy() + if not isinstance(contact_probs, np.ndarray): + logging.info('%s', type(contact_probs)) + raise TypeError('contact_probs should be a numpy array.') + + struct = inference_result.predicted_structure + chain_ids = struct.chain_id.tolist() + atom_plddts = struct.atom_b_factor.tolist() + token_chain_ids = [ + str(token_id) + for token_id in inference_result.metadata['token_chain_ids'] + ] + token_res_ids = [ + int(token_id) for token_id in inference_result.metadata['token_res_ids'] + ] + return cls( + pae=pae, + token_chain_ids=token_chain_ids, + token_res_ids=token_res_ids, + atom_plddts=atom_plddts, + atom_chain_ids=chain_ids, + contact_probs=contact_probs, + ) + + @classmethod + def from_json(cls, json_string: str) -> Self: + """Returns a new instance from a given json string.""" + return cls(**json.loads(json_string)) + + def to_json(self) -> str: + """Converts StructureConfidenceFull to json string.""" + return json.dumps(self, cls=StructureConfidenceFullEncoder) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidences.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidences.py new file mode 100644 index 000000000..9eeb22a25 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidences.py @@ -0,0 +1,665 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Functions for extracting and processing confidences from model outputs.""" +import warnings +import numpy as np +from absl import logging +from alphafold3 import structure +from alphafold3.constants import residue_names +from alphafold3.cpp import mkdssp +from scipy import spatial + + +# From Sander & Rost 1994 https://doi.org/10.1002/prot.340200303 + +MAX_ACCESSIBLE_SURFACE_AREA = { + 'ALA': 106.0, + 'ARG': 248.0, + 'ASN': 157.0, + 'ASP': 163.0, + 'CYS': 135.0, + 'GLN': 198.0, + 'GLU': 194.0, + 'GLY': 84.0, + 'HIS': 184.0, + 'ILE': 169.0, + 'LEU': 164.0, + 'LYS': 205.0, + 'MET': 188.0, + 'PHE': 197.0, + 'PRO': 136.0, + 'SER': 130.0, + 'THR': 142.0, + 'TRP': 227.0, + 'TYR': 222.0, + 'VAL': 142.0, +} + +# Weights for ranking confidence. +_IPTM_WEIGHT = 0.8 +_FRACTION_DISORDERED_WEIGHT = 0.5 +_CLASH_PENALIZATION_WEIGHT = 100.0 + + +def windowed_solvent_accessible_area(cif: str, window: int = 25) -> np.ndarray: + """Implementation of AlphaFold_RSA. + + AlphaFold_RSA defined in + https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767/. + + Args: + cif: raw cif string. + window: The window over which to average accessible surface area + + Returns: + An array of size num_res that predicts disorder by using windowed solvent + accessible surface area. + """ + result = mkdssp.get_dssp(cif, calculate_surface_accessibility=True) + parse_row = False + rasa = [] + for row in result.splitlines(): + if parse_row: + aa = row[13:14] + if aa == '!': + continue + aa3 = residue_names.PROTEIN_COMMON_ONE_TO_THREE.get(aa, 'ALA') + max_acc = MAX_ACCESSIBLE_SURFACE_AREA[aa3] + acc = int(row[34:38]) + norm_acc = acc / max_acc + if norm_acc > 1.0: + norm_acc = 1.0 + rasa.append(norm_acc) + if row.startswith(' # RESIDUE'): + parse_row = True + + half_w = (window - 1) // 2 + pad_rasa = np.pad(rasa, (half_w, half_w), 'reflect') + rasa = np.convolve(pad_rasa, np.ones(window), 'valid') / window + return rasa + + +def fraction_disordered( + struct: structure.Structure, rasa_disorder_cutoff: float = 0.581 +) -> float: + """Compute fraction of protein residues that are disordered. + + Args: + struct: A structure to compute rASA metrics on. + rasa_disorder_cutoff: The threshold at which residues are considered + disordered. Default value taken from + https://www.ncbi.nlm.nih.gov/pmc/articles/PMC9601767/. + + Returns: + The fraction of protein residues that are disordered + (rasa > rasa_disorder_cutoff). + """ + struct = struct.filter_to_entity_type(protein=True) + rasa = [] + seq_rasa = {} + for chain_id, chain_seq in struct.chain_single_letter_sequence().items(): + if chain_seq in seq_rasa: + # We assume that identical sequences have approximately similar rasa + # values to speed up the computation. + rasa.extend(seq_rasa[chain_seq]) + continue + chain_struct = struct.filter(chain_id=chain_id) + try: + rasa_per_residue = windowed_solvent_accessible_area( + chain_struct.to_mmcif() + ) + seq_rasa[chain_seq] = rasa_per_residue + rasa.extend(rasa_per_residue) + except (ValueError, RuntimeError): + logging.warning('%s: rasa calculation failed', struct.name) + + if not rasa: + return 0.0 + return np.mean(np.array(rasa) > rasa_disorder_cutoff) + + +def has_clash( + struct: structure.Structure, + cutoff_radius: float = 1.1, + min_clashes_for_overlap: int = 100, + min_fraction_for_overlap: float = 0.5, +) -> bool: + """Determine whether the structure has at least one clashing chain. + + A clashing chain is defined as having greater than 100 polymer atoms within + 1.1A of another polymer atom, or having more than 50% of the chain with + clashing atoms. + + Args: + struct: A structure to get clash metrics for. + cutoff_radius: atom distances under this threshold are considered a clash. + min_clashes_for_overlap: The minimum number of atom-atom clashes for a chain + to be considered overlapping. + min_fraction_for_overlap: The minimum fraction of atoms within a chain that + are clashing for the chain to be considered overlapping. + + Returns: + True if the structure has at least one clashing chain. + """ + struct = struct.filter_to_entity_type(protein=True, rna=True, dna=True) + if not struct.chains: + return False + coords = struct.coords + coord_kdtree = spatial.cKDTree(coords) + clashes_per_atom = coord_kdtree.query_ball_point( + coords, p=2.0, r=cutoff_radius + ) + per_atom_has_clash = np.zeros(len(coords), dtype=np.int32) + for atom_idx, clashing_indices in enumerate(clashes_per_atom): + for clashing_idx in clashing_indices: + if np.abs(struct.res_id[atom_idx] - struct.res_id[clashing_idx]) > 1 or ( + struct.chain_id[atom_idx] != struct.chain_id[clashing_idx] + ): + per_atom_has_clash[atom_idx] = True + break + for chain_id in struct.chains: + mask = struct.chain_id == chain_id + num_atoms = np.sum(mask) + if num_atoms == 0: + continue + num_clashes = np.sum(per_atom_has_clash * mask) + frac_clashes = num_clashes / num_atoms + if ( + num_clashes > min_clashes_for_overlap + or frac_clashes > min_fraction_for_overlap + ): + return True + return False + + +def get_ranking_score( + ptm: float, iptm: float, fraction_disordered_: float, has_clash_: bool +) -> float: + # ipTM is NaN for single chain structures. Use pTM for such cases. + if np.isnan(iptm): + ptm_iptm_average = ptm + else: + ptm_iptm_average = _IPTM_WEIGHT * iptm + (1.0 - _IPTM_WEIGHT) * ptm + return ( + ptm_iptm_average + + _FRACTION_DISORDERED_WEIGHT * fraction_disordered_ + - _CLASH_PENALIZATION_WEIGHT * has_clash_ + ) + + +def rank_metric( + full_pde: np.ndarray, contact_probs: np.ndarray +) -> np.ndarray: + """Compute the metric that will be used to rank predictions, higher is better. + + Args: + full_pde: A [num_samples, num_tokens,num_tokens] matrix of predicted + distance errors between pairs of tokens. + contact_probs: A [num_tokens, num_tokens] matrix consisting of the + probability of contact (<8A) that is returned from the distogram head. + + Returns: + A scalar that can be used to rank (higher is better). + """ + if not isinstance(full_pde, type(contact_probs)): + raise ValueError( + 'full_pde and contact_probs must be of the same type.') + + if isinstance(full_pde, np.ndarray): + sum_fn = np.sum + else: + raise ValueError('full_pde must be a numpy array or a jax array.') + # It was found that taking the contact_map weighted average was better than + # just the predicted distance error on its own. + return -sum_fn(full_pde * contact_probs[None, :, :], axis=(-2, -1)) / ( + sum_fn(contact_probs) + 1e-6 + ) + + +def weighted_mean(mask, value, axis): + return np.mean(mask * value, axis=axis) / (1e-8 + np.mean(mask, axis=axis)) + + +def pde_single( + num_tokens: int, + asym_ids: np.ndarray, + full_pde: np.ndarray, + contact_probs: np.ndarray, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Compute 1D PDE summaries. + + Args: + num_tokens: The number of tokens (not including padding). + asym_ids: The asym_ids (array of shape num_tokens). + full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted + distance errors. + contact_probs: A [num_tokens, num_tokens] matrix consisting of the + probability of contact (<8A) that is returned from the distogram head. + + Returns: + A tuple (ichain, xchain, full_chain) where: + `ichain` is a [num_samples, num_chains] matrix where the + value assigned to each chain is an average of the full PDE matrix over all + its within-chain interactions, weighted by `contact_probs`. + `xchain` is a [num_samples, num_chains] matrix where the + value assigned to each chain is an average of the full PDE matrix over all + its cross-chain interactions, weighted by `contact_probs`. + `full_chain` is a [num_samples, num_tokens] matrix where the + value assigned to each token is an average of it PDE against all tokens, + weighted by `contact_probs`. + """ + + full_pde = full_pde[:, :num_tokens, :num_tokens] + contact_probs = contact_probs[:num_tokens, :num_tokens] + asym_ids = asym_ids[:num_tokens] + unique_asym_ids = np.unique(asym_ids) + num_chains = len(unique_asym_ids) + num_samples = full_pde.shape[0] + + asym_ids = asym_ids[None] + contact_probs = contact_probs[None] + + ichain = np.zeros((num_samples, num_chains)) + xchain = np.zeros((num_samples, num_chains)) + + for idx, asym_id in enumerate(unique_asym_ids): + my_asym_id = asym_ids == asym_id + imask = my_asym_id[:, :, None] * my_asym_id[:, None, :] + xmask = my_asym_id[:, :, None] * ~my_asym_id[:, None, :] + imask = imask * contact_probs + xmask = xmask * contact_probs + ichain[:, idx] = weighted_mean( + mask=imask, value=full_pde, axis=(-2, -1)) + xchain[:, idx] = weighted_mean( + mask=xmask, value=full_pde, axis=(-2, -1)) + + full_chain = weighted_mean(mask=contact_probs, value=full_pde, axis=(-1,)) + + return ichain, xchain, full_chain + + +def chain_pair_pde( + num_tokens: int, asym_ids: np.ndarray, full_pde: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """Compute predicted distance errors for all pairs of chains. + + Args: + num_tokens: The number of tokens (not including padding). + asym_ids: The asym_ids (array of shape num_tokens). + full_pde: A [num_samples, num_tokens, num_tokens] matrix of predicted + distance errors. + + Returns: + chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average + per chain-pair predicted distance error. + chain_pair_pred_err_min - a [num_chains, num_chains] matrix with min + per chain-pair predicted distance error. + """ + full_pde = full_pde[:, :num_tokens, :num_tokens] + asym_ids = asym_ids[:num_tokens] + unique_asym_ids = np.unique(asym_ids) + num_chains = len(unique_asym_ids) + num_samples = full_pde.shape[0] + chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains)) + chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains)) + + for idx1, asym_id_1 in enumerate(unique_asym_ids): + subset = full_pde[:, asym_ids == asym_id_1, :] + for idx2, asym_id_2 in enumerate(unique_asym_ids): + subsubset = subset[:, :, asym_ids == asym_id_2] + chain_pair_pred_err_mean[:, idx1, idx2] = np.mean( + subsubset, axis=(1, 2)) + chain_pair_pred_err_min[:, idx1, idx2] = np.min( + subsubset, axis=(1, 2)) + return chain_pair_pred_err_mean, chain_pair_pred_err_min + + +def weighted_nanmean( + value: np.ndarray, mask: np.ndarray, axis: int +) -> np.ndarray: + """Nan-mean with weighting -- empty slices return NaN.""" + assert mask.shape == value.shape + assert not np.isnan(mask).all() + + nan_idxs = np.where(np.isnan(value)) + # Need to NaN the mask to get the correct denominator weighting. + mask_with_nan = mask.copy() + mask_with_nan[nan_idxs] = np.nan + with warnings.catch_warnings(): + # Mean of empty slice is ok and should return a NaN. + warnings.filterwarnings(action='ignore', message='Mean of empty slice') + return np.nanmean(value * mask_with_nan, axis=axis) / np.nanmean( + mask_with_nan, axis=axis + ) + + +def chain_pair_pae( + *, + num_tokens: int, + asym_ids: np.ndarray, + full_pae: np.ndarray, + mask: np.ndarray | None=None, + contact_probs: np.ndarray | None=None, +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Compute predicted errors for all pairs of chains. + + Args: + num_tokens: The number of tokens (not including padding). + asym_ids: The asym_ids (array of shape num_tokens). + full_pae: A [num_samples, num_tokens, num_tokens] matrix of predicted + errors. + mask: A [num_tokens, num_tokens] mask matrix. + contact_probs: A [num_tokens, num_tokens] matrix consisting of the + probability of contact (<8A) that is returned from the distogram head. + + Returns: + chain_pair_pred_err_mean - a [num_chains, num_chains] matrix with average + per chain-pair predicted error. + """ + if mask is None: + mask = np.ones(shape=full_pae.shape[1:], dtype=bool) + if contact_probs is None: + contact_probs = np.ones(shape=full_pae.shape[1:], dtype=float) + assert mask.shape == full_pae.shape[1:] + + full_pae = full_pae[:, :num_tokens, :num_tokens] + mask = mask[:num_tokens, :num_tokens] + asym_ids = asym_ids[:num_tokens] + contact_probs = contact_probs[:num_tokens, :num_tokens] + unique_asym_ids = np.unique(asym_ids) + num_chains = len(unique_asym_ids) + num_samples = full_pae.shape[0] + chain_pair_pred_err_mean = np.zeros((num_samples, num_chains, num_chains)) + chain_pair_pred_err_min = np.zeros((num_samples, num_chains, num_chains)) + + for idx1, asym_id_1 in enumerate(unique_asym_ids): + subset = full_pae[:, asym_ids == asym_id_1, :] + subset_mask = mask[asym_ids == asym_id_1, :] + subset_contact_probs = contact_probs[asym_ids == asym_id_1, :] + for idx2, asym_id_2 in enumerate(unique_asym_ids): + subsubset = subset[:, :, asym_ids == asym_id_2] + subsubset_mask = subset_mask[:, asym_ids == asym_id_2] + subsubset_contact_probs = subset_contact_probs[:, + asym_ids == asym_id_2] + (flat_mask_idxs,) = np.where(subsubset_mask.flatten() > 0) + flat_subsubset = subsubset.reshape([num_samples, -1]) + flat_contact_probs = subsubset_contact_probs.flatten() + # A ligand chain will have no valid frames if it contains fewer than + # three non-colinear atoms (e.g. a sodium ion). + if not flat_mask_idxs.size: + chain_pair_pred_err_mean[:, idx1, idx2] = np.nan + chain_pair_pred_err_min[:, idx1, idx2] = np.nan + else: + chain_pair_pred_err_min[:, idx1, idx2] = np.min( + flat_subsubset[:, flat_mask_idxs], axis=1 + ) + chain_pair_pred_err_mean[:, idx1, idx2] = weighted_mean( + mask=flat_contact_probs[flat_mask_idxs], + value=flat_subsubset[:, flat_mask_idxs], + axis=-1, + ) + return chain_pair_pred_err_mean, chain_pair_pred_err_min, unique_asym_ids + + +def reduce_chain_pair( + *, + chain_pair_met: np.ndarray, + num_chain_tokens: np.ndarray, + agg_over_col: bool, + agg_type: str, + weight_method: str, +) -> tuple[np.ndarray, np.ndarray]: + """Compute 1D summaries from a chain-pair summary. + + Args: + chain_pair_met: A [num_samples, num_chains, num_chains] aggregate matrix. + num_chain_tokens: A [num_chains] array of number of tokens for each chain. + Used for 'per_token' weighting. + agg_over_col: Whether to aggregate the PAE over rows (i.e. average error + when aligned to me) or columns (i.e. my average error when aligned to all + others.) + agg_type: The type of aggregation to use, 'mean' or 'min'. + weight_method: The method to use for weighting the PAE, 'per_token' or + 'per_chain'. + + Returns: + A tuple (ichain, xchain) where: + `ichain` is a [num_samples, num_chains] matrix where the + value assigned to each chain is an average of the full PAE matrix over all + its within-chain interactions, weighted by `contact_probs`. + `xchain` is a [num_samples, num_chains] matrix where the + value assigned to each chain is an average of the full PAE matrix over all + its cross-chain interactions, weighted by `contact_probs`. + """ + num_samples, num_chains, _ = chain_pair_met.shape + + ichain = chain_pair_met.diagonal(axis1=-2, axis2=-1) + + if weight_method == 'per_chain': + chain_weight = np.ones((num_chains,), dtype=float) + elif weight_method == 'per_token': + chain_weight = num_chain_tokens + else: + raise ValueError(f'Unknown weight method: {weight_method}') + + if agg_over_col: + agg_axis = -1 + else: + agg_axis = -2 + + if agg_type == 'mean': + weight = np.ones((num_samples, num_chains, num_chains), dtype=float) + weight -= np.eye(num_chains, dtype=float) + weight *= chain_weight[None] * chain_weight[:, None] + xchain = weighted_nanmean(chain_pair_met, mask=weight, axis=agg_axis) + elif agg_type == 'min': + is_self = np.eye(num_chains) + with warnings.catch_warnings(): + # Min over empty slice is ok and should return a NaN. + warnings.filterwarnings( + 'ignore', message='All-NaN slice encountered') + xchain = np.nanmin(chain_pair_met + 1e8 * is_self, axis=agg_axis) + else: + raise ValueError(f'Unknown aggregation method: {agg_type}') + + return ichain, xchain + + +def pae_metrics( + num_tokens: int, + asym_ids: np.ndarray, + full_pae: np.ndarray, + mask: np.ndarray, + contact_probs: np.ndarray, + tm_adjusted_pae: np.ndarray, +): + """PAE aggregate metrics.""" + assert mask.shape == full_pae.shape[1:] + assert contact_probs.shape == full_pae.shape[1:] + + chain_pair_contact_weighted, _, unique_asym_ids = chain_pair_pae( + num_tokens=num_tokens, + asym_ids=asym_ids, + full_pae=full_pae, + mask=mask, + contact_probs=contact_probs, + ) + + ret = {} + ret['chain_pair_pae_mean'], ret['chain_pair_pae_min'], _ = chain_pair_pae( + num_tokens=num_tokens, + asym_ids=asym_ids, + full_pae=full_pae, + mask=mask, + ) + chain_pair_iptm = np.stack( + [ + chain_pairwise_predicted_tm_scores( + tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens], + asym_id=asym_ids[:num_tokens], + pair_mask=mask[:num_tokens, :num_tokens], + ) + for sample_tm_adjusted_pae in tm_adjusted_pae + ], + axis=0, + ) + + num_chain_tokens = np.array( + [sum(asym_ids == asym_id) for asym_id in unique_asym_ids] + ) + + def reduce_chain_pair_fn(chain_pair: np.ndarray): + def inner(agg_over_col): + ichain_pae, xchain_pae = reduce_chain_pair( + num_chain_tokens=num_chain_tokens, + chain_pair_met=chain_pair, + agg_over_col=agg_over_col, + agg_type='mean', + weight_method='per_chain', + ) + return ichain_pae, xchain_pae + + ichain, xchain_row_agg = inner(False) + _, xchain_col_agg = inner(True) + with warnings.catch_warnings(): + # Mean of empty slice is ok and should return a NaN. + warnings.filterwarnings( + action='ignore', message='Mean of empty slice') + xchain = np.nanmean( + np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0 + ) + return ichain, xchain + + pae_ichain, pae_xchain = reduce_chain_pair_fn(chain_pair_contact_weighted) + iptm_ichain, iptm_xchain = reduce_chain_pair_fn(chain_pair_iptm) + + ret.update({ + 'chain_pair_iptm': chain_pair_iptm, + 'iptm_ichain': iptm_ichain, + 'iptm_xchain': iptm_xchain, + 'pae_ichain': pae_ichain, + 'pae_xchain': pae_xchain, + }) + + return ret + + +def get_iptm_xchain(chain_pair_iptm: np.ndarray) -> np.ndarray: + """Cross chain aggregate ipTM.""" + num_samples, num_chains, _ = chain_pair_iptm.shape + weight = np.ones((num_samples, num_chains, num_chains), dtype=float) + weight -= np.eye(num_chains, dtype=float) + xchain_row_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-2) + xchain_col_agg = weighted_nanmean(chain_pair_iptm, mask=weight, axis=-1) + with warnings.catch_warnings(): + # Mean of empty slice is ok and should return a NaN. + warnings.filterwarnings(action='ignore', message='Mean of empty slice') + iptm_xchain = np.nanmean( + np.stack([xchain_row_agg, xchain_col_agg], axis=0), axis=0 + ) + return iptm_xchain + + +def predicted_tm_score( + tm_adjusted_pae: np.ndarray, + pair_mask: np.ndarray, + asym_id: np.ndarray, + interface: bool = False, +) -> float: + """Computes predicted TM alignment or predicted interface TM alignment score. + + Args: + tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore + values. + pair_mask: A [num_res, num_res] mask. The TM score will only aggregate over + masked-on entries. + asym_id: [num_res] asymmetric unit ID (the chain ID). Only needed for ipTM + calculation, i.e. when interface=True. + interface: If True, the interface predicted TM score is computed. If False, + the predicted TM score without any residue pair restrictions is computed. + + Returns: + score: pTM or ipTM score. + """ + num_tokens, _ = tm_adjusted_pae.shape + if tm_adjusted_pae.shape != (num_tokens, num_tokens): + raise ValueError( + f'Bad tm_adjusted_pae shape, expected ({num_tokens, num_tokens}), got ' + f'{tm_adjusted_pae.shape}.' + ) + + if pair_mask.shape != (num_tokens, num_tokens): + raise ValueError( + f'Bad pair_mask shape, expected ({num_tokens, num_tokens}), got ' + f'{pair_mask.shape}.' + ) + if pair_mask.dtype != bool: + raise TypeError( + f'Bad pair mask type, expected bool, got {pair_mask.dtype}') + if asym_id.shape[0] != num_tokens: + raise ValueError( + f'Bad asym_id shape, expected ({num_tokens},), got {asym_id.shape}.' + ) + + # Create pair mask. + if interface: + pair_mask = pair_mask * (asym_id[:, None] != asym_id[None, :]) + + # Ions and other ligands with colinear atoms have ill-defined frames. + if pair_mask.sum() == 0: + return np.nan + + normed_residue_mask = pair_mask / ( + 1e-8 + np.sum(pair_mask, axis=-1, keepdims=True) + ) + per_alignment = np.sum(tm_adjusted_pae * normed_residue_mask, axis=-1) + return per_alignment.max() + + +def chain_pairwise_predicted_tm_scores( + tm_adjusted_pae: np.ndarray, + pair_mask: np.ndarray, + asym_id: np.ndarray, +) -> np.ndarray: + """Compute predicted TM (pTM) between each pair of chains independently. + + Args: + tm_adjusted_pae: [num_res, num_res] Relevant tensor for computing TMScore + values. + pair_mask: A [num_res, num_res] mask specifying which frames are valid. + Invalid frames can be the result of chains with not enough atoms (e.g. + ions). + asym_id: [num_res] asymmetric unit ID (the chain ID). + + Returns: + A [num_chains, num_chains] matrix, where row i, column j indicates the + predicted TM-score for the interface between chain i and chain j. + """ + unique_chains = list(np.unique(asym_id)) + num_chains = len(unique_chains) + all_pairs_iptms = np.zeros((num_chains, num_chains)) + for i, chain_i in enumerate(unique_chains): + chain_i_mask = asym_id == chain_i + for j, chain_j in enumerate(unique_chains[i:]): + chain_j_mask = asym_id == chain_j + mask = chain_i_mask | chain_j_mask + (indices,) = np.where(mask) + is_interface = chain_i != chain_j + indices = np.ix_(indices, indices) + iptm = predicted_tm_score( + tm_adjusted_pae=tm_adjusted_pae[indices], + pair_mask=pair_mask[indices], + asym_id=asym_id[mask], + interface=is_interface, + ) + all_pairs_iptms[i, i + j] = iptm + all_pairs_iptms[i + j, i] = iptm + return all_pairs_iptms diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data3.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data3.py new file mode 100644 index 000000000..26c799387 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data3.py @@ -0,0 +1,127 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Protein features that are computed from parsed mmCIF objects.""" + +from collections.abc import Mapping, MutableMapping +import datetime +from typing import TypeAlias + +from alphafold3.constants import residue_names +from alphafold3.cpp import msa_profile +from alphafold3.model import protein_data_processing +import numpy as np + + +FeatureDict: TypeAlias = Mapping[str, np.ndarray] +MutableFeatureDict: TypeAlias = MutableMapping[str, np.ndarray] + + +def fix_features(msa_features: MutableFeatureDict) -> MutableFeatureDict: + """Renames the deletion_matrix feature.""" + msa_features['deletion_matrix'] = msa_features.pop('deletion_matrix_int') + return msa_features + + +def get_profile_features( + msa: np.ndarray, deletion_matrix: np.ndarray +) -> FeatureDict: + """Returns the MSA profile and deletion_mean features.""" + num_restypes = residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + profile = msa_profile.compute_msa_profile( + msa=msa, num_residue_types=num_restypes + ) + + return { + 'profile': profile.astype(np.float32), + 'deletion_mean': np.mean(deletion_matrix, axis=0), + } + + +def fix_template_features( + sequence: str, + template_features: FeatureDict, +) -> FeatureDict: + """Convert template features to AlphaFold 3 format. + + Args: + sequence: amino acid sequence of the protein. + template_features: Template features for the protein. + + Returns: + Updated template_features for the chain. + """ + num_res = len(sequence) + if not template_features['template_aatype'].shape[0]: + template_features = empty_template_features(num_res) + else: + template_release_timestamp = [ + _get_timestamp(x.decode('utf-8')) + for x in template_features['template_release_date'] + ] + + # Convert from atom37 to dense atom + dense_atom_indices = np.take( + protein_data_processing.PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37, + template_features['template_aatype'], + axis=0, + ) + + atom_mask = np.take_along_axis( + template_features['template_all_atom_masks'], dense_atom_indices, axis=2 + ) + atom_positions = np.take_along_axis( + template_features['template_all_atom_positions'], + dense_atom_indices[..., None], + axis=2, + ) + atom_positions *= atom_mask[..., None] + + template_features = { + 'template_aatype': template_features['template_aatype'], + 'template_atom_mask': atom_mask.astype(np.int32), + 'template_atom_positions': atom_positions.astype(np.float32), + 'template_domain_names': np.array( + template_features['template_domain_names'], dtype=object + ), + 'template_release_timestamp': np.array( + template_release_timestamp, dtype=np.float32 + ), + } + return template_features + + +def empty_template_features(num_res: int) -> FeatureDict: + """Creates a fully masked out template features to allow padding to work. + + Args: + num_res: The length of the target chain. + + Returns: + Empty template features for the chain. + """ + template_features = { + 'template_aatype': np.zeros(num_res, dtype=np.int32)[None, ...], + 'template_atom_mask': np.zeros( + (num_res, protein_data_processing.NUM_DENSE), dtype=np.int32 + )[None, ...], + 'template_atom_positions': np.zeros( + (num_res, protein_data_processing.NUM_DENSE, 3), dtype=np.float32 + )[None, ...], + 'template_domain_names': np.array([b''], dtype=object), + 'template_release_timestamp': np.array([0.0], dtype=np.float32), + } + return template_features + + +def _get_timestamp(date_str: str): + dt = datetime.datetime.fromisoformat(date_str) + dt = dt.replace(tzinfo=datetime.timezone.utc) + return dt.timestamp() diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data_constants.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data_constants.py new file mode 100644 index 000000000..eabdcfda9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data_constants.py @@ -0,0 +1,27 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Constants shared across modules in the AlphaFold data pipeline.""" + +from alphafold3.constants import residue_names + +MSA_GAP_IDX = residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP.index( + '-' +) + +# Feature groups. +NUM_SEQ_NUM_RES_MSA_FEATURES = ('msa', 'msa_mask', 'deletion_matrix') +NUM_SEQ_MSA_FEATURES = ('msa_species_identifiers',) +TEMPLATE_FEATURES = ( + 'template_aatype', + 'template_atom_positions', + 'template_atom_mask', +) +MSA_PAD_VALUES = {'msa': MSA_GAP_IDX, 'msa_mask': 1, 'deletion_matrix': 0} diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py new file mode 100644 index 000000000..f10cbfd0b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py @@ -0,0 +1,466 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +from dataclasses import dataclass +import mindspore as ms +from mindspore import nn, ops, Tensor + +from alphafold3.model import base_config +from alphafold3.model.atom_layout import atom_layout +from alphafold3.model.components import base_modules as bm +from alphafold3.model.components import utils +from alphafold3.model.diffusion import diffusion_transformer + +@dataclass +class AtomCrossAttEncoderConfig(base_config.BaseConfig): + per_token_channels: int = 768 + per_atom_channels: int = 128 + atom_transformer: diffusion_transformer.CrossAttTransformer.Config = ( + base_config.autocreate(num_intermediate_factor=2, num_blocks=3) + ) + per_atom_pair_channels: int = 16 + + +class _PerAtomConditioning(nn.Cell): + """ + A class to compute per-atom and pairwise conditioning information for structural data. + + Args: + config: Configuration object containing model parameters. + + Inputs: + - **batch** (dict) - A dictionary containing structural information: + - **ref_structure.positions** (Tensor) - Tensor of atomic positions. + - **ref_structure.mask** (Tensor) - Tensor of masks indicating valid atoms. + - **ref_structure.element** (Tensor) - Tensor of atomic elements. + - **ref_structure.charge** (Tensor) - Tensor of atomic charges. + - **ref_structure.atom_name_chars** (Tensor) - Tensor of atomic name characters. + + Outputs: + - **act** (Tensor) - Per-atom conditioning information. + - **pair_act** (Tensor) - Pairwise conditioning information. + """ + + def __init__(self, config): + super().__init__() + self.c = config + self.linear1 = nn.Dense(3, self.c.per_atom_channels, has_bias=False) + self.linear2 = nn.Dense(1, self.c.per_atom_channels, has_bias=False) + self.linear3 = nn.Dense(128, self.c.per_atom_channels, has_bias=False) + self.linear4 = nn.Dense(1, self.c.per_atom_channels, has_bias=False) + self.linear5 = nn.Dense(256, self.c.per_atom_channels, has_bias=False) + self.linear_row_act = nn.Dense( + self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False) + self.linear_col_act = nn.Dense( + self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False) + self.linear_pair_act1 = nn.Dense( + 3, self.c.per_atom_pair_channels, has_bias=False) + self.linear_pair_act2 = nn.Dense( + 1, self.c.per_atom_pair_channels, has_bias=False) + + @ms.jit + def construct(self, batch): + # Compute per-atom single conditioning + # Shape (num_tokens, num_dense, channels) + act = self.linear1(batch.ref_structure.positions) + act += self.linear2(batch.ref_structure.mask[:, :, None]) + # Element is encoded as atomic number if the periodic table, so + # 128 should be fine. + act += self.linear3( + ops.one_hot(batch.ref_structure.element, 128, + Tensor(1.0, ms.float32), Tensor(0.0, ms.float32)) + .astype(act.dtype)) + act += self.linear4(ops.arcsinh(batch.ref_structure.charge) + [:, :, None]) + # Characters are encoded as ASCII code minus 32, so we need 64 classes, + # to encode all standard ASCII characters between 32 and 96. + atom_name_chars_1hot = ops.one_hot(batch.ref_structure.atom_name_chars, 64, + Tensor(1.0, ms.float32), Tensor(0.0, ms.float32)).astype(act.dtype) + num_token, num_dense, _ = act.shape + act += self.linear5(atom_name_chars_1hot.reshape(num_token, num_dense, -1)) + act *= batch.ref_structure.mask[:, :, None] + + # Compute pair conditioning + # shape (num_tokens, num_dense, num_dense, channels) + # Embed single features + row_act = self.linear_row_act(ops.relu(act)) + col_act = self.linear_col_act(ops.relu(act)) + pair_act = row_act[:, :, None, :] + col_act[:, None, :, :] + + # Embed pairwise offsets + pair_act += self.linear_pair_act1(batch.ref_structure.positions[:, :, None, :] + - batch.ref_structure.positions[:, None, :, :]) + # Embed pairwise inverse squared distances + sq_dists = ops.sum(ops.square(batch.ref_structure.positions[:, :, None, :] + - batch.ref_structure.positions[:, None, :, :]), dim=-1) + pair_act += self.linear_pair_act2(1.0 / (1 + sq_dists[:, :, :, None])) + return act, pair_act + +@dataclass +class AtomCrossAttEncoderOutput: + def __init__( + self, + token_act, + skip_connection, + queries_mask, + queries_single_cond, + keys_mask, + keys_single_cond, + pair_cond, + ): + self.token_act = token_act + self.skip_connection = skip_connection + self.queries_mask = queries_mask + self.queries_single_cond = queries_single_cond + self.keys_mask = keys_mask + self.keys_single_cond = keys_single_cond + self.pair_cond = pair_cond + + +class AtomCrossAttEncoder(nn.Cell): + """Cross-attention on flat atom subsets and mapping to per-token features. + + Args: + config: Configuration object containing model parameters. + global_config: Global configuration object with initialization settings. + name (str): Name of the module. + cond_channels (int): Number of conditioning channels. Default: ``384``. + with_cond (bool): Whether to include conditioning layers. Default: ``True``. + + Inputs: + - **token_atoms_act** (ms.Tensor): Tensor representing token atom activations. + - **trunk_single_cond** (ms.Tensor): Tensor representing single token conditioning. + - **trunk_pair_cond** (ms.Tensor): Tensor representing pair token conditioning. + - **batch** (feat_batch.Batch) : Batch of input data. + + Outputs: + - **token_act** (ms.Tensor): Activations for tokens after processing. + - **skip_connection** (ms.Tensor): Skip connection tensor for token queries. + - **queries_mask** (ms.Tensor): Mask for token queries. + - **queries_single_cond** (ms.Tensor): Single conditioning for token queries. + - **keys_mask** (ms.Tensor): Mask for token keys. + - **keys_single_cond** (ms.Tensor): Single conditioning for token keys. + - **pair_cond** (ms.Tensor): Pair conditioning tensor. + """ + + def __init__(self, config, global_config, name, cond_channels=384, with_cond=True, dtype=ms.float32): + super().__init__() + self.c = config + self.with_cond = with_cond + self.dtype = dtype + self._per_atom_conditioning = _PerAtomConditioning(config) + if self.with_cond: + self._embed_trunk_single_cond = nn.Dense(cond_channels, self.c.per_atom_channels, + weight_init=global_config.final_init, has_bias=False, dtype=dtype) + self._lnorm_trunk_single_cond = bm.LayerNorm((cond_channels,), + create_beta=False, gamma_init="ones", dtype=dtype) + self._atom_positions_to_features = nn.Dense(3, self.c.per_atom_channels, has_bias=False, dtype=dtype) + self._embed_trunk_pair_cond = nn.Dense(self.c.per_atom_channels, self.c.per_atom_pair_channels, + weight_init=global_config.final_init, has_bias=False, dtype=dtype) + self._lnorm_trunk_pair_cond = bm.LayerNorm((self.c.per_atom_channels,), create_beta=False, + gamma_init="ones", dtype=dtype) + + self._single_to_pair_cond_row = nn.Dense( + self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + self._single_to_pair_cond_col = nn.Dense( + self.c.per_atom_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + + self._embed_pair_offsets = nn.Dense( + 3, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + self._embed_pair_distances = nn.Dense( + 1, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + self._embed_pair_offsets_valid = nn.Dense( + 1, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + + self._pair_mlp_1 = nn.Dense( + self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + self._pair_mlp_2 = nn.Dense( + self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, has_bias=False, dtype=dtype) + self._pair_mlp_3 = nn.Dense(self.c.per_atom_pair_channels, self.c.per_atom_pair_channels, + weight_init=global_config.final_init, has_bias=False, dtype=dtype) + self.relu = nn.ReLU() + self._project_atom_features_for_aggr = nn.Dense( + self.c.per_atom_channels, self.c.per_token_channels, has_bias=False, dtype=dtype) + + self._atom_transformer_encoder = diffusion_transformer.CrossAttTransformer( + self.c.atom_transformer, global_config, in_shape=[ + self.c.per_atom_channels, self.c.per_atom_pair_channels], dtype=dtype + ) + + def construct( + self, + token_atoms_act, + trunk_single_cond, + trunk_pair_cond, + batch, + ): + # Compute single conditioning from atom meta data and convert to queries + # layout. + token_atoms_single_cond, _ = self._per_atom_conditioning( + batch) + token_atoms_mask = batch.predicted_structure_info.atom_mask + queries_single_cond = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + token_atoms_single_cond, + layout_axes=(-3, -2), + ) + queries_mask = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + token_atoms_mask, + layout_axes=(-2, -1), + ) + + # If provided, broadcast single conditioning from trunk to all queries + if trunk_single_cond is not None: + trunk_single_cond = self._embed_trunk_single_cond( + self._lnorm_trunk_single_cond( + trunk_single_cond) + ) + queries_single_cond += atom_layout.convert_ms( + batch.atom_cross_att.tokens_to_queries, + trunk_single_cond, + layout_axes=(-2,), + ) + + if token_atoms_act is None: + # if no token_atoms_act is given (e.g. begin of evoformer), we use the + # static conditioning only + queries_act = queries_single_cond + else: + # Convert token_atoms_act to queries layout and map to per_atom_channels + queries_act = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + token_atoms_act, + layout_axes=(-3, -2), + ) + queries_act = self._atom_positions_to_features( + queries_act) + queries_act *= queries_mask[..., None] + queries_act += queries_single_cond + + # Gather the keys from the queries. + keys_single_cond = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_keys, queries_single_cond, layout_axes=( + -3, -2), + ) + keys_mask = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_keys, queries_mask, layout_axes=( + -2, -1) + ) + + # Embed single features into the pair conditioning. + row_act = self._single_to_pair_cond_row( + self.relu(queries_single_cond)) + pair_cond_keys_input = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_keys, queries_single_cond, layout_axes=( + -3, -2), + ) + col_act = self._single_to_pair_cond_col( + self.relu(pair_cond_keys_input)) + pair_act = row_act[:, :, None, :] + col_act[:, None, :, :] + + if trunk_pair_cond is not None: + # If provided, broadcast the pair conditioning for the trunk (evoformer + # pairs) to the atom pair activations. This should boost ligands, but also + # help for cross attention within proteins, because we always have atoms + # from multiple residues in a subset. + # Map trunk pair conditioning to per_atom_pair_channels + trunk_pair_cond = self._embed_trunk_pair_cond( + self._lnorm_trunk_pair_cond( + trunk_pair_cond) + ) + + # Create the GatherInfo into a flattened trunk_pair_cond from the + # queries and keys gather infos. + num_tokens = trunk_pair_cond.shape[0] + tokens_to_queries = batch.atom_cross_att.tokens_to_queries + tokens_to_keys = batch.atom_cross_att.tokens_to_keys + + # Gather the conditioning and add it to the atom-pair activations. + gather_idxs = Tensor(num_tokens * tokens_to_queries.gather_idxs[:, :, None] + + tokens_to_keys.gather_idxs[:, None, :]) + gather_mask = ops.logical_and(tokens_to_queries.gather_mask[:, :, None], + tokens_to_keys.gather_mask[:, None, :]) + input_shape = Tensor((num_tokens, num_tokens)) + trunk_pair_to_atom_pair = atom_layout.GatherInfo(gather_idxs=gather_idxs, + gather_mask=gather_mask, + input_shape=input_shape) + pair_act += atom_layout.convert_ms( + trunk_pair_to_atom_pair, trunk_pair_cond, layout_axes=(-3, -2) + ) + + # Embed pairwise offsets + queries_ref_pos = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + batch.ref_structure.positions, + layout_axes=(-3, -2), + ) + queries_ref_space_uid = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + batch.ref_structure.ref_space_uid, + layout_axes=(-2, -1), + ) + keys_ref_pos = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_keys, + queries_ref_pos, + layout_axes=(-3, -2), + ) + keys_ref_space_uid = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_keys, + batch.ref_structure.ref_space_uid, + layout_axes=(-2, -1), + ) + + offsets_valid = ( + queries_ref_space_uid[:, :, None] == keys_ref_space_uid[:, None, :] + ) + offsets = queries_ref_pos[:, :, None, :] - keys_ref_pos[:, None, :, :] + pair_act += (self._embed_pair_offsets(offsets) + * offsets_valid[:, :, :, None]) + + # Embed pairwise inverse squared distances + sq_dists = ops.sum(ops.square(offsets), dim=-1) + pair_act += ( + self._embed_pair_distances(1.0 / (1 + sq_dists[:, :, :, None])) + * offsets_valid[:, :, :, None] + ) + + # Embed offsets valid mask + pair_act += self._embed_pair_offsets_valid( + offsets_valid[:, :, :, None].astype(ms.float32)) + + # Run a small MLP on the pair acitvations + pair_act2 = self._pair_mlp_1(self.relu(pair_act)) + pair_act2 = self._pair_mlp_2(self.relu(pair_act2)) + pair_act += self._pair_mlp_3(self.relu(pair_act2)) + + # Run the atom cross attention transformer. + queries_act = self._atom_transformer_encoder( + queries_act=queries_act, + queries_mask=queries_mask, + queries_to_keys=batch.atom_cross_att.queries_to_keys, + keys_mask=keys_mask, + queries_single_cond=queries_single_cond, + keys_single_cond=keys_single_cond, + pair_cond=pair_act, + ) + queries_act *= queries_mask[..., None] + skip_connection = queries_act + + # convert back to token-atom layout and aggregate to tokens + queries_act = self._project_atom_features_for_aggr(queries_act) + token_atoms_act = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_token_atoms, + queries_act, + layout_axes=(-3, -2), + ) + token_act = utils.mask_mean( + token_atoms_mask[..., None], self.relu(token_atoms_act), axis=-2 + ) + + return AtomCrossAttEncoderOutput( + token_act=token_act, + skip_connection=skip_connection, + queries_mask=queries_mask, + queries_single_cond=queries_single_cond, + keys_mask=keys_mask, + keys_single_cond=keys_single_cond, + pair_cond=pair_act, + ) + +@dataclass +class AtomCrossAttDecoderConfig(base_config.BaseConfig): + per_token_channels: int = 768 + per_atom_channels: int = 128 + per_atom_pair_channels: int = 16 + atom_transformer: diffusion_transformer.CrossAttTransformer.Config = ( + base_config.autocreate(num_intermediate_factor=2, num_blocks=3) + ) + + +class AtomCrossAttDecoder(nn.Cell): + """Mapping to per-atom features and self-attention on subsets. + + Args: + config: Configuration object containing model parameters. + global_config: Global configuration object with additional parameters. + name (str): Name of the decoder. Default: ``None``. + + Inputs: + - **token_act** (Tensor) - Tensor representing token activations. + - **enc** (AtomCrossAttEncoderOutput) - Output from the encoder containing necessary features and masks. + - **batch** (feat_batch.Batch) - Batch containing atom cross attention features. + + Outputs: + - **position_update** (Tensor) - Tensor representing the updated positions after processing. + """ + + def __init__(self, config, global_config, name, dtype=ms.float32): + super().__init__() + self.c = config + self._project_token_features_for_broadcast = nn.Dense( + self.c.per_token_channels, self.c.per_atom_channels, has_bias=False, dtype=dtype) + self._atom_features_layer_norm = bm.LayerNorm( + (self.c.per_atom_channels,), create_beta=False, gamma_init="ones", dtype=dtype) + self._atom_features_to_position_update = nn.Dense( + self.c.per_atom_channels, 3, weight_init=global_config.final_init, has_bias=False, dtype=dtype) + self._atom_transformer_decoder = diffusion_transformer.CrossAttTransformer( + self.c.atom_transformer, global_config, in_shape=[ + self.c.per_atom_channels, self.c.per_atom_pair_channels], dtype=dtype + ) + + def construct( + self, + token_act, + enc, + batch, + ): + # map per-token act down to per_atom channels + token_act = self._project_token_features_for_broadcast(token_act) + # Broadcast to token-atoms layout and convert to queries layout. + num_token, max_atoms_per_token = ( + batch.atom_cross_att.queries_to_token_atoms.shape + ) + token_atom_act = ops.broadcast_to( + token_act[:, None, :], + (num_token, max_atoms_per_token, self.c.per_atom_channels), + ) + queries_act = atom_layout.convert_ms( + batch.atom_cross_att.token_atoms_to_queries, + token_atom_act, + layout_axes=(-3, -2), + ) + queries_act += enc.skip_connection + queries_act *= enc.queries_mask[..., None] + + # Run the atom cross attention transformer. + queries_act = self._atom_transformer_decoder( + queries_act=queries_act, + queries_mask=enc.queries_mask, + queries_to_keys=batch.atom_cross_att.queries_to_keys, + keys_mask=enc.keys_mask, + queries_single_cond=enc.queries_single_cond, + keys_single_cond=enc.keys_single_cond, + pair_cond=enc.pair_cond, + ) + + queries_act *= enc.queries_mask[..., None] + queries_position_update = self._atom_features_to_position_update( + self._atom_features_layer_norm(queries_act) + ) + position_update = atom_layout.convert_ms( + batch.atom_cross_att.queries_to_token_atoms, + queries_position_update, + layout_axes=(-3, -2), + ) + return position_update diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/confidence_head.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/confidence_head.py new file mode 100644 index 000000000..3cd568129 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/confidence_head.py @@ -0,0 +1,289 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Confidence Head.""" +from dataclasses import dataclass +import mindspore as ms +from mindspore import nn, ops +from alphafold3.model import base_config +from alphafold3.model.atom_layout import atom_layout +from alphafold3.model.components import base_modules as bm +from alphafold3.model.diffusion import modules +from alphafold3.model.diffusion import template_modules + + +def _safe_norm(x, keepdims, axis, eps=1e-8): + return ops.sqrt(eps + ops.sum(ops.square(x), dim=axis, keepdims=keepdims)) + + +class ConfidenceHead(nn.Cell): + """Head to predict the distance errors in a prediction. + + Args: + config (ConfidenceHead.Config): Configuration for the ConfidenceHead module. + global_config (base_config.BaseConfig): Global configuration for the model. + pair_shape (tuple): Shape of the pair features. + single_shape (tuple): Shape of the single features. + atom_shape (tuple): Shape of the atom features. + feat_in_channel (int): Number of input channels for feature projections. + out_channel (int): Number of output channels for feature projections. + + Inputs: + - **dense_atom_positions** (Tensor): [N_res, N_atom, 3] array of atom positions. + - **embeddings** (dict): Dictionary containing pair, single, and target features. + - **seq_mask** (Tensor): Sequence mask indicating valid residues. + - **token_atoms_to_pseudo_beta** (Tensor): Pseudo beta information for atom tokens. + - **asym_id** (Tensor): Asym ID token features. + + Outputs: + - **predicted_lddt** (Tensor): Predicted LDDT scores for each residue. + - **predicted_experimentally_resolved** (Tensor): Predicted experimental resolution scores. + - **full_pde** (Tensor): Full predicted distance errors. + - **average_pde** (Tensor): Average predicted distance errors. + - **pae_outputs** (dict): Additional outputs from PAE (Predicted Alignment Error) calculations. + """ + @dataclass + class PAEConfig(base_config.BaseConfig): + max_error_bin: float = 31.0 + num_bins: int = 64 + + @dataclass + class Config(base_config.BaseConfig): + """Configuration for ConfidenceHead.""" + + pairformer: modules.PairFormerIteration.Config = base_config.autocreate( + single_attention=base_config.autocreate(), + single_transition=base_config.autocreate(), + num_layer=4, + ) + max_error_bin: float = 31.0 + num_plddt_bins: int = 50 + num_bins: int = 64 + no_embedding_prob: float = 0.2 + pae: 'ConfidenceHead.PAEConfig' = base_config.autocreate() + dgram_features: template_modules.DistogramFeaturesConfig = ( + base_config.autocreate() + ) + + def __init__(self, config, global_config, pair_shape, single_shape, atom_shape, + feat_in_channel, out_channel, dtype=ms.float32): + super().__init__() + self.dtype = dtype + self.config = config + self.global_config = global_config + self.left_target_feat_project = nn.Dense( + feat_in_channel, out_channel, has_bias=False, dtype=dtype) + self.right_target_feat_project = nn.Dense( + feat_in_channel, out_channel, has_bias=False, dtype=dtype) + self.distogram_feat_project = nn.Dense( + template_modules.DistogramFeaturesConfig.num_bins, out_channel, has_bias=False, dtype=dtype) + self.pairformer_block = ms.nn.CellList( + [ + modules.PairFormerIteration( + self.config.pairformer, global_config, pair_shape, single_shape, with_single=True, dtype=dtype + ) + for _ in range(self.config.pairformer.num_layer) + ] + ) + self.left_half_distance_logits = nn.Dense( + pair_shape[-1], self.config.num_bins, has_bias=False, dtype=ms.float32) + self.logits_ln = bm.LayerNorm(pair_shape, dtype=ms.float32) + self.pae_logits = nn.Dense( + pair_shape[-1], self.config.pae.num_bins, has_bias=False, dtype=ms.float32) + self.pae_logits_ln = bm.LayerNorm(pair_shape, dtype=ms.float32) + self.plddt_logits = bm.CustomDense( + single_shape[-1], (atom_shape[-2], self.config.num_plddt_bins), ndim=2, dtype=ms.float32) + self.plddt_logits_ln = bm.LayerNorm(single_shape, dtype=ms.float32) + self.experimentally_resolved_logits = bm.CustomDense( + single_shape[-1], (atom_shape[-2], 2), ndim=2, dtype=ms.float32) + self.experimentally_resolved_ln = bm.LayerNorm(single_shape, dtype=ms.float32) + + def _embed_features(self, dense_atom_positions, token_atoms_to_pseude_beta, + pair_mask, target_feat): + out = self.left_target_feat_project(target_feat) + out2 = self.right_target_feat_project(target_feat)[:, None] + out = out + out2 + positions = atom_layout.convert_ms( + token_atoms_to_pseude_beta, + dense_atom_positions, + layout_axes=(-3, -2), + ) + dgram = template_modules.dgram_from_positions( + positions, self.config.dgram_features, dtype=ms.float32 + ) + dgram *= pair_mask[..., None] + out += self.distogram_feat_project(dgram) + return out + + def construct(self, dense_atom_positions, embeddings, seq_mask, + token_atoms_to_pseudo_beta, asym_id): + seq_mask_cast = seq_mask + pair_mask = seq_mask_cast[:, None] * seq_mask_cast[None, :] + pair_act = embeddings['pair'] + single_act = embeddings['single'] + target_feat = embeddings['target_feat'] + pair_act += self._embed_features( + dense_atom_positions, + token_atoms_to_pseudo_beta, + pair_mask, + target_feat, + ) + + for i in range(self.config.pairformer.num_layer): + pair_act, single_act = self.pairformer_block[i]( + pair_act, pair_mask, single_act, seq_mask) + pair_act = pair_act.astype(ms.float32) + + # Produce logits to predict a distogram of pairwise distance errors + # between the input prediction and the ground truth. + # Shape (num_res, num_res, num_bins) + left_distance_logits = self.left_half_distance_logits( + self.logits_ln(pair_act)) + right_distance_logits = left_distance_logits + distance_logits = left_distance_logits + ops.swapaxes( # Symmetrize. + right_distance_logits, -2, -3 + ) + # Shape (num_bins,) + distance_breaks = ops.linspace( + 0.0, self.config.max_error_bin, self.config.num_bins - 1 + ) + + step = distance_breaks[1] - distance_breaks[0] + + # Add half-step to get the center + bin_centers = distance_breaks + step / 2 + # Add a catch-all bin at the end. + bin_centers = ops.concat( + [bin_centers, bin_centers[-1:] + step], axis=0 + ) + + distance_probs = ops.softmax(distance_logits, axis=-1) + + pred_distance_error = ( + ops.sum(distance_probs * bin_centers, dim=-1) * pair_mask + ) + average_pred_distance_error = ops.sum( + pred_distance_error, dim=[-2, -1] + ) / ops.sum(pair_mask, dim=[-2, -1]) + + # Predicted aligned error + pae_outputs = {} + # Shape (num_res, num_res, num_bins) + pae_logits = self.pae_logits(self.pae_logits_ln(pair_act)) + # Shape (num_bins,) + pae_breaks = ops.linspace( + 0.0, self.config.pae.max_error_bin, self.config.pae.num_bins - 1 + ) + step = pae_breaks[1] - pae_breaks[0] + # Add half-step to get the center + bin_centers = pae_breaks + step / 2 + # Add a catch-all bin at the end. + bin_centers = ops.concat( + [bin_centers, bin_centers[-1:] + step], axis=0 + ) + pae_probs = ops.softmax(pae_logits, axis=-1) + + seq_mask_bool = seq_mask.astype(bool) + pair_mask_bool = seq_mask_bool[:, None] * seq_mask_bool[None, :] + pae = ops.sum(pae_probs * bin_centers, dim=-1) * pair_mask_bool + pae_outputs.update({ + 'full_pae': pae, + }) + + # The pTM is computed outside of bfloat16 context. + tmscore_adjusted_pae_global, tmscore_adjusted_pae_interface = ( + self._get_tmscore_adjusted_pae( + asym_id=asym_id, + seq_mask=seq_mask, + pair_mask=pair_mask_bool, + bin_centers=bin_centers, + pae_probs=pae_probs, + ) + ) + pae_outputs.update({ + 'tmscore_adjusted_pae_global': tmscore_adjusted_pae_global, + 'tmscore_adjusted_pae_interface': tmscore_adjusted_pae_interface, + }) + + # pLDDT + # Shape (num_res, num_atom, num_bins) + plddt_logits = self.plddt_logits(self.plddt_logits_ln(single_act)) + + bin_width = 1.0 / self.config.num_plddt_bins + bin_centers = ops.arange(0.5 * bin_width, 1.0, bin_width) + predicted_lddt = ops.sum( + ops.softmax(plddt_logits, axis=-1) * bin_centers, dim=-1 + ) + predicted_lddt = predicted_lddt * 100.0 + + # Experimentally resolved + # Shape (num_res, num_atom, 2) + experimentally_resolved_logits = self.experimentally_resolved_logits( + self.experimentally_resolved_ln(single_act) + ) + + predicted_experimentally_resolved = ops.softmax( + experimentally_resolved_logits, axis=-1 + )[..., 1] + + return { + 'predicted_lddt': predicted_lddt, + 'predicted_experimentally_resolved': predicted_experimentally_resolved, + 'full_pde': pred_distance_error, + 'average_pde': average_pred_distance_error, + **pae_outputs, + } + + def _get_tmscore_adjusted_pae( + self, asym_id, seq_mask, pair_mask, bin_centers, pae_probs, + ): + def get_tmscore_adjusted_pae(num_interface_tokens, bin_centers, pae_probs): + # Clip to avoid negative/undefined d0. + clipped_num_res = ops.maximum(num_interface_tokens, 19) + + # Compute d_0(num_res) as defined by TM-score, eqn. (5) in + # http://zhanglab.ccmb.med.umich.edu/papers/2004_3.pdf + # Yang & Skolnick "Scoring function for automated + # assessment of protein structure template quality" 2004. + d0 = 1.24 * (clipped_num_res - 15) ** (1.0 / 3) - 1.8 + + # Make compatible with [num_tokens, num_tokens, num_bins] + d0 = d0[:, :, None] + bin_centers = bin_centers[None, None, :] + + # TM-Score term for every bin. + tm_per_bin = 1.0 / (1 + ops.square(bin_centers) / ops.square(d0)) + # E_distances tm(distance). + predicted_tm_term = ops.sum(pae_probs * tm_per_bin, dim=-1) + return predicted_tm_term + + # Interface version + x = asym_id[None, :] == asym_id[:, None] + num_chain_tokens = ops.sum(x * pair_mask, dim=-1) + num_interface_tokens = num_chain_tokens[None, + :] + num_chain_tokens[:, None] + # Don't double-count within a single chain + num_interface_tokens -= x * (num_interface_tokens // 2) + num_interface_tokens = num_interface_tokens * pair_mask + + num_global_tokens = ops.full( + size=pair_mask.shape, fill_value=seq_mask.sum() + ).astype(ms.int32) + + global_apae = get_tmscore_adjusted_pae( + num_global_tokens, bin_centers, pae_probs + ) + interface_apae = get_tmscore_adjusted_pae( + num_interface_tokens, bin_centers, pae_probs + ) + return global_apae, interface_apae diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py new file mode 100644 index 000000000..aeb9c5f0e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py @@ -0,0 +1,331 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Diffusion Head.""" + +from dataclasses import dataclass +from collections.abc import Callable +import math +import numpy as np +import mindspore as ms +from mindspore import mint, nn +from alphafold3.constants import residue_names +from alphafold3.model import base_config +from alphafold3.model.components import base_modules as bm +from alphafold3.model.components import utils +from alphafold3.model.diffusion import atom_cross_attention +from alphafold3.model.diffusion import diffusion_transformer +from alphafold3.model.diffusion import featurization + + +# Carefully measured by averaging multimer training set. +SIGMA_DATA = 16.0 +WEIGHT = ms.Tensor(np.load(f"./src/alphafold3/model/diffusion/random/weight.npy"), dtype=ms.float32) +BIAS = ms.Tensor(np.load(f"./src/alphafold3/model/diffusion/random/bias.npy"), dtype=ms.float32) + +def fourier_embeddings(x): + return mint.cos(2 * math.pi * (x[..., None] * WEIGHT + BIAS)) + +def random_rotation(key): + # Create a random rotation (Gram-Schmidt orthogonalization of two + # random normal vectors) + np.random.seed(key) + v0, v1 = ms.Tensor(np.random.normal(0, 1, (2, 3)), dtype=ms.float32) + e0 = v0 / mint.maximum(1e-10, mint.norm(v0)) + v1 = v1 - e0 * mint.matmul(v1, e0) + e1 = v1 / mint.maximum(1e-10, mint.norm(v1)) + e2 = mint.cross(e0, e1) + return mint.stack([e0, e1, e2]) + +def random_augmentation(rng_key, positions, mask): + """Apply random rigid augmentation. + Args: + rng_key: random key + positions: atom positions of shape (, 3) + mask: per-atom mask of shape (,) + Returns: + Transformed positions with the same shape as input positions. + """ + center = utils.mask_mean( + mask.unsqueeze(-1), positions, axis=(-2, -3), keepdims=True, eps=1e-6 + ).astype(ms.float32) + rot = random_rotation(rng_key) + np.random.seed(rng_key) + translation = ms.Tensor(np.random.normal(0, 1, (3,)), dtype=ms.float32) + + augmented_positions = ( + mint.einsum( + '...i,ij->...j', + (positions - center).astype(ms.float32), + rot, + ) + + translation + ) + return augmented_positions * mask[..., None] + +def noise_schedule(t, smin=0.0004, smax=160.0, p=7): + return ( + SIGMA_DATA + * (smax ** (1 / p) + t * (smin ** (1 / p) - smax ** (1 / p))) ** p + ) + +@dataclass +class ConditioningConfig(base_config.BaseConfig): + pair_channel: int + seq_channel: int + prob: float + +@dataclass +class SampleConfig(base_config.BaseConfig): + steps: int + gamma_0: float = 0.8 + gamma_min: float = 1.0 + noise_scale: float = 1.003 + step_scale: float = 1.5 + num_samples: int = 1 + +class DiffusionHead(nn.Cell): + """Denoising Diffusion Head. + + Args: + config (Config): Configuration object containing parameters for the diffusion head. + global_config (GlobalConfig): Global configuration object containing shared parameters. + in_shape (tuple): Input shape for the module. + max_relative_chain (int): Maximum number of relative chains for positional encoding. Default: ``2``. + max_relative_idx (int): Maximum relative index for positional encoding. Default: ``32``. + + Inputs: + - **positions_noisy** (Tensor) - Noisy atomic positions tensor. + - **noise_level** (Tensor) - Tensor representing the noise level. + - **batch** (Batch) - Batch of input data containing token features and structure information. + - **embeddings** (dict) - Dictionary of embeddings for single and pair features. + - **use_conditioning** (bool) - Flag to enable or disable conditioning. + + Outputs: + - **position_update** (Tensor) - Refined atomic positions tensor. + """ + + class Config( + atom_cross_attention.AtomCrossAttEncoderConfig, + atom_cross_attention.AtomCrossAttDecoderConfig, + ): + """Configuration for DiffusionHead.""" + eval_batch_size: int = 5 + eval_batch_dim_shard_size: int = 5 + conditioning: ConditioningConfig = base_config.autocreate( + prob=0.8, pair_channel=128, seq_channel=384 + ) + eval: SampleConfig = base_config.autocreate( + num_samples=5, + steps=200, + ) + transformer: diffusion_transformer.Transformer.Config = ( + base_config.autocreate() + ) + + def __init__(self, config, global_config, in_shape, max_relative_chain=2, max_relative_idx=32, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.dtype = dtype + in_channel = in_shape[-1] + self.max_relative_chain = max_relative_chain + self.max_relative_idx = max_relative_idx + + # _conditioning modules + in_channel_pair = in_channel + 4 * self.max_relative_idx + 4 + 2 * self.max_relative_chain + 2 + 1 + self.pair_cond_initial_norm = bm.LayerNorm( + in_shape[:-1] + (in_channel_pair,), + create_beta=False, gamma_init="ones", + name='pair_cond_initial_norm', dtype=dtype) + self.pair_cond_initial_projection = nn.Dense(in_channel_pair, self.config.conditioning.pair_channel, + has_bias=False, dtype=ms.float32) + self.transition_block1 = diffusion_transformer.TransitionBlock( + global_config, in_channel, 2, with_single_cond=False, + name=f'pair_transition_1', dtype=dtype + ) + self.transition_block2 = diffusion_transformer.TransitionBlock( + global_config, in_channel, 2, with_single_cond=False, + name=f'pair_transition_2', dtype=dtype + ) + in_channel_single = self.config.conditioning.seq_channel * 2 \ + + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP * 2 + 1 + self.single_cond_initial_norm = bm.LayerNorm( + in_shape[:-1] + (in_channel_single,), + create_beta=False, gamma_init="ones", + name='single_cond_initial_norm', dtype=dtype) + self.single_cond_initial_projection = nn.Dense(in_channel_single, self.config.conditioning.seq_channel, + has_bias=False, dtype=dtype) + self.num_noise_embedding = 256 + self.layer_norm_noise = bm.LayerNorm( + in_shape[:-1]+(self.num_noise_embedding,), + create_beta=False, gamma_init="ones", + name='noise_embedding_initial_norm', dtype=dtype) + self.linear_noise = nn.Dense(self.num_noise_embedding, self.config.conditioning.seq_channel, + has_bias=False, dtype=dtype) + self.single_transition1 = diffusion_transformer.TransitionBlock( + global_config, self.config.conditioning.seq_channel, 2, + ndim=2, with_single_cond=False, name=f'single_transition_1', + dtype=dtype + ) + self.single_transition2 = diffusion_transformer.TransitionBlock( + global_config, self.config.conditioning.seq_channel, 2, + ndim=2, with_single_cond=False, name=f'single_transition_2', + dtype=dtype + ) + + # modules + self.layer_norm_act = bm.LayerNorm( + (in_channel,)+(self.config.conditioning.seq_channel,), + create_beta=False, gamma_init="ones", + name='single_cond_embedding_norm', dtype=dtype) + self.linear_act = nn.Dense(self.config.conditioning.seq_channel, + self.config.per_token_channels, has_bias=False, dtype=dtype) + self.layer_norm_out = bm.LayerNorm( + in_shape[:-1]+(self.config.per_token_channels,), + create_beta=False, gamma_init="ones", + name='output_norm', dtype=dtype) + self.atom_cross_att_encoder = atom_cross_attention.AtomCrossAttEncoder( + self.config, self.global_config, "", dtype=dtype + ) + self.transformer = diffusion_transformer.Transformer( + self.config.transformer, self.global_config, in_shape[:-1] + (self.config.conditioning.seq_channel * 2,), + in_shape, using_pair_act=True, dtype=dtype + ) + self.atom_cross_att_decoder = atom_cross_attention.AtomCrossAttDecoder( + self.config, self.global_config, '', dtype=dtype + ) + + def _conditioning(self, batch, embeddings, noise_level, use_conditioning): + single_embedding = use_conditioning * embeddings['single'] + pair_embedding = use_conditioning * embeddings['pair'] + rel_features = featurization.create_relative_encoding( + batch.token_features, max_relative_idx=self.max_relative_idx, max_relative_chain=self.max_relative_chain + ).astype(pair_embedding.dtype) + features_2d = mint.concat([pair_embedding, rel_features], dim=-1) + pair_cond = self.pair_cond_initial_projection( + self.pair_cond_initial_norm(features_2d) + ) + pair_cond += self.transition_block1(pair_cond) + pair_cond += self.transition_block2(pair_cond) + + target_feat = embeddings['target_feat'] + features_1d = mint.concat([single_embedding, target_feat], dim=-1) + single_cond = self.single_cond_initial_norm(features_1d) + single_cond = self.single_cond_initial_projection(single_cond) + noise_embedding = fourier_embeddings( + (1 / 4) * mint.log(noise_level / SIGMA_DATA) + ) + single_cond += self.linear_noise(self.layer_norm_noise(noise_embedding)) + single_cond += self.single_transition1(single_cond) + single_cond += self.single_transition2(single_cond) + + return single_cond, pair_cond + + def construct(self, positions_noisy, noise_level, batch, embeddings, use_conditioning): + trunk_single_cond, trunk_pair_cond = self._conditioning( + batch=batch, + embeddings=embeddings, + noise_level=noise_level, + use_conditioning=use_conditioning, + ) + + # Extract features + sequence_mask = batch.token_features.mask + atom_mask = batch.predicted_structure_info.atom_mask + # Position features + act = positions_noisy * atom_mask[..., None] + act = act / mint.sqrt(noise_level**2 + SIGMA_DATA**2) + enc = self.atom_cross_att_encoder(act, embeddings["single"], trunk_pair_cond, batch) + + act = enc.token_act + act += self.linear_act(self.layer_norm_act(trunk_single_cond)) + act = self.transformer(act, trunk_single_cond, sequence_mask, trunk_pair_cond) + act = self.layer_norm_out(act) + position_update = self.atom_cross_att_decoder(act, enc, batch) + skip_scaling = SIGMA_DATA**2 / (noise_level**2 + SIGMA_DATA**2) + out_scaling = ( + noise_level * SIGMA_DATA / mint.sqrt(noise_level**2 + SIGMA_DATA**2) + ) + return ( + skip_scaling * positions_noisy + out_scaling * position_update + ) * atom_mask[..., None] + +def sample(denoising_step, batch, key, config, init_positions=None): + """Sample using denoiser on batch. + + Args: + denoising_step: the denoising function. + batch: the batch + key: random key + config: config for the sampling process (e.g. number of denoising steps, + etc.) + + Returns: + a dict + { + 'atom_positions': ms.Tensor # shape (, 3) + 'mask': ms.Tensor # shape (,) + } + where the are + (num_samples, num_tokens, max_atoms_per_token) + """ + + mask = batch.predicted_structure_info.atom_mask + # get weight and bias from Jax, this two values cannot be randomly generated + + def apply_denoising_step(carry, noise_level): + key, positions, noise_level_prev = carry + + positions = random_augmentation( + rng_key=key, positions=positions, mask=mask, + ) + gamma = config.gamma_0 * (noise_level > config.gamma_min) + t_hat = noise_level_prev * (1 + gamma) + + noise_scale = config.noise_scale * mint.sqrt(t_hat**2 - noise_level_prev**2) + np.random.seed(key) + noise = noise_scale * ms.Tensor(np.random.normal(0, 1, positions.shape), dtype=ms.float32) + positions_noisy = positions + noise + + positions_denoised = denoising_step(positions_noisy, t_hat) + grad = (positions_noisy - positions_denoised) / t_hat + + d_t = noise_level - t_hat + positions_out = positions_noisy + config.step_scale * d_t * grad + + return (key, positions_out, noise_level), positions_out + + num_samples = config.num_samples + + noise_levels = noise_schedule(mint.linspace(0, 1, config.steps + 1)) + + noise_key, key = key, key + 1 + np.random.seed(noise_key) + if init_positions is None: + init_positions = ms.Tensor(np.random.normal(0, 1, (num_samples,) + mask.shape + (3,)), dtype=ms.float32) + init_positions *= noise_levels[0] + init = (ms.Tensor([key + i for i in range(num_samples)]).reshape((-1, 1)), + init_positions, + mint.tile(noise_levels[None, 0], (num_samples,)).reshape((-1, 1))) + count = 0 + for noise_level in noise_levels[1:]: + for i in range(num_samples): + temp, _ = apply_denoising_step((count * 10 + i, init[1][i], init[2][i]), noise_level) + init[0][i], init[1][i], init[2][i] = temp + count += 1 + _, positions_out, _ = init + + final_dense_atom_mask = mint.tile(mask[None], (num_samples, 1, 1)) + + return {'atom_positions': positions_out, 'mask': final_dense_atom_mask} diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_transformer.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_transformer.py new file mode 100644 index 000000000..df02b870c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_transformer.py @@ -0,0 +1,488 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Diffusion transformer model.""" + +from dataclasses import dataclass +from alphafold3.model import base_config +from alphafold3.utils.gated_linear_unit import gated_linear_unit +from alphafold3.model.atom_layout import atom_layout +from alphafold3.model.components import base_modules as bm + +from mindspore import mint +import mindspore as ms +from mindspore import nn, ops +from mindchemistry.e3.utils import Ncon + + +class AdaptiveLayernorm(nn.Cell): + """ + If single condition is None, this layer is the same as layernorm. + If single condition is given, the layer is modified from Scalable Diffusion Models with Transformers + https://arxiv.org/abs/2212.09748 + + Args: + num_channels (int): Number of channels in the input tensor. + single_channel (int, optional): Number of channels in the single condition tensor. Required if `with_single_cond` is True. Default: ``None``. + ndim (int, optional): Number of dimensions for the dense layers. Default: ``3``. + with_single_cond (bool, optional): Whether to include the single condition adaptation. Default: ``True``. + + Inputs: + - **x** (Tensor) - Input tensor to be normalized. + - **single_cond** (Tensor, optional) - Optional single condition tensor used to adapt the normalization parameters. Required if `with_single_cond` is True. + + Outputs: + - **output** (Tensor) - The normalized output tensor. + """ + + def __init__(self, num_channels, single_channel=None, ndim=3, with_single_cond=True, dtype=ms.float32): + super().__init__() + self.with_single_cond = with_single_cond + if self.with_single_cond: + self.layernorm = bm.LayerNorm([num_channels], name='layer_norm', + create_gamma=False, create_beta=False, + gamma_init='ones', beta_init='zeros', dtype=ms.float32) + self.single_cond_layer_norm = bm.LayerNorm([single_channel], name='single_cond_layer_norm', + create_beta=False, gamma_init='ones', beta_init='zeros', + dtype=ms.float32) + self.single_cond_scale = bm.CustomDense(single_channel, num_channels, weight_init='zeros', + use_bias=True, bias_init='ones', ndim=ndim, dtype=dtype) + self.single_cond_bias = bm.CustomDense( + single_channel, num_channels, weight_init='zeros', ndim=ndim, dtype=dtype) + else: + self.layernorm = bm.LayerNorm([num_channels], dtype=ms.float32) + + def construct(self, x, single_cond=None): + if not self.with_single_cond: + x = self.layernorm(x) + else: + x = self.layernorm(x) + single_cond = self.single_cond_layer_norm(single_cond) + single_scale = self.single_cond_scale(single_cond) + single_bias = self.single_cond_bias(single_cond) + x = mint.add(mint.mul(mint.sigmoid(single_scale), x), single_bias) + return x + + +class AdaptiveZeroInit(nn.Cell): + """ + An adaptive initialization layer that combines two conditional linear transformations. + + Args: + global_config: Configuration object containing initialization settings. + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + single_channels (int, optional): Number of single conditional channels. Default: ``None``. + ndim (int, optional): Number of dimensions for the dense layer input. Default: ``3``. + with_single_cond (bool, optional): Whether to use single conditional transformation. Default: ``True``. + + Inputs: + - **x** (Tensor) - Input tensor to the layer. + - **single_cond** (Tensor, optional) - Single conditional tensor. Required if `with_single_cond` is True. + + Outputs: + - **output** (Tensor) - Output tensor after applying the adaptive initialization. + """ + + def __init__(self, global_config, in_channels, out_channels, single_channels=None, ndim=3, with_single_cond=True, dtype=ms.float32): + super().__init__() + self.with_single_cond = with_single_cond + self.cond_linear1 = bm.CustomDense( + in_channels, out_channels, weight_init='zeros', ndim=ndim, dtype=dtype) + if self.with_single_cond: + if single_channels is None: + single_channels = in_channels + self.cond_linear2 = bm.CustomDense(single_channels, out_channels, weight_init='zeros', + use_bias=True, bias_init='zeros', ndim=ndim, dtype=dtype) + self.cond_linear2.bias = ms.Parameter(self.cond_linear2.bias * (-2)) + + def construct(self, x, single_cond=None): + if not self.with_single_cond: + output = self.cond_linear1(x) + else: + output = self.cond_linear1(x) + cond = self.cond_linear2(single_cond) + output = mint.mul(mint.sigmoid(cond), output) + return output + + +class TransitionBlock(nn.Cell): + """ + A neural network layer that combines adaptive layer normalization, a gated linear unit (GLU), and adaptive zero initialization to process input data with optional conditional inputs. + + Args: + global_config: Configuration object containing initialization settings. + in_channels (int): Number of input channels. + num_intermediate_factor (int): Factor to determine the number of intermediate channels. + single_channels (int, optional): Number of single conditional channels. Default: ``None``. + ndim (int, optional): Number of dimensions for input tensor. Default: ``3``. + with_single_cond (bool, optional): Whether to use single conditional processing. Default: ``True``. + use_glu_kernel (bool, optional): Whether to use GLU. Default: ``True``. + name (str, optional): Name of the layer. Default: ``''``. + + Inputs: + - **x** (Tensor) - Input tensor to the layer. + - **single_cond** (Tensor, optional) - Single conditional tensor. Required if `with_single_cond` is True. + + Outputs: + - **output** (Tensor) - Output tensor after processing through the TransitionBlock. + """ + + def __init__(self, global_config, in_channels, num_intermediate_factor, single_channels=None, ndim=3, with_single_cond=True, use_glu_kernel=True, name='', dtype=ms.float32): + super().__init__() + self.num_intermediate = num_intermediate_factor * in_channels + if single_channels is None: + single_channels = in_channels + self.adaptive_layernorm = AdaptiveLayernorm( + in_channels, single_channels, ndim=ndim, with_single_cond=with_single_cond, dtype=dtype) + self.use_glu_kernel = use_glu_kernel + if self.use_glu_kernel: + self.weights = bm.custom_initializer( + 'relu', [in_channels, self.num_intermediate * 2], dtype=dtype) + self.weights = ms.Parameter(ms.Tensor(self.weights).reshape( + in_channels, 2, self.num_intermediate)) + else: + self.linear = bm.CustomDense( + in_channels, self.num_intermediate * 2, weight_init='zeros', ndim=3, dtype=dtype) + self.adaptive_zero_init = AdaptiveZeroInit( + global_config, self.num_intermediate, in_channels, single_channels, ndim=ndim, with_single_cond=with_single_cond, dtype=dtype) + + def construct(self, x, single_cond=None): + x = self.adaptive_layernorm(x, single_cond) + if self.use_glu_kernel: + c = gated_linear_unit.gated_linear_unit( + x=x, weight=self.weights.astype(x.dtype), + implementation=None, activation=mint.nn.functional.silu, precision=None + ).astype(x.dtype) + else: + x = self.linear(x) + x0, x1 = ops.split(x, int(x.shape[-1]/2), axis=-1) + c = ops.silu(x0) * x1 + output = self.adaptive_zero_init(c, single_cond) + return output + +@dataclass +class SelfAttentionConfig(base_config.BaseConfig): + num_head: int = 16 + key_dim: int | None = None + value_dim: int | None = None + + +class SelfAttention(nn.Cell): + """ + A self-attention mechanism implementation with adaptive layer normalization and adaptive zero initialization. + + This class implements the self-attention mechanism commonly used in transformer models. It includes adaptive layer normalization for input processing and adaptive zero initialization for the final output. The mechanism computes attention scores using query, key, and value transformations, applies masking, and optionally incorporates pair-wise logits. + + Args: + config: Configuration object containing parameters such as key dimension, value dimension, and number of attention heads. + global_config: Global configuration object for additional settings. + num_channels (int): Number of channels in the input tensor. + in_shape (tuple): Shape of the input tensor. + ndim (int, optional): Number of dimensions for the dense layers. Default: ``3``. + with_single_cond (bool, optional): Whether to include single condition adaptation. Default: ``True``. + + Inputs: + - **x** (Tensor) - Input tensor to the self-attention layer. + - **mask** (Tensor) - Attention mask to apply. + - **single_cond** (Tensor, optional) - Single condition tensor for adaptation. + - **pair_logits** (Tensor, optional) - Additional logits to incorporate into attention scores. + + Outputs: + - **output** (Tensor) - The output tensor after self-attention and adaptive zero initialization. + + Notes: + - The class uses adaptive layer normalization and adaptive zero initialization for processing inputs and outputs. + - The attention mechanism supports optional single condition adaptation and pair-wise logits. + """ + + def __init__(self, config, global_config, num_channels, in_shape, ndim=3, with_single_cond=True, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.adaptive_layernorm = AdaptiveLayernorm(num_channels, int( + num_channels//2), ndim=ndim, with_single_cond=with_single_cond, dtype=dtype) + key_dim = self.config.key_dim if self.config.key_dim is not None else num_channels + value_dim = self.config.value_dim if self.config.value_dim is not None else num_channels + num_head = self.config.num_head + assert key_dim % num_head == 0, f'{key_dim=} % {num_head=} != 0' + assert value_dim % num_head == 0, f'{value_dim=} % {num_head=} != 0' + key_dim = key_dim // num_head + self.key_dim = key_dim + value_dim = value_dim // num_head + qk_shape = (num_head, key_dim) + v_shape = (num_head, value_dim) + self.q_linear = bm.CustomDense(num_channels, qk_shape, use_bias=True, dtype=dtype) + self.k_linear = bm.CustomDense(num_channels, qk_shape, use_bias=False, dtype=dtype) + self.v_linear = bm.CustomDense(num_channels, v_shape, use_bias=False, dtype=dtype) + self.linear = bm.CustomDense( + num_channels, num_head * value_dim, weight_init='zeros', dtype=dtype) + self.adaptive_zero_init = AdaptiveZeroInit(global_config, num_channels, num_channels, int( + num_channels//2), 2, with_single_cond=with_single_cond, dtype=dtype) + self.ncon1 = Ncon([[-2, -1, 1], [-3, -1, 1]]) + self.ncon2 = Ncon([[-2, -1, 2], [2, -2, -3]]) + + def construct(self, x, mask, single_cond, pair_logits): + bias = (1e9 * (mask - 1.0))[..., None, None, :].astype(x.dtype) + x = self.adaptive_layernorm(x, single_cond) + q = self.q_linear(x) + k = self.k_linear(x) + logits = mint.einsum('...qhc,...khc->...hqk', q * self.key_dim ** (-0.5), k) + bias + if pair_logits is not None: + logits += pair_logits + weights = mint.softmax(logits, dim=-1) + weights = weights.astype(q.dtype) + v = self.v_linear(x) + weighted_avg = mint.einsum('...hqk,...khc->...qhc', weights, v) + weighted_avg = weighted_avg.reshape(weighted_avg.shape[:-2] + (-1,)) + gate_logits = self.linear(x) + weighted_avg *= mint.sigmoid(gate_logits) + output = self.adaptive_zero_init(weighted_avg, single_cond) + return output + + +class Transformer(nn.Cell): + @dataclass + class Config(base_config.BaseConfig): + attention: SelfAttentionConfig = base_config.autocreate() + num_blocks: int = 24 + block_remat: bool = False + super_block_size: int = 4 + num_intermediate_factor: int = 2 + + def __init__(self, config, global_config, in_shape, pair_shape, using_pair_act=False, name="transformer", dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.using_pair_act = using_pair_act + self.act = [] + if using_pair_act: + self.pair_layernorm = bm.LayerNorm(pair_shape, create_beta=False, dtype=ms.float32) + else: + self.pair_layernorm = None + assert self.config.num_blocks % self.config.super_block_size == 0 + self.num_super_blocks = self.config.num_blocks // self.config.super_block_size + self.super_blocks = ms.nn.CellList( + [ + SuperBlock( + config, global_config, self.config.num_blocks, + using_pair_act, in_shape, pair_shape, name, dtype=dtype + ) + for _ in range(self.num_super_blocks) + ] + ) + + @ms.jit + def construct(self, act, single_cond, mask, pair_cond=None): + if pair_cond is None: + pair_act = None + else: + pair_act = self.pair_layernorm(pair_cond) + for i in range(self.num_super_blocks): + act = self.super_blocks[i](act, mask, single_cond, pair_act) + return act + + +class Block(nn.Cell): + def __init__(self, config, global_config, in_shape, dtype=ms.float32): + super().__init__() + self.self_attention = SelfAttention( + config.attention, global_config, in_shape[-1], in_shape, ndim=2, dtype=dtype) + self.transition_block = TransitionBlock(global_config, in_shape[-1], + config.num_intermediate_factor, int(in_shape[-1]//2), ndim=2, dtype=dtype) + + def construct(self, act, mask, single_cond, pair_logits): + act += self.self_attention(act, mask, single_cond, pair_logits) + act += self.transition_block(act, single_cond) + return act + + +class SuperBlock(nn.Cell): + def __init__(self, config, global_config, num_blocks, using_pair_act, in_shape, pair_shape=None, name='', dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.num_blocks = num_blocks + self.using_pair_act = using_pair_act + self.blocks = ms.nn.CellList( + [ + Block( + config, global_config, in_shape, dtype=dtype + ) + for _ in range(self.config.super_block_size) + ] + ) + if self.using_pair_act: + self.pair_linear = bm.CustomDense( + pair_shape[-1], (self.config.super_block_size, self.config.attention.num_head), ndim=3, dtype=dtype) + else: + self.pair_linear = None + + def construct(self, act, mask, single_cond, pair_act): + if pair_act is None: + pair_logits = None + else: + pair_logits = self.pair_linear(pair_act).transpose([2, 3, 0, 1]) + for j in range(self.config.super_block_size): + act = self.blocks[j](act, mask, single_cond, pair_logits[j]) + return act + +@dataclass +class CrossAttentionConfig(base_config.BaseConfig): + num_head: int = 4 + key_dim: int = 128 + value_dim: int = 128 + + +class CrossAttention(nn.Cell): + """ + A CrossAttention class implementing multi-head cross-attention mechanism for processing sequential data. + + Args: + config (Config): Configuration object containing attention settings. + global_config (GlobalConfig): Global configuration object. + in_channel (int): Input dimension for the attention mechanism. + + Inputs: + - **x_q** (Tensor) - Query tensor. + - **x_k** (Tensor) - Key tensor. + - **mask_q** (Tensor) - Query mask tensor. + - **mask_k** (Tensor) - Key mask tensor. + - **pair_logits** (Tensor, optional) - Optional pair logits tensor. Default: ``None``. + - **single_cond_q** (Tensor) - Single condition tensor for queries. + - **single_cond_k** (Tensor) - Single condition tensor for keys. + + Outputs: + - **output** (Tensor) - Output tensor after cross-attention processing. + """ + + def __init__(self, config, global_config, in_channel, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.adaptive_layernorm_q = AdaptiveLayernorm(in_channel, in_channel, dtype=dtype) + self.adaptive_layernorm_k = AdaptiveLayernorm(in_channel, in_channel, dtype=dtype) + assert config.key_dim % config.num_head == 0 + assert config.value_dim % config.num_head == 0 + self.key_dim = config.key_dim // config.num_head + self.value_dim = config.value_dim // config.num_head + self.linear_q = bm.CustomDense( + in_channel, (self.config.num_head, self.key_dim), use_bias=True, ndim=3, dtype=dtype) + self.linear_k = bm.CustomDense( + in_channel, (self.config.num_head, self.key_dim), use_bias=False, ndim=3, dtype=dtype) + self.linear_v = bm.CustomDense( + in_channel, (self.config.num_head, self.value_dim), use_bias=False, ndim=3, dtype=dtype) + self.ncon1 = Ncon([[-1, -3, -2, 1], [-1, -4, -2, 1]]) + self.ncon2 = Ncon([[-1, -3, -2, 1], [-1, 1, -3, -4]]) + self.gating_query = bm.CustomDense( + in_channel, self.config.num_head * self.value_dim, use_bias=False, + weight_init='zeros', bias_init='ones', ndim=3, dtype=dtype) + self.adaptive_zero_init = AdaptiveZeroInit( + global_config, in_channel, in_channel, in_channel, dtype=dtype) + + def construct(self, x_q, x_k, mask_q, mask_k, pair_logits, single_cond_q, single_cond_k): + """Multihead self-attention.""" + bias = ( + 1e9 + * (mask_q - 1.0)[..., None, :, None] + * (mask_k - 1.0)[..., None, None, :] + ) + x_q = self.adaptive_layernorm_q(x_q, single_cond_q) + x_k = self.adaptive_layernorm_k(x_k, single_cond_k) + q = self.linear_q(x_q) + k = self.linear_k(x_k) + logits = mint.einsum('...qhc,...khc->...hqk', q * self.key_dim ** (-0.5), k) + bias + if pair_logits is not None: + logits += pair_logits + weights = ops.softmax(logits, axis=-1) + v = self.linear_v(x_k) + weighted_avg = mint.einsum('...hqk,...khc->...qhc', weights, v) + weighted_avg = ops.reshape( + weighted_avg, weighted_avg.shape[:-2] + (-1,)) + + gate_logits = self.gating_query(x_q) + weighted_avg *= ops.sigmoid(gate_logits) + + output = self.adaptive_zero_init(weighted_avg, single_cond_q,) + return output + + +class CrossAttTransformer(nn.Cell): + """ + A CrossAttTransformer class implementing a transformer that applies cross attention between two sets of subsets. + + Args: + config (Config): Configuration object containing settings for the transformer. + global_config (GlobalConfig): Global configuration object. + in_shape (tuple): Input shape for the transformer. + + Inputs: + - **queries_act** (Tensor) - Query activations tensor. + - **queries_mask** (Tensor) - Mask tensor for queries. + - **queries_to_keys** (Tensor) - Tensor mapping queries to keys. + - **keys_mask** (Tensor) - Mask tensor for keys. + - **queries_single_cond** (Tensor) - Single condition tensor for queries. + - **keys_single_cond** (Tensor) - Single condition tensor for keys. + - **pair_cond** (Tensor) - Pair condition tensor. + + Outputs: + - **queries_act** (Tensor) - Processed query activations tensor after cross attention. + """ + @dataclass + class Config(base_config.BaseConfig): + num_intermediate_factor: int + num_blocks: int + attention: CrossAttentionConfig = base_config.autocreate() + + def __init__(self, config, global_config, in_shape, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.pair_input_layer_norm = bm.LayerNorm(in_shape, create_beta=False, dtype=ms.float32) + self.pair_logits_projection = bm.CustomDense( + in_shape[-1], (self.config.num_blocks, self.config.attention.num_head), ndim=4, dtype=dtype) + self.block = ms.nn.CellList( + [ + CrossAttTransformerBlock( + config, global_config, in_shape[-2], dtype=dtype + ) + for _ in range(self.config.num_blocks) + ] + ) + + def construct(self, queries_act, queries_mask, queries_to_keys, + keys_mask, queries_single_cond, keys_single_cond, + pair_cond): + pair_act = self.pair_input_layer_norm(pair_cond) + pair_logits = self.pair_logits_projection(pair_act) + pair_logits = ops.transpose(pair_logits, (3, 0, 4, 1, 2)) + for i in range(self.config.num_blocks): + queries_act = self.block[i](queries_act, queries_mask, queries_to_keys, keys_mask, pair_logits[i], + queries_single_cond, keys_single_cond) + return queries_act + + +class CrossAttTransformerBlock(nn.Cell): + def __init__(self, config, global_config, in_channel, dtype=ms.float32): + super().__init__() + self.cross_attention = CrossAttention( + config.attention, global_config, in_channel, dtype=dtype) + self.transition = TransitionBlock( + global_config, in_channel, config.num_intermediate_factor, dtype=dtype) + + def construct(self, queries_act, queries_mask, queries_to_keys, keys_mask, pair_logits, + queries_single_cond, keys_single_cond): + keys_act = atom_layout.convert_ms( + queries_to_keys, queries_act, layout_axes=(-3, -2) + ) + queries_act += self.cross_attention(queries_act, keys_act, queries_mask, keys_mask, + pair_logits, queries_single_cond, keys_single_cond) + queries_act += self.transition(queries_act, queries_single_cond) + return queries_act diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/distogram_head.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/distogram_head.py new file mode 100644 index 000000000..0d7a665f6 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/distogram_head.py @@ -0,0 +1,88 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Distogram head.""" + +from typing import Final +from dataclasses import dataclass +import mindspore as ms +from mindspore import nn, ops +from alphafold3.model import base_config +from alphafold3.model.components import base_modules as bm +from mindchemistry.e3.utils import Ncon + + +_CONTACT_THRESHOLD: Final[float] = 8.0 +_CONTACT_EPSILON: Final[float] = 1e-3 + + +class DistogramHead(nn.Cell): + """ + A DistogramHead class that computes a distogram from pair embeddings, predicting distances between residues. + + Args: + config (Config): Configuration object containing parameters for the distogram head. + global_config (GlobalConfig): Global configuration object. + in_channel (int): Number of input channels for the linear layer. + + Inputs: + - **batch** (dict) - Dictionary containing batch features. + - **embeddings** (dict) - Dictionary containing pair embeddings. + + Outputs: + - **bin_edges** (Tensor) - Tensor of bin edges for distance predictions. + - **contact_probs** (Tensor) - Tensor of contact probabilities. + + Notes: + - The distogram head computes distance probabilities using a linear transformation and softmax. + - The Ncon class is used for tensor contraction operations. + """ + @dataclass + class Config(base_config.BaseConfig): + first_break: float = 2.3125 + last_break: float = 21.6875 + num_bins: int = 64 + + def __init__( + self, config, global_config, in_channel, dtype=ms.float32 + ): + super().__init__() + self.config = config + self.global_config = global_config + self.linear = bm.CustomDense( + in_channel, self.config.num_bins, weight_init=self.global_config.final_init, ndim=3, dtype=dtype) + self.ncon = Ncon([[-1, -2, 1], [1]]) + + def construct(self, batch, embeddings): + pair_act = embeddings["pair"] + seq_mask = batch.token_features.mask.astype(ms.bool_) + pair_mask = seq_mask[:, None] * seq_mask[None, :] + left_half_logits = self.linear(pair_act) + right_half_logits = left_half_logits + logits = left_half_logits + ops.swapaxes(right_half_logits, -2, -3) + probs = ops.softmax(logits, axis=-1) + breaks = ops.linspace( + self.config.first_break, + self.config.last_break, + self.config.num_bins - 1, + ) + bin_tops = ops.concat( + (breaks, (breaks[-1] + breaks[-1] - breaks[-2]).reshape(-1))) + threshold = _CONTACT_THRESHOLD + _CONTACT_EPSILON + is_contact_bin = 1.0 * (bin_tops <= threshold) + contact_probs = self.ncon([probs.astype(ms.float32), is_contact_bin.astype(ms.float32)]) + contact_probs = pair_mask * contact_probs + return { + 'bin_edges': breaks, + 'contact_probs': contact_probs, + } diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py new file mode 100644 index 000000000..439c6502b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py @@ -0,0 +1,212 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Model-side of the input features processing.""" +import math +import numpy as np +import mindspore as ms +from mindspore import ops +from alphafold3.constants import residue_names +from alphafold3.model.components import utils + + +def _grid_keys(key, shape): + """Generate a grid of rng keys that is consistent with different padding. + + Generate random keys such that the keys will be identical, regardless of + how much padding is added to any dimension. + + Args: + key: A PRNG key. + shape: The shape of the output array of keys that will be generated. + + Returns: + An array of shape `shape` consisting of random keys. + """ + if not shape: + return key + + def partial_bitwise_xor(other): + return ops.bitwise_xor(key, other) + + def _partial_grid_keys(key): + return _grid_keys(key, shape[1:]) + new_keys = ms.vmap(partial_bitwise_xor)( + ops.arange(shape[0]) + ) + return ms.vmap(_partial_grid_keys)(new_keys) + + +def _padding_consistent_rng(f): + def inner(key, shape, **kwargs): + keys = _grid_keys(key, shape) + out = keys.flatten() + count = 0 + for key in keys.flatten(): + out[count] = (f((), key)) + count += 1 + return out.reshape(keys.shape) + return inner + + +def gumbel_sample(shape): + uniform_samples = ms.Tensor(np.random.uniform(0.0, 1.0, shape)) + gumbel_samples = -ops.log(-ops.log(uniform_samples)) + return gumbel_samples + + +def gumbel_argsort_sample_idx(key, logits): + gumbel = _padding_consistent_rng(gumbel_sample) + z = gumbel(key, logits.shape) + perm = ops.argsort(logits + z, axis=-1, descending=False) + return perm[::-1] + + +def create_msa_feat(msa): + msa_1hot = ops.one_hot(msa.rows.astype( + ms.int64), residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + 1) + deletion_matrix = msa.deletion_matrix + has_deletion = ops.clip(deletion_matrix, 0.0, 1.0)[..., None] + deletion_value = (ops.arctan(deletion_matrix / 3.0) + * (2.0 / math.pi))[..., None] + msa_feat = [msa_1hot.astype(deletion_value.dtype), has_deletion, deletion_value] + return ops.concat(msa_feat, axis=-1) + + +def truncate_msa_batch(msa, num_msa): + indices = ops.arange(num_msa) + return msa.index_msa_rows(indices) + + +def create_target_feat(batch, append_per_atom_features, dtype=ms.float32): + token_features = batch.token_features + target_features = [] + target_features.append(ops.one_hot( + token_features.aatype.astype(ms.int64), + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP).astype(dtype)) + target_features.append(batch.msa.profile) + target_features.append(batch.msa.deletion_mean[..., None]) + + if append_per_atom_features: + ref_mask = batch.ref_structure.mask + element_feat = ops.one_hot(batch.ref_structure.element, 128) + element_feat = utils.mask_mean( + mask=ref_mask[..., None], value=element_feat, axis=-2, eps=1e-6) + target_features.append(element_feat) + pos_feat = batch.ref_structure.positions + pos_feat = pos_feat.reshape([pos_feat.shape[0], -1]) + target_features.append(pos_feat) + target_features.append(ref_mask) + return ops.concat(target_features, axis=-1) + + +def create_relative_encoding( + seq_features, + max_relative_idx, + max_relative_chain, +): + """Add relative position encodings.""" + rel_feats = [] + token_index = seq_features.token_index + residue_index = seq_features.residue_index + asym_id = seq_features.asym_id + entity_id = seq_features.entity_id + sym_id = seq_features.sym_id + + left_asym_id = asym_id[:, None] + right_asym_id = asym_id[None, :] + + left_residue_index = residue_index[:, None] + right_residue_index = residue_index[None, :] + + left_token_index = token_index[:, None] + right_token_index = token_index[None, :] + + left_entity_id = entity_id[:, None] + right_entity_id = entity_id[None, :] + left_sym_id = sym_id[:, None] + right_sym_id = sym_id[None, :] + + # Embed relative positions using a one-hot embedding of distance along chain + offset = left_residue_index - right_residue_index + clipped_offset = ops.clip( + offset + max_relative_idx, min=0, max=2 * max_relative_idx + ) + asym_id_same = left_asym_id == right_asym_id + final_offset = ops.where( + asym_id_same, + clipped_offset, + (2 * max_relative_idx + 1) * ops.ones_like(clipped_offset), + ) + rel_pos = ops.one_hot(final_offset.astype( + ms.int64), 2 * max_relative_idx + 2) + rel_feats.append(rel_pos) + + # Embed relative token index as a one-hot embedding of distance along residue + token_offset = left_token_index - right_token_index + clipped_token_offset = ops.clip( + token_offset + max_relative_idx, min=0, max=2 * max_relative_idx + ) + residue_same = ops.logical_and((left_asym_id == right_asym_id), ( + left_residue_index == right_residue_index + )) + final_token_offset = ops.where( + residue_same, + clipped_token_offset, + (2 * max_relative_idx + 1) * ops.ones_like(clipped_token_offset), + ) + rel_token = ops.one_hot(final_token_offset.astype( + ms.int64), 2 * max_relative_idx + 2) + rel_feats.append(rel_token) + + # Embed same entity ID + entity_id_same = left_entity_id == right_entity_id + rel_feats.append(entity_id_same.astype(rel_pos.dtype)[..., None]) + + # Embed relative chain ID inside each symmetry class + rel_sym_id = left_sym_id - right_sym_id + + max_rel_chain = max_relative_chain + + clipped_rel_chain = ops.clip( + rel_sym_id + max_rel_chain, min=0, max=2 * max_rel_chain + ) + + final_rel_chain = ops.where( + entity_id_same, + clipped_rel_chain, + (2 * max_rel_chain + 1) * ops.ones_like(clipped_rel_chain), + ) + rel_chain = ops.one_hot(final_rel_chain.astype( + ms.int64), 2 * max_relative_chain + 2) + + rel_feats.append(rel_chain) + + return ops.concat(rel_feats, axis=-1) + + +def shuffle_msa(key, msa): + """Shuffle MSA randomly, return batch with shuffled MSA. + + Args: + key: rng key for random number generation. + msa: MSA object to sample msa from. + + Returns: + Protein with sampled msa. + """ + key, sample_key = key, key + 1 + # Sample uniformly among sequences with at least one non-masked position. + logits = (ops.clip(ops.sum(msa.mask, dim=-1), 0.0, 1.0) - 1.0) * 1e6 + index_order = gumbel_argsort_sample_idx(sample_key, logits) + return msa.index_msa_rows(index_order), sample_key diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py new file mode 100644 index 000000000..2f1cdfedf --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py @@ -0,0 +1,579 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + + +import pathlib +import mindspore as ms +from mindspore import ops +from alphafold3.model.params import get_model_af3_params + + +def np_slice(arr, i, j, dtype=ms.bfloat16): + if i is not None and j is not None: + return ms.Parameter(ms.Tensor(arr[i, j], dtype)) + if i is not None and j is None: + return ms.Parameter(ms.Tensor(arr[i], dtype)) + if i is None and j is not None: + return ms.Parameter(ms.Tensor(arr[j], dtype)) + return ms.Parameter(ms.Tensor(arr, dtype)) + + + +def load_adaptive_layernorm(adaptive_layernorm, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + if not ckpt.get(f'{path}single_cond_layer_norm'): + adaptive_layernorm.layernorm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}layer_norm']['scale'], i, j, dtype=ms.float32)) + adaptive_layernorm.layernorm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}layer_norm']['offset'], i, j, dtype=ms.float32)) + else: + adaptive_layernorm.single_cond_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}single_cond_layer_norm']['scale'], i, j, dtype=ms.float32)) + adaptive_layernorm.single_cond_scale.weight.set_data( + np_slice(ckpt[f'{path}single_cond_scale']['weights'], i, j, dtype=dtype)) + adaptive_layernorm.single_cond_scale.bias.set_data( + np_slice(ckpt[f'{path}single_cond_scale']['bias'], i, j, dtype=dtype)) + adaptive_layernorm.single_cond_bias.weight.set_data( + np_slice(ckpt[f'{path}single_cond_bias']['weights'], i, j, dtype=dtype)) + + +def load_adaptive_zero_init(adaptive_zero_init, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + adaptive_zero_init.cond_linear1.weight.set_data( + np_slice(ckpt[f'{path}transition2']['weights'], i, j, dtype=dtype)) + if ckpt.get(f'{path}adaptive_zero_cond'): + adaptive_zero_init.cond_linear2.weight.set_data( + np_slice(ckpt[f'{path}adaptive_zero_cond']['weights'], i, j, dtype=dtype)) + adaptive_zero_init.cond_linear2.bias.set_data( + np_slice(ckpt[f'{path}adaptive_zero_cond']['bias'], i, j, dtype=dtype)) + + +def load_transition(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_adaptive_layernorm( + transition_block.adaptive_layernorm, f'{path}ffw_', ckpt, i, j, dtype=dtype) + transition_block.weights.set_data( + np_slice(ckpt[f'{path}ffw_transition1']['weights'], i, j, dtype=dtype).reshape( + (transition_block.weights.shape[0], 2, transition_block.num_intermediate))) + load_adaptive_zero_init( + transition_block.adaptive_zero_init, f'{path}ffw_', ckpt, i, j, dtype=dtype) + + +def load_self_attention(self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_adaptive_layernorm( + self_attention.adaptive_layernorm, path, ckpt, i, j) + self_attention.q_linear.weight.set_data( + np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) + self_attention.q_linear.bias.set_data( + np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) + self_attention.k_linear.weight.set_data( + np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) + self_attention.v_linear.weight.set_data( + np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) + self_attention.linear.weight.set_data( + np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) + load_adaptive_zero_init( + self_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) + + +def load_transformer(transformer, path, ckpt, dtype=ms.bfloat16): + for i in range(6): + for j in range(4): + transformer_path = (path + + '/__layer_stack_with_per_layer/__layer_stack_with_per_layer/transformer') + load_self_attention(transformer.super_blocks[i].blocks[j].self_attention, + transformer_path, ckpt, i, j, dtype=dtype) + load_transition(transformer.super_blocks[i].blocks[j].transition_block, + transformer_path, ckpt, i, j, dtype=dtype) + if transformer.using_pair_act is True: + pair_projection_path = f'{path}/__layer_stack_with_per_layer/pair_logits_projection' + transformer.super_blocks[i].pair_linear.weight.set_data( + np_slice(ckpt[pair_projection_path]['weights'], i, None, dtype=dtype)) + if transformer.using_pair_act is True: + pair_norm_path = f'{path}/pair_input_layer_norm' + transformer.pair_layernorm.layernorm.gamma.set_data( + np_slice(ckpt[pair_norm_path]['scale'].T, dtype=ms.float32)) + + +def load_transition_block(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + transition_block.glu_weight.set_data( + np_slice(ckpt[f'{path}/transition1']['weights'], i, j, dtype=dtype).reshape( + (-1, 2, transition_block.num_intermediate))) + transition_block.out_linear.weight.set_data( + np_slice(ckpt[f'{path}/transition2']['weights'], i, j, dtype=dtype)) + transition_block.layernorm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/input_layer_norm']['scale'], i, j, dtype=ms.float32)) + transition_block.layernorm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/input_layer_norm']['offset'], i, j, dtype=ms.float32)) + + +def load_grid_self_attention(grid_self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + grid_self_attention.q_projection.weight.set_data( + np_slice(ckpt[f'{path}/q_projection']['weights'], i, j, dtype=dtype).transpose(2, 0, 1)) + grid_self_attention.k_projection.weight.set_data( + np_slice(ckpt[f'{path}/k_projection']['weights'], i, j, dtype=dtype).transpose(2, 0, 1)) + grid_self_attention.v_projection.weight.set_data( + np_slice(ckpt[f'{path}/v_projection']['weights'], i, j, dtype=dtype)) + grid_self_attention.gating_query.weight.set_data( + np_slice(ckpt[f'{path}/gating_query']['weights'], i, j, dtype=dtype).T) + grid_self_attention.output_projection.weight.set_data( + np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) + grid_self_attention.pair_bias_projection.weight.set_data( + np_slice(ckpt[f'{path}/pair_bias_projection']['weights'], i, j, dtype=dtype)) + grid_self_attention.act_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/act_norm']['scale'], i, j, dtype=ms.float32)) + grid_self_attention.act_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/act_norm']['offset'], i, j, dtype=ms.float32)) + + +def load_outer_product_mean(outer_product_mean, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + outer_product_mean.outer_product_mean.o_biases.set_data( + np_slice(ckpt[path]['output_b'], i, j, dtype=dtype)) + outer_product_mean.outer_product_mean.linear_output_weight.set_data( + np_slice(ckpt[path]['output_w'], i, j, dtype=dtype)) + outer_product_mean.outer_product_mean.left_projection_weight.set_data( + np_slice(ckpt[f'{path}/left_projection']['weights'], i, j, dtype=dtype).T) + outer_product_mean.outer_product_mean.right_projection_weight.set_data( + np_slice(ckpt[f'{path}/right_projection']['weights'], i, j, dtype=dtype).T) + outer_product_mean.outer_product_mean.layer_norm_input_gamma.set_data( + np_slice(ckpt[f'{path}/layer_norm_input']['scale'], i, j, dtype=ms.float32)) + outer_product_mean.outer_product_mean.layer_norm_input_beta.set_data( + np_slice(ckpt[f'{path}/layer_norm_input']['offset'], i, j, dtype=ms.float32)) + + +def load_msa_attention(msa_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + msa_attention.actnorm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/act_norm']['scale'], i, j, dtype=ms.float32)) + msa_attention.actnorm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/act_norm']['offset'], i, j, dtype=ms.float32)) + msa_attention.pairnorm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/pair_norm']['scale'], i, j, dtype=ms.float32)) + msa_attention.pairnorm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/pair_norm']['offset'], i, j, dtype=ms.float32)) + msa_attention.pair_logits.weight.set_data( + np_slice(ckpt[f'{path}/pair_logits']['weights'], i, j, dtype=dtype)) + msa_attention.v_projection.weight.set_data( + np_slice(ckpt[f'{path}/v_projection']['weights'], i, j, dtype=dtype)) + msa_attention.gating_query.weight.set_data( + np_slice(ckpt[f'{path}/gating_query']['weights'], i, j, dtype=dtype)) + msa_attention.output_projection.weight.set_data( + np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) + + +def load_triangle_multiplication(triangle_multiplication, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + triangle_multiplication.triangle_multi.gate.weight.set_data( + np_slice(ckpt[f'{path}/gate']['weights'], i, j, dtype=dtype).T) + triangle_multiplication.triangle_multi.projection.weight.set_data( + np_slice(ckpt[f'{path}/projection']['weights'], i, j, dtype=dtype).T) + triangle_multiplication.triangle_multi.weight_glu = ops.stack( + [triangle_multiplication.triangle_multi.gate.weight, + triangle_multiplication.triangle_multi.projection.weight], axis=1) + triangle_multiplication.triangle_multi.output_projection.weight.set_data( + np_slice(ckpt[f'{path}/output_projection']['weights'], i, j, dtype=dtype)) + triangle_multiplication.triangle_multi.gating_linear.weight.set_data( + np_slice(ckpt[f'{path}/gating_linear']['weights'], i, j, dtype=dtype)) + triangle_multiplication.triangle_multi.left_norm_input.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/left_norm_input']['scale'], i, j, dtype=ms.float32)) + triangle_multiplication.triangle_multi.left_norm_input.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/left_norm_input']['offset'], i, j, dtype=ms.float32)) + triangle_multiplication.triangle_multi.center_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/center_norm']['scale'], i, j, dtype=ms.float32)) + triangle_multiplication.triangle_multi.center_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/center_norm']['offset'], i, j, dtype=ms.float32)) + + +def load_pair_former(pair_former, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_grid_self_attention(pair_former.grid_self_attention1, f'{path}/pair_attention1', + ckpt, i, j, dtype=dtype) + load_grid_self_attention(pair_former.grid_self_attention2, f'{path}/pair_attention2', + ckpt, i, j, dtype=dtype) + load_triangle_multiplication(pair_former.triangle_multiplication1, + f'{path}/triangle_multiplication_outgoing', ckpt, i, j, dtype=dtype) + load_triangle_multiplication(pair_former.triangle_multiplication2, + f'{path}/triangle_multiplication_incoming', ckpt, i, j, dtype=dtype) + load_transition_block(pair_former.transition_block, f'{path}/pair_transition', + ckpt, i, j, dtype=dtype) + if pair_former.with_single: + pair_former.single_pair_logits_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/single_pair_logits_norm']['scale'], i, j, dtype=ms.float32)) + pair_former.single_pair_logits_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/single_pair_logits_norm']['offset'], i, j, dtype=ms.float32)) + pair_former.single_pair_logits_projection.weight.set_data( + np_slice(ckpt[f'{path}/single_pair_logits_projection']['weights'], i, j, dtype=dtype)) + load_self_attention(pair_former.single_attention, f'{path}/single_attention_', + ckpt, i, j, dtype=dtype) + load_transition_block(pair_former.single_transition, f'{path}/single_transition', + ckpt, i, j, dtype=dtype) + + +def load_evo_former(evo_former, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_outer_product_mean(evo_former.outer_product_mean, f'{path}/outer_product_mean', + ckpt, i, j, dtype=dtype) + load_msa_attention(evo_former.msa_attention, f'{path}/msa_attention1', + ckpt, i, j, dtype=dtype) + load_transition_block(evo_former.msa_transition, f'{path}/msa_transition', + ckpt, i, j, dtype=dtype) + load_triangle_multiplication(evo_former.triangle_multiplication1, + f'{path}/triangle_multiplication_outgoing', ckpt, i, j, dtype=dtype) + load_triangle_multiplication(evo_former.triangle_multiplication2, + f'{path}/triangle_multiplication_incoming', ckpt, i, j, dtype=dtype) + load_grid_self_attention(evo_former.pair_attention1, f'{path}/pair_attention1', + ckpt, i, j, dtype=dtype) + load_grid_self_attention(evo_former.pair_attention2, f'{path}/pair_attention2', + ckpt, i, j, dtype=dtype) + load_transition_block(evo_former.transition_block, f'{path}/pair_transition', + ckpt, i, j, dtype=dtype) + + +def load_single_template_embedding(single_template_embedding, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + num_layer = single_template_embedding.config.template_stack.num_layer + for ii in range(num_layer): + template_path = f'{path}/__layer_stack_no_per_layer/template_embedding_iteration' + load_pair_former(single_template_embedding.template_stack[ii], template_path, + ckpt, ii, dtype=dtype) + for jj in range(9): + template_pair_path = f'{path}/template_pair_embedding_{jj}' + single_template_embedding.template_pair_embedding[jj].weight.set_data( + np_slice(ckpt[template_pair_path]['weights'], None, None, dtype=dtype)) + single_template_embedding.output_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/output_layer_norm']['scale'], i, j, dtype=ms.float32)) + single_template_embedding.output_layer_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/output_layer_norm']['offset'], i, j, dtype=ms.float32)) + single_template_embedding.query_embedding_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/query_embedding_norm']['scale'], i, j, dtype=ms.float32)) + single_template_embedding.query_embedding_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/query_embedding_norm']['offset'], i, j, dtype=ms.float32)) + + +def load_template_embedding(template_embedding, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + template_embedding.output_linear.weight.set_data( + np_slice(ckpt[f'{path}/output_linear']['weights'], i, j, dtype=dtype)) + load_single_template_embedding(template_embedding.template_embedder, + f'{path}/single_template_embedding', ckpt, i, j, dtype=dtype) + + +def load_distogram_head(distogram_head, path, ckpt, i=None, j=None, dtype=ms.float32): + distogram_head.linear.weight.set_data( + np_slice(ckpt[f'{path}/half_logits']['weights'], i, j, dtype=dtype)) + + +def load_evoformer(evoformer, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + relative_encoding_path = f'{path}/~_relative_encoding/position_activations' + evoformer.position_activations.weight.set_data( + np_slice(ckpt[relative_encoding_path]['weights'], i, j, dtype=dtype)) + evoformer.left_single.weight.set_data( + np_slice(ckpt[f'{path}/left_single']['weights'], i, j, dtype=dtype)) + evoformer.right_single.weight.set_data( + np_slice(ckpt[f'{path}/right_single']['weights'], i, j, dtype=dtype)) + evoformer.bond_embedding.weight.set_data( + np_slice(ckpt[f'{path}/bond_embedding']['weights'], i, j, dtype=dtype)) + evoformer.msa_activations.weight.set_data( + np_slice(ckpt[f'{path}/msa_activations']['weights'], i, j, dtype=dtype)) + evoformer.extra_msa_target_feat.weight.set_data( + np_slice(ckpt[f'{path}/extra_msa_target_feat']['weights'], i, j, dtype=dtype)) + evoformer.prev_embedding.weight.set_data( + np_slice(ckpt[f'{path}/prev_embedding']['weights'], i, j, dtype=dtype)) + evoformer.prev_embedding_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/prev_embedding_layer_norm']['scale'], i, j, dtype=ms.float32)) + evoformer.prev_embedding_layer_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/prev_embedding_layer_norm']['offset'], i, j, dtype=ms.float32)) + evoformer.single_activations.weight.set_data( + np_slice(ckpt[f'{path}/single_activations']['weights'], i, j, dtype=dtype)) + evoformer.prev_single_embedding.weight.set_data( + np_slice(ckpt[f'{path}/prev_single_embedding']['weights'], i, j, dtype=dtype)) + evoformer.prev_single_embedding_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/prev_single_embedding_layer_norm']['scale'], i, j, dtype=ms.float32)) + evoformer.prev_single_embedding_layer_norm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/prev_single_embedding_layer_norm']['offset'], i, j, dtype=ms.float32)) + load_template_embedding(evoformer.template_module, f'{path}/template_embedding', + ckpt, i, j, dtype=dtype) + for ii in range(evoformer.config.pairformer.num_layer): + pairformer_path = path+'/__layer_stack_no_per_layer_1/trunk_pairformer' + load_pair_former( + evoformer.pairformer_stack[ii], pairformer_path, ckpt, ii, dtype=dtype) + for jj in range(evoformer.config.msa_stack.num_layer): + msa_stack_path = path+'/__layer_stack_no_per_layer/msa_stack' + load_evo_former( + evoformer.evoformer_stack[jj], msa_stack_path, ckpt, jj, dtype=dtype) + + +def load_adaptive_layernorm_ms(adaptive_layernorm, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + if not ckpt.get(f'{path}single_cond_layer_norm'): + adaptive_layernorm.layernorm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}layer_norm']['scale'], i, j, dtype=dtype)) + adaptive_layernorm.layernorm.layernorm.beta.set_data( + np_slice(ckpt[f'{path}layer_norm']['offset'], i, j, dtype=dtype)) + else: + adaptive_layernorm.single_cond_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}single_cond_layer_norm']['scale'], i, j, dtype=dtype)) + adaptive_layernorm.single_cond_scale.weight.set_data( + np_slice(ckpt[f'{path}single_cond_scale']['weights'], i, j, dtype=dtype)) + adaptive_layernorm.single_cond_scale.bias.set_data( + np_slice(ckpt[f'{path}single_cond_scale']['bias'], i, j, dtype=dtype)) + adaptive_layernorm.single_cond_bias.weight.set_data( + np_slice(ckpt[f'{path}single_cond_bias']['weights'], i, j, dtype=dtype)) + + +def load_adaptive_zero_init_ms(adaptive_zero_init, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + adaptive_zero_init.cond_linear1.weight.set_data( + np_slice(ckpt[f'{path}transition2']['weights'], i, j, dtype=dtype)) + if ckpt.get(f'{path}adaptive_zero_cond'): + adaptive_zero_init.cond_linear2.weight.set_data( + np_slice(ckpt[f'{path}adaptive_zero_cond']['weights'], i, j, dtype=dtype)) + adaptive_zero_init.cond_linear2.bias.set_data( + np_slice(ckpt[f'{path}adaptive_zero_cond']['bias'], i, j, dtype=dtype)) + + +def load_transition_ms(transition_block, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_adaptive_layernorm_ms( + transition_block.adaptive_layernorm, f'{path}ffw_', ckpt, i, j, dtype=dtype) + transition_block.weights.set_data( + np_slice(ckpt[f'{path}ffw_transition1']['weights'], i, j, dtype=dtype).reshape( + (transition_block.weights.shape[0], 2, transition_block.num_intermediate))) + load_adaptive_zero_init_ms( + transition_block.adaptive_zero_init, f'{path}ffw_', ckpt, i, j, dtype=dtype) + + +def load_self_attention_ms(self_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_adaptive_layernorm_ms( + self_attention.adaptive_layernorm, path, ckpt, i, j, dtype=dtype) + self_attention.q_linear.weight.set_data( + np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) + self_attention.q_linear.bias.set_data( + np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) + self_attention.k_linear.weight.set_data( + np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) + self_attention.v_linear.weight.set_data( + np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) + self_attention.linear.weight.set_data( + np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) + load_adaptive_zero_init_ms( + self_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) + + +def load_transformer_ms(transformer, path, ckpt, dtype=ms.float16): + for i in range(6): + for j in range(4): + transformer_path = (path + + f'/__layer_stack_with_per_layer/__layer_stack_with_per_layer/transformer') + load_self_attention_ms(transformer.super_blocks[i].blocks[j].self_attention, + transformer_path, ckpt, i, j, dtype=dtype) + load_transition_ms(transformer.super_blocks[i].blocks[j].transition_block, + transformer_path, ckpt, i, j, dtype=dtype) + if transformer.using_pair_act: + pair_projection_path = path + f'/__layer_stack_with_per_layer/pair_logits_projection' + transformer.super_blocks[i].pair_linear.weight.set_data( + np_slice(ckpt[pair_projection_path]['weights'], i, None, dtype=dtype)) + if transformer.using_pair_act: + pair_norm_path = f'{path}/pair_input_layer_norm' + transformer.pair_layernorm.layernorm.gamma.set_data( + np_slice(ckpt[pair_norm_path]['scale'].T, None, None, dtype=ms.float32)) + + +def load_cross_attention(cross_attention, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + load_adaptive_layernorm_ms( + cross_attention.adaptive_layernorm_q, f'{path}q', ckpt, i, j, dtype=dtype) + load_adaptive_layernorm_ms( + cross_attention.adaptive_layernorm_k, f'{path}k', ckpt, i, j, dtype=dtype) + cross_attention.linear_q.weight.set_data( + np_slice(ckpt[f'{path}q_projection']['weights'], i, j, dtype=dtype)) + cross_attention.linear_q.bias.set_data( + np_slice(ckpt[f'{path}q_projection']['bias'], i, j, dtype=dtype)) + cross_attention.linear_k.weight.set_data( + np_slice(ckpt[f'{path}k_projection']['weights'], i, j, dtype=dtype)) + cross_attention.linear_v.weight.set_data( + np_slice(ckpt[f'{path}v_projection']['weights'], i, j, dtype=dtype)) + cross_attention.gating_query.weight.set_data( + np_slice(ckpt[f'{path}gating_query']['weights'], i, j, dtype=dtype)) + load_adaptive_zero_init_ms( + cross_attention.adaptive_zero_init, path, ckpt, i, j, dtype=dtype) + + +def load_cross_att_transformer_block(cross_att_transformer_block, path, ckpt, i=None, dtype=ms.bfloat16): + load_cross_attention( + cross_att_transformer_block.cross_attention, path, ckpt, i, dtype=dtype) + load_transition_ms(cross_att_transformer_block.transition, + path, ckpt, i, dtype=dtype) + + +def load_cross_attention_transformer(cross_attention_transformer, path, ckpt, last_name, i, j, dtype=ms.bfloat16): + cross_attention_transformer.pair_input_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/pair_input_layer_norm']['scale'], i, j, dtype=dtype)) + cross_attention_transformer.pair_logits_projection.weight.set_data( + np_slice(ckpt[f'{path}/pair_logits_projection']['weights'], i, j, dtype=dtype)) + for ii in range(cross_attention_transformer.config.num_blocks): + block_path = path + f'/__layer_stack_with_per_layer/{last_name}' + load_cross_att_transformer_block(cross_attention_transformer.block[ii], block_path, + ckpt, ii, dtype=dtype) + + +def load_per_atom_conditioning(per_atom_conditioning, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + per_atom_conditioning.linear1.weight.set_data( + np_slice(ckpt[f'{path}_embed_ref_pos']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear2.weight.set_data( + np_slice(ckpt[f'{path}_embed_ref_mask']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear3.weight.set_data( + np_slice(ckpt[f'{path}_embed_ref_element']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear4.weight.set_data( + np_slice(ckpt[f'{path}_embed_ref_charge']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear5.weight.set_data( + np_slice(ckpt[f'{path}_embed_ref_atom_name']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear_row_act.weight.set_data( + np_slice(ckpt[f'{path}_single_to_pair_cond_row']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear_col_act.weight.set_data( + np_slice(ckpt[f'{path}_single_to_pair_cond_col']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear_pair_act1.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_offsets']['weights'].T, i, j, dtype=dtype)) + per_atom_conditioning.linear_pair_act2.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_distances']['weights'].T, i, j, dtype=dtype)) + + +def load_atom_cross_encoder(atom_cross_att_encoder, path, ckpt, last_name, i=None, j=None, dtype=ms.bfloat16): + load_per_atom_conditioning( + atom_cross_att_encoder._per_atom_conditioning, path, ckpt, dtype=dtype) + if atom_cross_att_encoder.with_cond: + atom_cross_att_encoder._embed_trunk_single_cond.weight.set_data( + np_slice(ckpt[f'{path}_embed_trunk_single_cond']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._lnorm_trunk_single_cond.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}_lnorm_trunk_single_cond']['scale'], i, j, dtype=ms.float32)) + atom_cross_att_encoder._atom_positions_to_features.weight.set_data( + np_slice(ckpt[f'{path}_atom_positions_to_features']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._embed_trunk_pair_cond.weight.set_data( + np_slice(ckpt[f'{path}_embed_trunk_pair_cond']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._lnorm_trunk_pair_cond.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}_lnorm_trunk_pair_cond']['scale'], i, j, dtype=ms.float32)) + atom_cross_att_encoder._single_to_pair_cond_row.weight.set_data( + np_slice(ckpt[f'{path}_single_to_pair_cond_row_1']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._single_to_pair_cond_col.weight.set_data( + np_slice(ckpt[f'{path}_single_to_pair_cond_col_1']['weights'].T, i, j, dtype=dtype)) + if atom_cross_att_encoder.with_cond: + atom_cross_att_encoder._embed_pair_offsets.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_offsets_1']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._embed_pair_distances.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_distances_1']['weights'].T, i, j, dtype=dtype)) + else: + atom_cross_att_encoder._embed_pair_offsets.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_offsets']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._embed_pair_distances.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_distances']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._embed_pair_offsets_valid.weight.set_data( + np_slice(ckpt[f'{path}_embed_pair_offsets_valid']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._pair_mlp_1.weight.set_data( + np_slice(ckpt[f'{path}_pair_mlp_1']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._pair_mlp_2.weight.set_data( + np_slice(ckpt[f'{path}_pair_mlp_2']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._pair_mlp_3.weight.set_data( + np_slice(ckpt[f'{path}_pair_mlp_3']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_encoder._project_atom_features_for_aggr.weight.set_data( + np_slice(ckpt[f'{path}_project_atom_features_for_aggr']['weights'].T, i, j, dtype=dtype)) + load_cross_attention_transformer(atom_cross_att_encoder._atom_transformer_encoder, + f'{path}_atom_transformer_encoder', ckpt, + f"{last_name}_atom_transformer_encoder", i, j, dtype=dtype) + + +def load_atom_cross_decoder(atom_cross_att_decoder, path, ckpt, i=None, j=None, dtype=ms.bfloat16): + atom_cross_att_decoder._project_token_features_for_broadcast.weight.set_data( + np_slice(ckpt[f'{path}_project_token_features_for_broadcast']['weights'].T, i, j, dtype=dtype)) + atom_cross_att_decoder._atom_features_layer_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}_atom_features_layer_norm']['scale'], i, j, dtype=ms.float32)) + atom_cross_att_decoder._atom_features_to_position_update.weight.set_data( + np_slice(ckpt[f'{path}_atom_features_to_position_update']['weights'].T, i, j, dtype=dtype)) + load_cross_attention_transformer(atom_cross_att_decoder._atom_transformer_decoder, + f'{path}_atom_transformer_decoder', ckpt, + last_name='diffusion_atom_transformer_decoder', i=i, j=j, dtype=dtype) + + +def load_diffusion_head(diffusion_head, path, ckpt, i=None, j=None, dtype=ms.float32): + diffusion_head.pair_cond_initial_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/pair_cond_initial_norm']['scale'], i, j, dtype=ms.float32)) + diffusion_head.pair_cond_initial_projection.weight.set_data( + np_slice(ckpt[f'{path}/pair_cond_initial_projection']['weights'].T, i, j, dtype=ms.float32)) + load_transition_ms(diffusion_head.transition_block1, + f'{path}/pair_transition_0', ckpt, dtype=dtype) + load_transition_ms(diffusion_head.transition_block2, + f'{path}/pair_transition_1', ckpt, dtype=dtype) + diffusion_head.single_cond_initial_norm.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/single_cond_initial_norm']['scale'], i, j, dtype=ms.float32)) + diffusion_head.single_cond_initial_projection.weight.set_data( + np_slice(ckpt[f'{path}/single_cond_initial_projection']['weights'].T, i, j, dtype=dtype)) + diffusion_head.layer_norm_noise.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/noise_embedding_initial_norm']['scale'], i, j, dtype=ms.float32)) + diffusion_head.linear_noise.weight.set_data( + np_slice(ckpt[f'{path}/noise_embedding_initial_projection']['weights'].T, i, j, dtype=dtype)) + load_transition_ms(diffusion_head.single_transition1, + f'{path}/single_transition_0', ckpt, dtype=dtype) + load_transition_ms(diffusion_head.single_transition2, + f'{path}/single_transition_1', ckpt, dtype=dtype) + diffusion_head.layer_norm_act.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/single_cond_embedding_norm']['scale'], i, j, dtype=ms.float32)) + diffusion_head.linear_act.weight.set_data( + np_slice(ckpt[f'{path}/single_cond_embedding_projection']['weights'].T, i, j, dtype=dtype)) + diffusion_head.layer_norm_out.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/output_norm']['scale'], i, j, dtype=ms.float32)) + load_atom_cross_encoder(diffusion_head.atom_cross_att_encoder, f'{path}/diffusion', ckpt, + last_name="diffusion", dtype=dtype) + load_transformer_ms(diffusion_head.transformer, path + + '/transformer', ckpt, dtype=dtype) + load_atom_cross_decoder( + diffusion_head.atom_cross_att_decoder, f'{path}/diffusion', ckpt, dtype=dtype) + + +def load_confidence_head(confidence_head, path, ckpt, i=None, j=None, dtype=ms.float32): + confidence_head.left_target_feat_project.weight.set_data( + np_slice(ckpt[f'{path}/~_embed_features/left_target_feat_project']['weights'].T, i, j, dtype=dtype)) + confidence_head.right_target_feat_project.weight.set_data( + np_slice(ckpt[f'{path}/~_embed_features/right_target_feat_project']['weights'].T, i, j, dtype=dtype)) + confidence_head.distogram_feat_project.weight.set_data( + np_slice(ckpt[f'{path}/~_embed_features/distogram_feat_project']['weights'].T, i, j, dtype=dtype)) + for ii in range(confidence_head.config.pairformer.num_layer): + confidence_pairformer_path = path + \ + f'/__layer_stack_no_per_layer/confidence_pairformer' + load_pair_former(confidence_head.pairformer_block[ii], confidence_pairformer_path, + ckpt, ii, dtype=dtype) + confidence_head.left_half_distance_logits.weight.set_data( + np_slice(ckpt[f'{path}/left_half_distance_logits']['weights'].T, i, j, dtype=ms.float32)) + confidence_head.logits_ln.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/logits_ln']['scale'], i, j, dtype=ms.float32)) + confidence_head.logits_ln.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/logits_ln']['offset'], i, j, dtype=ms.float32)) + confidence_head.pae_logits.weight.set_data( + np_slice(ckpt[f'{path}/pae_logits']['weights'].T, i, j, dtype=ms.float32)) + confidence_head.pae_logits_ln.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/pae_logits_ln']['scale'], i, j, dtype=ms.float32)) + confidence_head.pae_logits_ln.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/pae_logits_ln']['offset'], i, j, dtype=ms.float32)) + confidence_head.plddt_logits.weight.set_data( + np_slice(ckpt[f'{path}/plddt_logits']['weights'], i, j, dtype=ms.float32)) + confidence_head.plddt_logits_ln.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/plddt_logits_ln']['scale'], i, j, dtype=ms.float32)) + confidence_head.plddt_logits_ln.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/plddt_logits_ln']['offset'], i, j, dtype=ms.float32)) + confidence_head.experimentally_resolved_logits.weight.set_data( + np_slice(ckpt[f'{path}/experimentally_resolved_logits']['weights'], i, j, dtype=ms.float32)) + confidence_head.experimentally_resolved_ln.layernorm.gamma.set_data( + np_slice(ckpt[f'{path}/experimentally_resolved_ln']['scale'], i, j, dtype=ms.float32)) + confidence_head.experimentally_resolved_ln.layernorm.beta.set_data( + np_slice(ckpt[f'{path}/experimentally_resolved_ln']['offset'], i, j, dtype=ms.float32)) + + +def load_diffuser(diffuser, ckpt_dir, dtype=ms.bfloat16): + path = 'diffuser' + ckpt = get_model_af3_params(pathlib.Path(ckpt_dir)) + load_evoformer(diffuser.embedding_module, path + + '/evoformer', ckpt, dtype=dtype) + load_distogram_head(diffuser.distogram_head, path + + '/distogram_head', ckpt, dtype=ms.float32) + load_atom_cross_encoder(diffuser.create_target_feat_embedding.atom_cross_att_encoder, + f'{path}/evoformer_conditioning', ckpt, + last_name='evoformer_conditioning', dtype=ms.float32) + load_diffusion_head(diffuser.diffusion_module, path + + '/~/diffusion_head', ckpt, dtype=ms.float32) + load_confidence_head(diffuser.confidence_head, path + + '/confidence_head', ckpt, dtype=ms.float32) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py new file mode 100644 index 000000000..312c9613a --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py @@ -0,0 +1,758 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +from dataclasses import dataclass +import random +import concurrent +import functools +from absl import logging +import numpy as np +import mindspore as ms +from mindspore import ops, nn +from alphafold3.constants import residue_names +from alphafold3.model import base_config +from alphafold3.model import confidences +from alphafold3.model import model_config +from alphafold3.model.atom_layout import atom_layout +from alphafold3.model.components import base_model +from alphafold3.model.components import base_modules as bm +from alphafold3.model.diffusion import atom_cross_attention +from alphafold3.model.diffusion import confidence_head +from alphafold3.model.diffusion import diffusion_head +from alphafold3.model.diffusion import distogram_head +from alphafold3.model.diffusion import featurization +from alphafold3.model.diffusion import modules +from alphafold3.model.diffusion import template_modules +from alphafold3.structure import mmcif + + +def get_predicted_structure(result, batch): + """Creates the predicted structure and ion preditions. + + Args: + result: model output in a model specific layout + batch: model input batch + + Returns: + Predicted structure. + """ + model_output_coords = result['diffusion_samples']['atom_positions'] + + # Rearrange model output coordinates to the flat output layout. + model_output_to_flat = atom_layout.compute_gather_idxs( + source_layout=batch.convert_model_output.token_atoms_layout, + target_layout=batch.convert_model_output.flat_output_layout, + ) + pred_flat_atom_coords = atom_layout.convert( + gather_info=model_output_to_flat, + arr=model_output_coords.asnumpy(), + layout_axes=(-3, -2), + ) + + predicted_lddt = result.get('predicted_lddt') + + if predicted_lddt is not None: + pred_flat_b_factors = atom_layout.convert( + gather_info=model_output_to_flat, + arr=predicted_lddt.asnumpy(), + layout_axes=(-2, -1), + ) + else: + # Handle models which don't have predicted_lddt outputs. + pred_flat_b_factors = np.zeros(pred_flat_atom_coords.shape[:-1]) + + (missing_atoms_indices,) = np.nonzero( + model_output_to_flat.gather_mask == 0) + if missing_atoms_indices.shape[0] > 0: + missing_atoms_flat_layout = batch.convert_model_output.flat_output_layout[ + missing_atoms_indices + ] + missing_atoms_uids = list( + zip( + missing_atoms_flat_layout.chain_id, + missing_atoms_flat_layout.res_id, + missing_atoms_flat_layout.res_name, + missing_atoms_flat_layout.atom_name, + ) + ) + logging.warning( + 'Target %s: warning: %s atoms were not predicted by the ' + 'model, setting their coordinates to (0, 0, 0). ' + 'Missing atoms: %s', + batch.convert_model_output.empty_output_struc.name, + missing_atoms_indices.shape[0], + missing_atoms_uids, + ) + + # Put them into a structure + pred_struc = batch.convert_model_output.empty_output_struc + pred_struc = pred_struc.copy_and_update_atoms( + atom_x=pred_flat_atom_coords[..., 0], + atom_y=pred_flat_atom_coords[..., 1], + atom_z=pred_flat_atom_coords[..., 2], + atom_b_factor=pred_flat_b_factors, + # Always 1.0. + atom_occupancy=np.ones(pred_flat_atom_coords.shape[:-1]), + ) + # Set manually/differently when adding metadata. + pred_struc = pred_struc.copy_and_update_globals(release_date=None) + return pred_struc + + +class CreateTargetFeatEmbedding(nn.Cell): + """ + A class that creates target feature embeddings by combining raw features with cross-attention encoded features. + + Args: + config (Config): Configuration object containing parameters for the target feature embedding. + global_config (GlobalConfig): Global configuration object. + + Inputs: + - **batch** (dict) - Dictionary containing batch features. + + Outputs: + - **target_feat** (Tensor) - Tensor of target feature embeddings. + """ + + def __init__(self, config, global_config, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.dtype = dtype + self.atom_cross_att_encoder = atom_cross_attention.AtomCrossAttEncoder( + self.config.per_atom_conditioning, self.global_config, '', with_cond=False, dtype=dtype + ) + + def construct(self, batch): + target_feat = featurization.create_target_feat( + batch, + append_per_atom_features=False, + dtype=ms.float32 + ).astype(self.dtype) + enc = self.atom_cross_att_encoder( + token_atoms_act=None, + trunk_single_cond=None, + trunk_pair_cond=None, + batch=batch, + ) + target_feat = ops.concat( + [target_feat, enc.token_act.astype(self.dtype)], axis=-1) + return target_feat + + +def _compute_ptm(result, num_tokens, asym_id, pae_single_mask, interface): + """Computes the pTM metrics from PAE.""" + return np.stack( + [ + confidences.predicted_tm_score( + tm_adjusted_pae=tm_adjusted_pae[:num_tokens, :num_tokens].asnumpy( + ), + asym_id=asym_id.asnumpy(), + pair_mask=pae_single_mask[:num_tokens, :num_tokens], + interface=interface, + ) + for tm_adjusted_pae in result['tmscore_adjusted_pae_global'] + ], + axis=0, + ) + + +def _compute_chain_pair_iptm( + num_tokens, + asym_ids, + mask, + tm_adjusted_pae): + """Computes the chain pair ipTM metrics from PAE.""" + return np.stack( + [ + confidences.chain_pairwise_predicted_tm_scores( + tm_adjusted_pae=sample_tm_adjusted_pae[:num_tokens], + asym_id=asym_ids[:num_tokens], + pair_mask=mask[:num_tokens, :num_tokens], + ) + for sample_tm_adjusted_pae in tm_adjusted_pae + ], + axis=0, + ) + + +class Diffuser(nn.Cell): + """ + Diffuser class for processing and generating diffusion samples, confidence scores, and distanceograms. + + Args: + config (Diffuser.Config): Configuration object containing parameters for the diffuser. + in_channel (int): Number of input channels. + feat_shape (tuple): Shape of the feature tensor. + act_shape (tuple): Shape of the activation tensor. + pair_shape (tuple): Shape of the pair tensor. + single_shape (tuple): Shape of the single tensor. + atom_shape (tuple): Shape of the atom tensor. + out_channel (int): Number of output channels. + num_templates (int): Number of templates. + + Inputs: + - **batch** (dict): Dictionary containing batch data. + - **key** (int): Random key generator. + + Outputs: + - **result** (dict): Dictionary containing diffusion samples, distanceogram, and confidence outputs. + """ + @dataclass + class HeadsConfig(base_config.BaseConfig): + diffusion: diffusion_head.DiffusionHead.Config = base_config.autocreate() + confidence: confidence_head.ConfidenceHead.Config = base_config.autocreate() + distogram: distogram_head.DistogramHead.Config = base_config.autocreate() + + @dataclass + class Config(base_config.BaseConfig): + evoformer: 'Evoformer.Config' = base_config.autocreate() + global_config: model_config.GlobalConfig = base_config.autocreate() + heads: 'Diffuser.HeadsConfig' = base_config.autocreate() + num_recycles: int = 10 + return_embeddings: bool = False + + def __init__(self, config, in_channel, feat_shape, act_shape, pair_shape, single_shape, atom_shape, + out_channel, num_templates, dtype=ms.float32, name="model"): + super().__init__(auto_prefix=True) + self.config = config + self.global_config = config.global_config + self.dtype = dtype + self.diffusion_module = diffusion_head.DiffusionHead( + self.config.heads.diffusion, self.global_config, pair_shape, dtype=ms.float32 + ) + self.embedding_module = Evoformer(self.config.evoformer, self.global_config, + feat_shape, act_shape, pair_shape, single_shape, num_templates, dtype=dtype) + self.create_target_feat_embedding = CreateTargetFeatEmbedding( + self.embedding_module.config, self.global_config, dtype=ms.float32) + self.confidence_head = confidence_head.ConfidenceHead( + self.config.heads.confidence, self.global_config, + pair_shape, single_shape, atom_shape, feat_shape[-1], out_channel, dtype=dtype + ) + self.distogram_head = distogram_head.DistogramHead( + self.config.heads.distogram, self.global_config, pair_shape[-1], dtype=ms.float32 + ) + + def _sample_diffusion(self, batch, embeddings, sample_config, key, init_positions=None): + denoising_step = functools.partial( + self.diffusion_module, + batch=batch, + embeddings=embeddings, + use_conditioning=True, + ) + sample = diffusion_head.sample( + denoising_step=denoising_step, + batch=batch, + key=key+1, + config=sample_config, + init_positions=init_positions, + ) + return sample + + def construct(self, batch, key): + if key is None: + # generate a random number + key = int(np.random.randint(100)) + # batch = feat_batch.Batch.from_data_dict(batch) + target_feat = self.create_target_feat_embedding( + batch) + + def recycle_body(prev, key): + key, subkey = random.randint(0, 1e6), key + embeddings = self.embedding_module( + batch=batch, + prev=prev, + target_feat=target_feat, + key=subkey, + ) + embeddings['pair'] = embeddings['pair'] + embeddings['single'] = embeddings['single'] + return embeddings, key + + num_res = batch.num_res + embeddings = { + 'pair': ops.zeros( + [num_res, num_res, self.config.evoformer.pair_channel], + dtype=ms.float32, + ), + 'single': ops.zeros( + [num_res, self.config.evoformer.seq_channel], dtype=ms.float32 + ), + 'target_feat': target_feat, + } + num_iter = self.config.num_recycles + 1 + for _ in range(num_iter): + embeddings, _ = recycle_body(embeddings, key) + + samples = self._sample_diffusion( + batch, + embeddings, + sample_config=self.config.heads.diffusion.eval, + key=key + ) + confidence_output = [] + for i in range(samples['atom_positions'].shape[0]): + confidence_output.append(self.confidence_head( + dense_atom_positions=samples['atom_positions'][i], + embeddings=embeddings, + seq_mask=batch.token_features.mask, + token_atoms_to_pseudo_beta=batch.pseudo_beta_info.token_atoms_to_pseudo_beta, + asym_id=batch.token_features.asym_id, + )) + for key in confidence_output[0].keys(): + confidence_output[0][key] = ops.stack( + [value[key] for value in confidence_output]) + confidence_output = confidence_output[0] + distogram = self.distogram_head(batch, embeddings) + output = { + 'diffusion_samples': samples, + 'distogram': distogram, + **confidence_output, + } + if self.config.return_embeddings: + output['single_embeddings'] = embeddings['single'] + output['pair_embeddings'] = embeddings['pair'] + return output + + @classmethod + def get_inference_result(cls, batch, result, target_name,): + """Get the predicted structure, scalars, and arrays for inference. + + This function also computes any inference-time quantities, which are not a + part of the forward-pass, e.g. additional confidence scores. Note that this + function is not serialized, so it should be slim if possible. + + Args: + batch: data batch used for model inference, incl. TPU invalid types. + result: output dict from the model's forward pass. + target_name: target name to be saved within structure. + + Yields: + inference_result: dataclass object that contains a predicted structure, + important inference-time scalars and arrays, as well as a slightly trimmed + dictionary of raw model result from the forward pass (for debugging). + """ + del target_name + # Retrieve structure and construct a predicted structure. + pred_structure = get_predicted_structure(result=result, batch=batch) + num_tokens = batch.token_features.seq_length.item() + pae_single_mask = np.tile( + batch.frames.mask[:, None], + [1, batch.frames.mask.shape[0]], + ) + ptm = _compute_ptm( + result=result, + num_tokens=num_tokens, + asym_id=batch.token_features.asym_id[:num_tokens], + pae_single_mask=pae_single_mask, + interface=False, + ) + iptm = _compute_ptm( + result=result, + num_tokens=num_tokens, + asym_id=batch.token_features.asym_id[:num_tokens], + pae_single_mask=pae_single_mask, + interface=True, + ) + ptm_iptm_average = 0.8 * iptm + 0.2 * ptm + + asym_ids = batch.token_features.asym_id[:num_tokens].asnumpy() + chain_ids = [mmcif.int_id_to_str_id(asym_id) for asym_id in asym_ids] + res_ids = batch.token_features.residue_index[:num_tokens] + + if len(np.unique(asym_ids)) > 1: + # There is more than one chain, hence interface pTM (i.e. ipTM) defined, + # so use it. + ranking_confidence = ptm_iptm_average + else: + # There is only one chain, hence ipTM=NaN, so use just pTM. + ranking_confidence = ptm + + contact_probs = result['distogram']['contact_probs'].astype(ms.float32) + # Compute PAE related summaries. + _, chain_pair_pae_min, _ = confidences.chain_pair_pae( + num_tokens=num_tokens, + asym_ids=batch.token_features.asym_id.asnumpy(), + full_pae=result['full_pae'].asnumpy(), + mask=pae_single_mask, + ) + chain_pair_pde_mean, chain_pair_pde_min = confidences.chain_pair_pde( + num_tokens=num_tokens, + asym_ids=batch.token_features.asym_id.asnumpy(), + full_pde=result['full_pde'].asnumpy(), + ) + intra_chain_single_pde, cross_chain_single_pde, _ = confidences.pde_single( + num_tokens, + batch.token_features.asym_id.asnumpy(), + result['full_pde'].asnumpy(), + contact_probs.asnumpy(), + ) + pae_metrics = confidences.pae_metrics( + num_tokens=num_tokens, + asym_ids=batch.token_features.asym_id.asnumpy(), + full_pae=result['full_pae'].asnumpy(), + mask=pae_single_mask, + contact_probs=contact_probs.asnumpy(), + tm_adjusted_pae=result['tmscore_adjusted_pae_interface'].asnumpy(), + ) + ranking_confidence_pae = confidences.rank_metric( + result['full_pae'].asnumpy(), + contact_probs.asnumpy() * batch.frames.mask[:, None].astype(float), + ) + chain_pair_iptm = _compute_chain_pair_iptm( + num_tokens=num_tokens, + asym_ids=batch.token_features.asym_id.asnumpy(), + mask=pae_single_mask, + tm_adjusted_pae=result['tmscore_adjusted_pae_interface'].asnumpy(), + ) + # iptm_ichain is a vector of per-chain ptm values. iptm_ichain[0], + # for example, is just the zeroth diagonal entry of the chain pair iptm + # matrix: + # [[x, , ], + # [ , , ], + # [ , , ]]] + iptm_ichain = chain_pair_iptm.diagonal(axis1=-2, axis2=-1) + # iptm_xchain is a vector of cross-chain interactions for each chain. + # iptm_xchain[0], for example, is an average of chain 0's interactions with + # other chains: + # [[ ,x,x], + # [x, , ], + # [x, , ]]] + iptm_xchain = confidences.get_iptm_xchain(chain_pair_iptm) + + predicted_distance_errors = result['average_pde'] + + # Computing solvent accessible area with dssp can be slow for large + # structures with lots of chains, so we parallelize the call. + pred_structures = pred_structure.unstack() + num_workers = len(pred_structures) + with concurrent.futures.ThreadPoolExecutor( + max_workers=num_workers + ) as executor: + has_clash = list(executor.map( + confidences.has_clash, pred_structures)) + fraction_disordered = list( + executor.map(confidences.fraction_disordered, pred_structures) + ) + for idx, pred_structure in enumerate(pred_structures): + ranking_score = confidences.get_ranking_score( + ptm=ptm[idx], + iptm=iptm[idx], + fraction_disordered_=fraction_disordered[idx], + has_clash_=has_clash[idx], + ) + print(f"####### result {idx} ######") + print(f"####### ranking_score {ranking_score} ######") + print(f"####### predicted_tm_score {ptm[idx]} ######") + print(f"####### interface_predicted_tm_score {iptm[idx]} ######") + yield base_model.InferenceResult( + predicted_structure=pred_structure, + numerical_data={ + 'full_pde': result['full_pde'][idx, :num_tokens, :num_tokens], + 'full_pae': result['full_pae'][idx, :num_tokens, :num_tokens], + 'contact_probs': contact_probs[:num_tokens, :num_tokens], + }, + metadata={ + 'predicted_distance_error': predicted_distance_errors[idx], + 'ranking_score': ranking_score, + 'fraction_disordered': fraction_disordered[idx], + 'has_clash': has_clash[idx], + 'predicted_tm_score': ptm[idx], + 'interface_predicted_tm_score': iptm[idx], + 'chain_pair_pde_mean': chain_pair_pde_mean[idx], + 'chain_pair_pde_min': chain_pair_pde_min[idx], + 'chain_pair_pae_min': chain_pair_pae_min[idx], + 'ptm': ptm[idx], + 'iptm': iptm[idx], + 'ptm_iptm_average': ptm_iptm_average[idx], + 'intra_chain_single_pde': intra_chain_single_pde[idx], + 'cross_chain_single_pde': cross_chain_single_pde[idx], + 'pae_ichain': pae_metrics['pae_ichain'][idx], + 'pae_xchain': pae_metrics['pae_xchain'][idx], + 'ranking_confidence': ranking_confidence[idx], + 'ranking_confidence_pae': ranking_confidence_pae[idx], + 'chain_pair_iptm': chain_pair_iptm[idx], + 'iptm_ichain': iptm_ichain[idx], + 'iptm_xchain': iptm_xchain[idx], + 'token_chain_ids': chain_ids, + 'token_res_ids': res_ids, + }, + ) + + +class Evoformer(nn.Cell): + """ + Evoformer class for generating 'single' and 'pair' embeddings in protein structure prediction. + + Args: + config (Evoformer.Config): Configuration object defining the parameters for the Evoformer module. + global_config (base_config.BaseConfig): Global configuration object containing general settings. + feat_shape (tuple): Shape of the feature tensor. + act_shape (tuple): Shape of the activation tensor. + pair_shape (tuple): Shape of the pair tensor. + single_shape (tuple): Shape of the single tensor. + num_templates (int): Number of templates used in the model. + + Inputs: + - **batch** (dict): Dictionary containing batch data including token features, MSA, and other + relevant information. + - **prev** (dict): Dictionary containing previous embeddings for 'single' and 'pair' activations. + - **target_feat** (Tensor): Target feature tensor used for generating embeddings. + - **key** (int): Random key for reproducibility. + + Outputs: + - **output** (dict): Dictionary containing the generated embeddings: + - **single** (Tensor): Single residue embeddings. + - **pair** (Tensor): Pairwise residue embeddings. + - **target_feat** (Tensor): Target feature tensor. + + Notes: + - The class processes input data through multiple modules including position encoding, bond embedding, + template embedding, MSA processing, and Pairformer iterations. + - The `construct` method iteratively processes the input data to generate rich embeddings for + downstream tasks in protein structure prediction. + """ + @dataclass + # pytype: disable=invalid-function-definition + class PairformerConfig(modules.PairFormerIteration.Config): + block_remat: bool = False + remat_block_size: int = 8 + + @dataclass + class Config(base_config.BaseConfig): + """Configuration for Evoformer.""" + + max_relative_chain: int = 2 + msa_channel: int = 64 + seq_channel: int = 384 + max_relative_idx: int = 32 + num_msa: int = 1024 + pair_channel: int = 128 + pairformer: 'Evoformer.PairformerConfig' = base_config.autocreate( + single_transition=base_config.autocreate(), + single_attention=base_config.autocreate(), + num_layer=48, + ) + per_atom_conditioning: atom_cross_attention.AtomCrossAttEncoderConfig = ( + base_config.autocreate( + per_token_channels=384, + per_atom_channels=128, + atom_transformer=base_config.autocreate( + num_intermediate_factor=2, + num_blocks=3, + ), + per_atom_pair_channels=16, + ) + ) + template: template_modules.TemplateEmbedding.Config = ( + base_config.autocreate() + ) + msa_stack: modules.EvoformerIteration.Config = base_config.autocreate() + + def __init__(self, config, global_config, feat_shape, act_shape, pair_shape, single_shape, + num_templates, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + in_channel = feat_shape[-1] + position_activations_in = 4 * self.config.max_relative_idx + \ + 4 + 2 * self.config.max_relative_chain + 2 + 1 + self.position_activations = bm.CustomDense( + position_activations_in, self.config.pair_channel, ndim=3, dtype=dtype) + self.left_single = bm.CustomDense( + in_channel, self.config.pair_channel, ndim=2, dtype=dtype) + self.right_single = bm.CustomDense( + in_channel, self.config.pair_channel, ndim=2, dtype=dtype) + self.bond_embedding = bm.CustomDense( + 1, self.config.pair_channel, ndim=3, dtype=dtype) + self.template_module = template_modules.TemplateEmbedding( + self.config.template, self.global_config, num_templates, act_shape, dtype=dtype + ) + self.msa_activations = bm.CustomDense( + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP + 3, self.config.msa_channel, ndim=3, dtype=dtype) + self.extra_msa_target_feat = bm.CustomDense( + in_channel, self.config.msa_channel, ndim=2, dtype=dtype) + evofromer_act_shape = (self.config.num_msa, + act_shape[1], self.config.msa_channel) + self.evoformer_stack = nn.CellList( + [ + modules.EvoformerIteration( + self.config.msa_stack, self.global_config, evofromer_act_shape, pair_shape, dtype=dtype + ) for _ in range(self.config.msa_stack.num_layer) + ] + ) + self.prev_embedding = bm.CustomDense( + pair_shape[-1], pair_shape[-1], ndim=3, dtype=dtype) + self.prev_embedding_layer_norm = bm.LayerNorm( + pair_shape, dtype=ms.float32) + self.single_activations = bm.CustomDense( + in_channel, self.config.seq_channel, ndim=2, dtype=dtype) + self.prev_single_embedding = bm.CustomDense( + self.config.seq_channel, self.config.seq_channel, ndim=2, dtype=dtype) + self.prev_single_embedding_layer_norm = bm.LayerNorm(act_shape[:-1] + + (self.config.seq_channel,), dtype=ms.float32) + self.pairformer_stack = nn.CellList( + [ + modules.PairFormerIteration( + self.config.pairformer, self.global_config, pair_shape, single_shape, with_single=True, dtype=dtype + ) for _ in range(self.config.pairformer.num_layer) + ] + ) + + def _relative_encoding(self, batch, pair_activations): + rel_feat = featurization.create_relative_encoding( + batch.token_features, + self.config.max_relative_idx, + self.config.max_relative_chain, + ) + rel_feat = rel_feat.astype(pair_activations.dtype) + pair_activations += self.position_activations(rel_feat) + return pair_activations + + def _seq_pair_embedding(self, token_features, target_feat): + left_single = self.left_single(target_feat)[:, None] + right_single = self.right_single(target_feat)[None] + dtype = left_single.dtype + pair_activations = left_single + right_single + num_residues = pair_activations.shape[0] + mask = token_features.mask + pair_mask = (mask[:, None] * mask[None, :]).astype(dtype) + assert pair_mask.shape == (num_residues, num_residues) + return pair_activations, pair_mask + + def _embed_bonds(self, batch, pair_activations): + """Embeds bond features and merges into pair activations.""" + # Construct contact matrix. + num_tokens = batch.token_features.token_index.shape[0] + contact_matrix = ops.zeros((num_tokens, num_tokens)) + + tokens_to_polymer_ligand_bonds = ( + batch.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds + ) + gather_idxs_polymer_ligand = tokens_to_polymer_ligand_bonds.gather_idxs + gather_mask_polymer_ligand = ( + tokens_to_polymer_ligand_bonds.gather_mask.prod(dim=1).astype( + gather_idxs_polymer_ligand.dtype + )[:, None] + ) + # If valid mask then it will be all 1's, so idxs should be unchanged. + gather_idxs_polymer_ligand = ( + gather_idxs_polymer_ligand * gather_mask_polymer_ligand + ) + tokens_to_ligand_ligand_bonds = ( + batch.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds + ) + gather_idxs_ligand_ligand = tokens_to_ligand_ligand_bonds.gather_idxs + gather_mask_ligand_ligand = tokens_to_ligand_ligand_bonds.gather_mask.prod( + dim=1 + ).astype(gather_idxs_ligand_ligand.dtype)[:, None] + gather_idxs_ligand_ligand = ( + gather_idxs_ligand_ligand * gather_mask_ligand_ligand + ) + gather_idxs = ops.concat( + [gather_idxs_polymer_ligand, gather_idxs_ligand_ligand] + ) + contact_matrix[gather_idxs[:, 0], gather_idxs[:, 1]] = 1.0 + contact_matrix[0, 0] = 0.0 + + bonds_act = self.bond_embedding( + contact_matrix[:, :, None].astype(pair_activations.dtype) + ) + return pair_activations + bonds_act + + def _embed_template_pair(self, batch, pair_activations, pair_mask, key): + """Embeds Templates and merges into pair activations.""" + dtype = pair_activations.dtype + key, subkey = key, key + 1 + + templates = batch.templates + asym_id = batch.token_features.asym_id + # Construct a mask such that only intra-chain template features are + # computed, since all templates are for each chain individually. + multichain_mask = (asym_id[:, None] == asym_id[None, :]).astype(dtype) + template_fn = functools.partial(self.template_module, key=subkey) + template_act = template_fn( + query_embedding=pair_activations, + templates=templates, + multichain_mask_2d=multichain_mask, + padding_mask_2d=pair_mask, + ) + return pair_activations + template_act, key + + def _embed_process_msa(self, msa_batch, pair_activations, pair_mask, key, target_feat): + """Processes MSA and returns updated pair activations.""" + dtype = pair_activations.dtype + msa_batch = featurization.truncate_msa_batch( + msa_batch, self.config.num_msa) + msa_feat = featurization.create_msa_feat(msa_batch).astype(dtype) + + msa_activations = self.msa_activations(msa_feat) + msa_activations += self.extra_msa_target_feat(target_feat)[None] + msa_mask = msa_batch.mask.astype(dtype) + # Evoformer MSA stack. + evoformer_input = {'msa': msa_activations, 'pair': pair_activations} + mask = {'msa': msa_mask, 'pair': pair_mask} + for i in range(self.config.msa_stack.num_layer): + evoformer_input = self.evoformer_stack[i](evoformer_input, mask) + + return evoformer_input['pair'], key + + def construct(self, batch, prev, target_feat, key): + + dtype = (ms.bfloat16 if self.global_config.bfloat16 == + 'all' else ms.float32) + pair_activations, pair_mask = self._seq_pair_embedding( + batch.token_features, target_feat + ) + pair_activations += self.prev_embedding( + self.prev_embedding_layer_norm( + prev['pair'] + ).astype(pair_activations.dtype) + ) + pair_activations = self._relative_encoding(batch, pair_activations) + pair_activations = self._embed_bonds( + batch=batch, pair_activations=pair_activations + ) + pair_activations, key = self._embed_template_pair( + batch=batch, + pair_activations=pair_activations, + pair_mask=pair_mask, + key=key, + ) + pair_activations, key = self._embed_process_msa( + msa_batch=batch.msa, + pair_activations=pair_activations, + pair_mask=pair_mask, + key=key, + target_feat=target_feat, + ) + del key # Unused after this point. + single_activations = self.single_activations(target_feat) + + single_activations += self.prev_single_embedding( + self.prev_single_embedding_layer_norm( + prev['single'].astype(single_activations.dtype) + ) + ) + for i in range(self.config.pairformer.num_layer): + pair_activations, single_activations = self.pairformer_stack[i]( + pair_activations, pair_mask, single_act=single_activations, + seq_mask=batch.token_features.mask.astype(dtype) + ) + output = { + 'single': single_activations, + 'pair': pair_activations, + 'target_feat': target_feat, + } + + return output diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py new file mode 100644 index 000000000..9ba957636 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py @@ -0,0 +1,562 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""modules for the Diffuser model.""" + +from dataclasses import dataclass +from typing import Literal + +import mindspore as ms +from mindspore import nn, ops, Tensor, mint +from mindchemistry.e3.utils import Ncon +from alphafold3.model import base_config +from alphafold3.utils.attention import attention +from alphafold3.utils.gated_linear_unit.gated_linear_unit import gated_linear_unit +from alphafold3.model.components import base_modules as bm +from alphafold3.model.components import mapping +from alphafold3.model.diffusion import diffusion_transformer +from alphafold3.model.diffusion.triangle import TriangleMultiplication as Triangle +from alphafold3.model.diffusion.triangle import OuterProductMean as ProductMean + + +def get_shard_size(num_residues, shard_spec): + shard_size = shard_spec[0][-1] + for num_residues_upper_bound, num_residues_shard_size in shard_spec: + shard_size = num_residues_shard_size + if ( + num_residues_upper_bound is None + or num_residues <= num_residues_upper_bound + ): + break + return shard_size + + +class TransitionBlock(nn.Cell): + """ + A transition block for transformer networks, implementing either a GLU-based or linear-based transformation. + + Args: + config (Config): Configuration object containing parameters for the transition block. + global_config (GlobalConfig): Global configuration object. + normalized_shape (tuple): Shape of the input tensor for normalization. + ndim (int): Number of dimensions of the input tensor. Default: ``3``. + + Inputs: + - **act** (Tensor) - Input activation tensor to be processed. + + Outputs: + - **output** (Tensor) - Output tensor after processing through the transition block. + """ + @dataclass + class Config(base_config.BaseConfig): + num_intermediate_factor: int = 4 + use_glu_kernel: bool = True + + def __init__( + self, config, global_config, normalized_shape, ndim=3, dtype=ms.float32 + ): + super().__init__() + self.config = config + self.global_config = global_config + num_channels = normalized_shape[-1] + self.num_intermediate = int( + num_channels * self.config.num_intermediate_factor) + self.layernorm = bm.LayerNorm( + normalized_shape, name='input_layer_norm', dtype=ms.float32) + if self.config.use_glu_kernel: + self.glu_weight = bm.custom_initializer( + 'relu', (num_channels, 2 * self.num_intermediate), dtype=dtype) + self.glu_weight = ms.Parameter(Tensor(self.glu_weight).reshape( + num_channels, 2, self.num_intermediate)) + else: + self.linear = bm.CustomDense(num_channels, self.num_intermediate * 2, + weight_init='zeros', ndim=ndim, dtype=dtype) + self.linear.weight = bm.custom_initializer( + 'zeros', self.linear.weight.shape, dtype=dtype) + self.out_linear = bm.CustomDense(self.num_intermediate, num_channels, + weight_init=self.global_config.final_init, ndim=ndim, dtype=dtype) + + def construct(self, act, broadcast_dim=0): + act = self.layernorm(act) + if self.config.use_glu_kernel: + c = gated_linear_unit( + x=act, + weight=self.glu_weight, + implementation=None, + activation=mint.nn.functional.silu, + precision=None + ) + else: + act = self.linear(act) + a, b = mint.split(act, act.shape[-1]//2, axis=-1) + c = mint.nn.functional.silu(a) * b + return self.out_linear(c) + + +class MSAAttention(nn.Cell): + """ + Multi-Head Self-Attention (MSA) attention mechanism for processing sequence and pair data. + + Args: + config (Config): Configuration object containing parameters for the attention mechanism. + global_config (GlobalConfig): Global configuration object. + act_shape (tuple): Shape of the activation tensor. + pair_shape (tuple): Shape of the pair tensor. + + Inputs: + - **act** (Tensor) - Input activation tensor. + - **mask** (Tensor) - Mask tensor to prevent attention weights from focusing on invalid positions. + - **pair_act** (Tensor) - Pair activation tensor. + + Outputs: + - **output** (Tensor) - Output tensor after processing through the attention mechanism. + """ + @dataclass + class Config(base_config.BaseConfig): + num_head: int = 8 + + def __init__(self, config, global_config, act_shape, pair_shape, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.actnorm = bm.LayerNorm(act_shape, dtype=ms.float32) + self.pairnorm = bm.LayerNorm(pair_shape, dtype=ms.float32) + num_channel = act_shape[-1] + value_dim = num_channel // self.config.num_head + self.pair_logits = bm.CustomDense(pair_shape[-1], self.config.num_head, use_bias=False, + weight_init='zeros', ndim=3, dtype=dtype) + self.v_projection = bm.CustomDense(num_channel, (self.config.num_head, value_dim), + use_bias=False, ndim=len(act_shape), dtype=dtype) + ncon_list1 = [-3, -2, 1] + ncon_list2 = [-1, 1, -3, -4] + self.ncon = Ncon([ncon_list1, ncon_list2]) + self.gating_query = bm.CustomDense( + num_channel, self.config.num_head * value_dim, weight_init='zeros', use_bias=False, ndim=3, dtype=dtype) + self.output_projection = bm.CustomDense(self.config.num_head * value_dim, num_channel, + weight_init=self.global_config.final_init, + use_bias=False, ndim=3, dtype=dtype) + + def construct(self, act, mask, pair_act): + act = self.actnorm(act) + pair_act = self.pairnorm(pair_act) + logits = self.pair_logits(pair_act).transpose([2, 0, 1]) + logits += 1e9 * (mint.max(mask, dim=0)[0] - 1.0) + weights = mint.softmax(logits, dim=-1) + v = self.v_projection(act) + v_avg = self.ncon([weights, v]) + v_avg = v_avg.reshape(v_avg.shape[:-2]+(-1,)) + gate_value = self.gating_query(act) + v_avg *= mint.sigmoid(gate_value) + out = self.output_projection(v_avg) + return out + + +class GridSelfAttention(nn.Cell): + """ + Self-attention mechanism that operates either per-sequence or per-residue. + + Args: + config (Config): Configuration object containing parameters for the attention mechanism. + global_config (GlobalConfig): Global configuration object. + transpose (bool): Whether to transpose the activation tensor during processing. + normalized_shape (tuple): Shape of the input tensor for normalization. + + Inputs: + - **act** (Tensor) - Input activation tensor. + - **pair_mask** (Tensor) - Mask tensor indicating valid regions in the input. + + Outputs: + - **output** (Tensor) - Output tensor after processing through the self-attention mechanism. + """ + @dataclass + class Config(base_config.BaseConfig): + num_head: int = 4 + + def __init__( + self, config, global_config, transpose, normalized_shape, dtype=ms.float32 + ): + super().__init__() + self.config = config + self.global_config = global_config + self.transpose = transpose + num_channels = normalized_shape[-1] + in_shape = normalized_shape[-1] + assert num_channels % self.config.num_head == 0 + qkv_dim = max(num_channels // self.config.num_head, 16) + qkv_shape = (self.config.num_head, qkv_dim) + self.q_projection = bm.CustomDense( + in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) + self.k_projection = bm.CustomDense( + in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) + self.v_projection = bm.CustomDense( + in_shape, qkv_shape, use_bias=False, ndim=3, dtype=dtype) + self.gating_query = bm.CustomDense( + num_channels, self.config.num_head * qkv_dim, weight_init='zeros', use_bias=False, ndim=3, dtype=dtype) + self.output_projection = bm.CustomDense(self.config.num_head * qkv_dim, num_channels, + weight_init=self.global_config.final_init, ndim=3, dtype=dtype) + self.act_norm = bm.LayerNorm(normalized_shape, dtype=ms.float32) + self.pair_bias_projection = bm.CustomDense( + num_channels, self.config.num_head, use_bias=False, weight_init='linear', ndim=3, dtype=dtype) + num_residues = normalized_shape[0] + self.chunk_size = get_shard_size( + num_residues, self.global_config.pair_attention_chunk_size + ) + + def _attention(self, act, mask, bias): + q = self.q_projection(act) + k = self.k_projection(act) + v = self.v_projection(act) + bias = ops.expand_dims(bias, 0) + weighted_avg = attention.dot_product_attention( + q, + k, + v, + mask=mask, + bias=bias, + logits_dtype=ms.float32, + precision=None, + implementation=self.global_config.flash_attention_implementation, + ) + weighted_avg = weighted_avg.reshape(weighted_avg.shape[:-2] + (-1,)) + gate_value = self.gating_query(act) + weighted_avg *= mint.sigmoid(gate_value) + return self.output_projection(weighted_avg) + + def construct(self, act, pair_mask): + """Builds a module. + + Arguments: + act: [num_seq, num_res, channels] activations tensor + pair_mask: [num_seq, num_res] mask of non-padded regions in the tensor. + Only used in inducing points attention currently. + + Returns: + Result of the self-attention operation. + """ + pair_mask = mint.swapaxes(pair_mask, -1, -2) + act = self.act_norm(act) + + non_batched_bias = self.pair_bias_projection(act) + non_batched_bias = non_batched_bias.transpose(2, 0, 1) + if self.transpose: + act = mint.swapaxes(act, -2, -3) + pair_mask = pair_mask[:, None, None, :].astype(ms.bool_) + act = self._attention(act, pair_mask, non_batched_bias) + if self.transpose: + act = mint.swapaxes(act, -2, -3) + return act + + +class TriangleMultiplication(nn.Cell): + """ + Implements triangle multiplication for tensor operations. + + Args: + config (Config): Configuration object specifying the equation and whether to use a GLU kernel. + global_config (GlobalConfig): Global configuration object. + in_channel (int): Number of input channels. + normalized_shape (tuple): Shape of the input tensor for normalization. + batch_size (int, optional): Batch size for processing. Default: ``None``. + + Inputs: + - **act** (Tensor) - Input activation tensor. + - **mask** (Tensor) - Mask tensor indicating valid regions in the input. + + Outputs: + - **out** (Tensor) - Output tensor after triangle multiplication. + """ + @dataclass + class Config(base_config.BaseConfig): + equation: Literal['ikc,jkc->ijc', 'kjc,kic->ijc'] + use_glu_kernel: bool = True + + def __init__(self, config, global_config, in_channel, normalized_shape, batch_size=None, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.triangle_multi = Triangle( + self.config, + self.global_config, + num_intermediate_channel=in_channel, + equation=self.config.equation, + normalized_shape=normalized_shape, + batch_size=batch_size, + dtype=dtype) + + def construct(self, act, mask): + out = self.triangle_multi(act, mask) + return out + + +class OuterProductMean(nn.Cell): + """ + Implements the OuterProductMean operation for tensor computations. + + Args: + config (Config): Configuration object containing parameters for the operation. + global_config (GlobalConfig): Global configuration object. + num_output_channel (int): Number of output channels. + in_channel (int): Number of input channels. + + Inputs: + - **act** (Tensor) - Input activation tensor. + - **mask** (Tensor) - Mask tensor indicating valid regions in the input. + + Outputs: + - **out** (Tensor) - Output tensor after applying the outer product mean operation. + """ + @dataclass + class Config(base_config.BaseConfig): + chunk_size: int = 128 + num_outer_channel: int = 32 + + def __init__(self, config, global_config, num_output_channel, in_channel, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.num_output_channel = num_output_channel + self.outer_product_mean = ProductMean(self.config.num_outer_channel, + in_channel, + self.num_output_channel, + dtype=dtype) + + def construct(self, act, mask): + mask_norm = ops.expand_dims(mint.matmul(mask.T, mask), -1) + out = self.outer_product_mean(act, mask, mask_norm) + return out + + +class PairFormerIteration(nn.Cell): + """ + Single Iteration of PairFormer, which processes pairwise and single activations in a single iteration. + + Args: + config (PairFormerIteration.Config): Configuration for the PairFormerIteration module. + global_config: Global configuration for the model. + normalized_shape (tuple): Shape of the input tensor for normalization. + single_shape (tuple | None): Shape of the single activation tensor. Default: ``None``. + with_single (bool): Whether to include single activation processing. Default: ``False``. + + Inputs: + - **act** (Tensor) - Pairwise activations tensor. + - **pair_mask** (Tensor) - Padding mask for pairwise activations. + - **single_act** (Tensor | None) - Single activations tensor, optional. + - **seq_mask** (Tensor | None) - Sequence mask, optional. + + Outputs: + - **act** (Tensor) - Processed pairwise activations tensor. + - **single_act** (Tensor) - Processed single activations tensor (if `with_single` is True). + """ + @dataclass + class Config(base_config.BaseConfig): + """Config for PairFormerIteration.""" + num_layer: int = 1 + pair_attention: GridSelfAttention.Config = base_config.autocreate() + pair_transition: TransitionBlock.Config = base_config.autocreate() + single_attention: diffusion_transformer.SelfAttentionConfig | None = base_config.autocreate() + single_transition: TransitionBlock.Config | None = base_config.autocreate() + triangle_multiplication_incoming: TriangleMultiplication.Config = ( + base_config.autocreate(equation='kjc,kic->ijc') + ) + triangle_multiplication_outgoing: TriangleMultiplication.Config = ( + base_config.autocreate(equation='ikc,jkc->ijc') + ) + shard_transition_blocks: bool = True + + def __init__(self, config, global_config, normalized_shape, single_shape=None, with_single=False, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.with_single = with_single + num_channel = normalized_shape[-1] + self.triangle_multiplication1 = TriangleMultiplication( + self.config.triangle_multiplication_outgoing, + self.global_config, + num_channel, + normalized_shape, + dtype=dtype + ) + self.triangle_multiplication2 = TriangleMultiplication( + self.config.triangle_multiplication_incoming, + self.global_config, + num_channel, + normalized_shape, + dtype=dtype + ) + self.grid_self_attention1 = GridSelfAttention( + self.config.pair_attention, + self.global_config, + False, + normalized_shape, + dtype=dtype + ) + self.grid_self_attention2 = GridSelfAttention( + self.config.pair_attention, + self.global_config, + True, + normalized_shape, + dtype=dtype + ) + self.transition_block = TransitionBlock( + self.config.pair_transition, self.global_config, normalized_shape, dtype=dtype + ) + num_residues = normalized_shape[0] + if self.config.shard_transition_blocks: + self.transition_block = mapping.sharded_apply( + self.transition_block, + get_shard_size( + num_residues, self.global_config.pair_transition_shard_spec + ) + ) + if self.with_single: + assert self.config.single_attention is not None + self.single_pair_logits_projection = bm.CustomDense( + num_channel, self.config.single_attention.num_head, ndim=3, dtype=dtype + ) + self.single_pair_logits_norm = bm.LayerNorm(normalized_shape, dtype=ms.float32) + self.single_attention = diffusion_transformer.SelfAttention( + self.config.single_attention, self.global_config, + single_shape[-1], normalized_shape, with_single_cond=False, dtype=dtype) + self.single_transition = TransitionBlock( + self.config.single_transition, + self.global_config, + single_shape, + 2, + dtype=dtype + ) + + def construct(self, act, pair_mask, single_act=None, seq_mask=None): + act += self.triangle_multiplication1(act, pair_mask) + act += self.triangle_multiplication2(act, pair_mask) + act += self.grid_self_attention1(act, pair_mask) + act += self.grid_self_attention2(act, pair_mask) + act += self.transition_block(act) + if self.with_single: + norm_act = self.single_pair_logits_norm(act) + pair_logits = self.single_pair_logits_projection(norm_act) + pair_logits = pair_logits.transpose((2, 0, 1)) + single_act += self.single_attention( + single_act, seq_mask, None, pair_logits + ) + single_act += self.single_transition(single_act, + broadcast_dim=None) + return act, single_act + return act + + +class EvoformerIteration(nn.Cell): + """ + EvoformerIteration is a single iteration of the Evoformer main stack, which processes + activations and masks through a series of attention and transformation layers to + update the MSA (Multiple Sequence Alignment) and pair representations. + + Args: + config (EvoformerIteration.Config): Configuration for the EvoformerIteration. + global_config (base_config.BaseConfig): Global configuration for the model. + act_shape (tuple): Shape of the activation tensor. + pair_shape (tuple): Shape of the pair tensor. + + Inputs: + - **activations** (dict): A dictionary containing the MSA and pair activations. + - **masks** (dict): A dictionary containing the MSA and pair masks. + + Outputs: + - **activations** (dict): A dictionary containing the updated MSA and pair activations. + """ + @dataclass + class Config(base_config.BaseConfig): + """Configuration for EvoformerIteration.""" + + num_layer: int = 4 + msa_attention: MSAAttention.Config = base_config.autocreate() + outer_product_mean: OuterProductMean.Config = base_config.autocreate() + msa_transition: TransitionBlock.Config = base_config.autocreate() + pair_attention: GridSelfAttention.Config = base_config.autocreate() + pair_transition: TransitionBlock.Config = base_config.autocreate() + triangle_multiplication_incoming: TriangleMultiplication.Config = ( + base_config.autocreate(equation='kjc,kic->ijc') + ) + triangle_multiplication_outgoing: TriangleMultiplication.Config = ( + base_config.autocreate(equation='ikc,jkc->ijc') + ) + shard_transition_blocks: bool = False + + def __init__(self, config, global_config, act_shape, pair_shape, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + num_channel = pair_shape[-1] + self.outer_product_mean = OuterProductMean( + config=self.config.outer_product_mean, + global_config=self.global_config, + num_output_channel=num_channel, + in_channel=act_shape[-1], + dtype=dtype + ) + self.msa_attention = MSAAttention(self.config.msa_attention, + self.global_config, act_shape, pair_shape, dtype=dtype) + self.msa_transition = TransitionBlock( + self.config.msa_transition, self.global_config, act_shape, dtype=dtype + ) + self.triangle_multiplication1 = TriangleMultiplication( + self.config.triangle_multiplication_outgoing, + self.global_config, + num_channel, + pair_shape, + dtype=dtype + ) + self.triangle_multiplication2 = TriangleMultiplication( + self.config.triangle_multiplication_incoming, + self.global_config, + num_channel, + pair_shape, + dtype=dtype + ) + self.pair_attention1 = GridSelfAttention( + self.config.pair_attention, + self.global_config, + False, + pair_shape, + dtype=dtype + ) + self.pair_attention2 = GridSelfAttention( + self.config.pair_attention, + self.global_config, + True, + pair_shape, + dtype=dtype + ) + self.transition_block = TransitionBlock( + self.config.msa_transition, self.global_config, pair_shape, dtype=dtype + ) + num_residues = act_shape[0] + if self.config.shard_transition_blocks: + self.transition_block = mapping.sharded_apply( + self.transition_block, + get_shard_size( + num_residues, self.global_config.pair_transition_shard_spec + ) + ) + + def construct(self, activations, masks): + msa_act, pair_act = activations["msa"], activations["pair"] + msa_mask, pair_mask = masks['msa'], masks['pair'] + pair_act += self.outer_product_mean(msa_act, msa_mask) + msa_act += self.msa_attention(msa_act, msa_mask, pair_act) + msa_act += self.msa_transition(msa_act) + pair_act += self.triangle_multiplication1(pair_act, pair_mask) + pair_act += self.triangle_multiplication2(pair_act, pair_mask) + pair_act += self.pair_attention1(pair_act, pair_mask) + pair_act += self.pair_attention2(pair_act, pair_mask) + pair_act += self.transition_block(pair_act) + return {"msa": msa_act, "pair": pair_act} diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/bias.npy b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/bias.npy new file mode 100644 index 0000000000000000000000000000000000000000..c7cd7468f857d0a2849491f4a3de779471901d38 GIT binary patch literal 1152 zcmbV}>rc~X0EK@ph{O#9Vi-Y?IuSZ_3Y8&B-*XxS>~L0e;w`n5(xL(Ztd&)*S}zk+ z)H>Zn0!9~vsu?nGt4d5W6T{UJMTePjRX0Rn${Z?UbYuU*o^R**bdu+wCSzXuvJq0T zv@yzPTAOW-nk9=;=EOx!kwxVcnl0w6g3Lm*(e$5B&B|YE8un{fWfhr*w_NdQ{FINU z$TrFTH>0Kc@b=1cuZ3T`}_&Y9a1iXZ{<<$vxYvE^SS$A9M8Y4g{$o^ z#DqK-%JeQIlvJVN@ON-dpTO&V3I^~01Tt(E_hO&H@S%kjHR0@dJ%oPOUeS=cjQmdiz^5x5q%xq1a&Ce??Ve>@r zr}NQV)LYDbHzqN(IE!W70W5m6O$7JGQrZ3p?wS_Jzn>4~%~O!M)(Pv%N0|C1lG3_l zWt}F0vQe|)wfz=^!je*rgS z#?gCjFms+1(Dw8n*jBg0TWh5AhcB=yuLW7v8lhZQGd7@w=7|B6U7n@#jY^oq;=VH~{Bfp4$obJ0vS{j92I+?P> zUbxcY>5|8y`)~&BO*fUHEj=iR9m7)pD%k2qGWmWzoP{X}?b`*F;cFrF*+ts!*_1E7 z4n=to%X1ZQpUM&sA6u!eTEnN#TeM!t|U(TeJq&)pJ)Q{D)Z#g8=b?qX_ K*TUf28vY9@O2t3` literal 0 HcmV?d00001 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/weight.npy b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/weight.npy new file mode 100644 index 0000000000000000000000000000000000000000..c595d2a5f23945d87a8589c43cb6879ee3a2de48 GIT binary patch literal 1152 zcmbV||3B1s9KchThevwM*9Se!a*G_>^>`H0$mjjKMOW$wY3h+p7k9o!xf50yN;nzS zs8p_eyVV1EOxnub>m^?sCbigDdziH5OS>#n8;jY$u;*{j=a0{`{=57F_82TWyy%D^ zLZ%2$6nOFlUUD~qkS~xYBq|f7@nH#x5wicfSQ@90Ep$bc^q_3Pom|$t3!R1hWBmWk zvh<;GHmJzrd`*gU+g(+7q}Tx*Z}sA-RJo`());~vH2C7Rp-2+1f%=FGP#920=E3tY zY5tJvm#Dy5(Wq@Q*$)MqU$Kl%H|At~k!h9wV6>SJ-$W*%Nyu?B;EAAbbUVh5o#*zM zz6W<#*;1*}g?vgzaJPJf^pRSx*(L#JRM)_oZ%?6q<|ru#52KZDE;WDj8AZiKvEG@R zutwSrRi!h!t8#z#wcSc^3Q1x$wQ*o^Wda7q!@-I=naMyiJ^1Y$+r(=Dqlp^y-jmE8 zjxHtnuZPgO>;UXYv!^WrJ8BgBLQ%aRU7Q?6o0A&2KAEG-3rvU7?p2hd5wOYSjU>#< z2ai^jHu~o?O!cWJiT#`*W%(=G{=gMxXOl_d@e-e@`{>%`38siMqXwCVRc+Lx=8sJ9 z{%$v%HoX96Rb%uyC2?0}D=0tiHUvKY3Js$57~i{`#F8EY4_9{OO&goeE}#srVhm3) zq_-=tQtJj4MAIC05kDqO$tPo-iFY7hFQ#sWv(k^Ba$p=_)YOm}3^PajH1acVWn zHidy#`HzqudJAS9J&3id>HVleuJe*R4Csn=9g(q=WF~BKRu1F3>IRmv!;-!{kqQ|z zrI>bTKZU86HvP5jCG%FdYM;gCu)XF!coPkqu(O=NvX-s~7>Y_Yjxg-`F`Mw2!1#Oi zta0cHuAOis(@KGWlFQ!Y zH&f4=NjzS+l3Qi9I34-0G=cjVoQyhgK_N{=8kpX*1q|68>9|H#Ubc zK?h)2K{=Ht?4o_#W8}s3vm%=;%A2}FMo(C?BsVxu-FeC{hGDa%zqn saH}A{n!9YsX)Rb=KLY!Eb6oW*YhA2WKJ4M0#z%?M7@p}0F2+Lo7aO`FEC2ui literal 0 HcmV?d00001 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py new file mode 100644 index 000000000..1749b19b8 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py @@ -0,0 +1,326 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""template modules""" +from dataclasses import dataclass +import mindspore as ms +from mindspore import nn, ops, Tensor, mint + +from alphafold3.model import base_config +from alphafold3.constants import residue_names +from alphafold3.utils import geometry +from alphafold3.model import protein_data_processing +from alphafold3.model.components import base_modules as bm +from alphafold3.model.diffusion import modules +from alphafold3.model.scoring import scoring + + +@dataclass +class DistogramFeaturesConfig(base_config.BaseConfig): + # The left edge of the first bin. + min_bin: float = 3.25 + # The left edge of the final bin. The final bin catches everything larger than + # `max_bin`. + max_bin: float = 50.75 + # The number of bins in the distogram. + num_bins: int = 39 + + +def dgram_from_positions(positions, config, dtype=ms.float32): + """Compute distogram from amino acid positions. + + Args: + positions: (num_res, 3) Position coordinates. + config: Distogram bin configuration. + + Returns: + Distogram with the specified number of bins. + """ + lower_breaks = mint.linspace( + config.min_bin, config.max_bin, config.num_bins) + lower_breaks = mint.square(lower_breaks) + upper_breaks = mint.concat( + [lower_breaks[1:], Tensor([1e8], dtype=ms.float32)], dim=-1) + dist2 = mint.sum(mint.square(ops.expand_dims(positions, axis=-2) + - ops.expand_dims(positions, axis=-3)), dim=-1, keepdim=True) + dgram = (dist2 > lower_breaks).astype(ms.float32) * \ + (dist2 < upper_breaks).astype(ms.float32) + return dgram + + +def slice_index(x, idx): + return ops.gather_d(x, 1, idx.reshape(-1, 1)).squeeze() + + +def make_backbone_rigid(positions, mask, group_indices,): + """Make backbone Rigid3Array and mask. + + Args: + positions: (num_res, num_atoms) of atom positions as Vec3Array. + mask: (num_res, num_atoms) for atom mask. + group_indices: (num_res, num_group, 3) for atom indices forming groups. + + Returns: + tuple of backbone Rigid3Array and mask (num_res,). + """ + backbone_indices = group_indices[:, 0] + + # main backbone frames differ in sidechain frame convention. + # for sidechain it's (C, CA, N), for backbone it's (N, CA, C) + # Hence using c, b, a, each of shape (num_res,). + c, b, a = [backbone_indices[..., i] for i in range(3)] + + rigid_mask = slice_index(mask, a) * \ + slice_index(mask, b) * slice_index(mask, c) + frame_positions = [] + for indices in [a, b, c]: + frame_positions.append(geometry.vector.tree_map( + lambda x, idx=indices: slice_index(x, idx), positions + )) + rotation = geometry.Rot3Array.from_two_vectors( + frame_positions[2] - frame_positions[1], + frame_positions[0] - frame_positions[1], + ) + rigid = geometry.Rigid3Array(rotation, frame_positions[1]) + return rigid, rigid_mask + + +class TemplateEmbedding(nn.Cell): + """ + Embed a set of templates. + + Args: + config (TemplateEmbedding.Config): Configuration for the template embedding. + global_config (base_config.BaseConfig): Global configuration for the model. + num_templates (int): Number of templates to process. + normalized_shape (tuple): Shape of the normalized input tensor. + num_atoms (int): Number of atoms per residue. Default: ``24``. + + Inputs: + - **query_embedding** (Tensor) - Query tensor of shape [num_res, num_res, num_channel]. + - **templates** (Templates) - Object containing template data. + - **padding_mask_2d** (Tensor) - Pair mask for attention operations of shape [num_res, num_res]. + - **multichain_mask_2d** (Tensor) - Pair mask for multichain operations of shape [num_res, num_res]. + - **key** (int) - Random key generator. + + Outputs: + - **embedding** (Tensor) - Output embedding tensor of shape [num_res, num_res, num_channels]. + """ + @dataclass + class Config(base_config.BaseConfig): + num_channels: int = 64 + template_stack: modules.PairFormerIteration.Config = base_config.autocreate( + num_layer=2, + pair_transition=base_config.autocreate(num_intermediate_factor=2), + ) + dgram_features: DistogramFeaturesConfig = base_config.autocreate() + + def __init__(self, config, global_config, num_templates, normalized_shape, num_atoms=24, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.num_residues = normalized_shape[0] + self.num_templates = num_templates + self.query_num_channels = normalized_shape[2] + self.num_atoms = num_atoms + self.template_embedder = SingleTemplateEmbedding( + self.config, self.global_config, normalized_shape, dtype=dtype) + self.output_linear = bm.CustomDense( + self.config.num_channels, self.query_num_channels, ndim=3, dtype=dtype) + self.output_linear.weight = bm.custom_initializer( + 'relu', (self.config.num_channels, self.query_num_channels), dtype=dtype) + + def construct(self, query_embedding, templates, padding_mask_2d, + multichain_mask_2d, key): + """Generate an embedding for a set of templates. + + Args: + query_embedding: [num_res, num_res, num_channel] a query tensor that will + be used to attend over the templates to remove the num_templates + dimension. + templates: A 'Templates' object. + padding_mask_2d: [num_res, num_res] Pair mask for attention operations. + multichain_mask_2d: [num_res, num_res] Pair mask for multichain. + key: random key generator. + + Returns: + An embedding of size [num_res, num_res, num_channels] + """ + subkeys = mint.arange(key, key + self.num_templates, 1) + summed_template_embeddings = mint.zeros( + (self.num_residues, self.num_residues, + self.config.num_channels), dtype=query_embedding.dtype + ) + + def scan_fn(carry, x): + templates, key = x + embedding = self.template_embedder( + query_embedding, + templates, + padding_mask_2d, + multichain_mask_2d, + key, + ) + return carry + embedding + for i in range(len(subkeys)): + summed_template_embeddings = scan_fn( + summed_template_embeddings, (templates[i], subkeys[i])) + embedding = summed_template_embeddings / (1e-7 + self.num_templates) + embedding = mint.nn.functional.relu(embedding) + embedding = self.output_linear(embedding) + return embedding + + +class SingleTemplateEmbedding(nn.Cell): + """ + Embed a single template. + + Args: + config: Configuration object containing model parameters. + global_config: Global configuration object. + normalized_shape (tuple): Shape for normalization layers. + + Inputs: + - **query_embedding** (Tensor) - Query embedding tensor of shape (num_res, num_res, num_channels). + - **templates** (Templates object) - Object containing single template data. + - **padding_mask_2d** (Tensor) - Padding mask tensor. + - **multichain_mask_2d** (Tensor) - Mask indicating intra-chain residue pairs. + - **key** (random.KeyArray) - Random key generator. + + Outputs: + - **output** (Tensor) - Template embedding tensor of shape (num_res, num_res, num_channels). + """ + + def __init__( + self, + config, + global_config, + normalized_shape, + dtype=ms.float32 + ): + super().__init__() + self.config = config + self.global_config = global_config + num_channels = self.config.num_channels + self.query_embedding_norm = bm.LayerNorm( + normalized_shape, dtype=ms.float32) + + # to be determined the shape of input, output and number of layers + num_layers = 9 + in_shape_list = [39, (), 31, 31, (), (), (), (), 128] + ndim_list = [3, 2, 3, 3, 2, 2, 2, 2, 3] + self.template_pair_embedding = ms.nn.CellList( + [ + bm.CustomDense( + in_shape_list[i], num_channels, weight_init="relu", ndim=ndim_list[i], dtype=dtype + ) + for i in range(num_layers) + ] + ) + self.template_stack = ms.nn.CellList( + [ + modules.PairFormerIteration( + self.config.template_stack, self.global_config, normalized_shape[:-1] + ( + num_channels,), dtype=dtype + ) + for _ in range(self.config.template_stack.num_layer) + ] + ) + self.output_layer_norm = bm.LayerNorm( + normalized_shape[:-1] + (num_channels,), dtype=ms.float32) + + def construct(self, query_embedding, templates, padding_mask_2d, multichain_mask_2d, key): + act = self.construct_input( + query_embedding, templates, multichain_mask_2d) + if self.config.template_stack.num_layer: + for i in range(self.config.template_stack.num_layer): + act = self.template_stack[i](act, padding_mask_2d) + act = self.output_layer_norm(act) + return act + + def construct_input(self, query_embedding, templates, multichain_mask_2d): + # Compute distogram feature for the template. + dtype = multichain_mask_2d.dtype + aatype = templates.aatype + dense_atom_mask = templates.atom_mask + dense_atom_positions = templates.atom_positions + dense_atom_positions *= dense_atom_mask[..., None] + pseudo_beta_positions, pseudo_beta_mask = [ms.Tensor(x) for x in scoring.pseudo_beta_fn( + templates.aatype, dense_atom_positions, dense_atom_mask + )] + pseudo_beta_mask_2d = ( + pseudo_beta_mask[:, None] * pseudo_beta_mask[None, :] + ) + pseudo_beta_mask_2d *= multichain_mask_2d + dgram = dgram_from_positions( + pseudo_beta_positions, self.config.dgram_features + ) + dgram *= pseudo_beta_mask_2d[..., None] + pseudo_beta_mask_2d = pseudo_beta_mask_2d.astype(dtype) + to_concat = [(dgram, 1), (pseudo_beta_mask_2d, 0)] + aatype = mint.nn.functional.one_hot( + aatype.astype(ms.int64), + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP, + ).astype(dtype) + to_concat.append((aatype[None, :, :], 1)) + to_concat.append((aatype[:, None, :], 1)) + template_group_indices = mint.index_select( + ms.Tensor(protein_data_processing.RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX), + 0, + templates.aatype, + ) + rigid, backbone_mask = make_backbone_rigid( + geometry.Vec3Array.from_array(dense_atom_positions), + dense_atom_mask, + template_group_indices, + ) + points = rigid.translation + x = rigid.translation.x.unsqueeze(-1) + y = rigid.translation.y.unsqueeze(-1) + z = rigid.translation.z.unsqueeze(-1) + xx = rigid.rotation.xx.unsqueeze(-1) + xy = rigid.rotation.xy.unsqueeze(-1) + xz = rigid.rotation.xz.unsqueeze(-1) + yx = rigid.rotation.yx.unsqueeze(-1) + yy = rigid.rotation.yy.unsqueeze(-1) + yz = rigid.rotation.yz.unsqueeze(-1) + zx = rigid.rotation.zx.unsqueeze(-1) + zy = rigid.rotation.zy.unsqueeze(-1) + zz = rigid.rotation.zz.unsqueeze(-1) + rigid = geometry.Rigid3Array(geometry.Rot3Array( + xx, xy, xz, yx, yy, yz, zx, zy, zz), geometry.Vec3Array(x, y, z)) + rigid_vec = rigid.inverse().apply_to_point(points) + + unit_vector = rigid_vec.normalized() + unit_vector = [unit_vector.x, unit_vector.y, unit_vector.z] + unit_vector = [x for x in unit_vector] + backbone_mask = backbone_mask + + backbone_mask_2d = backbone_mask[:, None] * backbone_mask[None, :] + backbone_mask_2d *= multichain_mask_2d + unit_vector = [x * backbone_mask_2d for x in unit_vector] + + # Note that the backbone_mask takes into account C, CA and N (unlike + # pseudo beta mask which just needs CB) so we add both masks as features. + to_concat.extend([(x, 0) for x in unit_vector]) + to_concat.append((backbone_mask_2d, 0)) + query_embedding = self.query_embedding_norm(query_embedding) + # Allow the template embedder to see the query embedding. Note this + # contains the position relative feature, so this is how the network knows + # which residues are next to each other. + to_concat.append((query_embedding, 1)) + + act = 0 + for i, (x, _) in enumerate(to_concat): + act += self.template_pair_embedding[i](x) + return act diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/triangle.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/triangle.py new file mode 100644 index 000000000..af006b7ea --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/triangle.py @@ -0,0 +1,262 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Triangle""" +import numpy as np +import mindspore as ms +import mindspore.nn as nn +import mindspore.common.dtype as mstype +from mindspore import Parameter, mint +from mindspore.common.tensor import Tensor +import mindspore.ops as ops +from mindspore.ops import operations as P +from mindspore.common.initializer import initializer +from mindsponge.common.utils import _memory_reduce +from mindsponge.cell.initializer import lecun_init +from mindsponge.cell.mask import MaskedLayerNorm +from mindchemistry.e3.utils import Ncon + +from alphafold3.utils.gated_linear_unit import gated_linear_unit +from alphafold3.model.components.base_modules import LayerNorm, CustomDense + + +class TriangleMultiplication(nn.Cell): + r""" + Triangle multiplication layer. for the detailed implementation process, refer to + `TriangleMultiplication `_. + + The information between the amino acid pair is integrated through the information of three edges ij, ik, jk, and + the result of the dot product between ik and jk is added to the edge of ij. + + Args: + num_intermediate_channel (float): The number of intermediate channel. + equation (str): The equation used in triangle multiplication layer. edge update forms + corresponding to 'incoming' and 'outgoing', + :math:`(ikc,jkc->ijc, kjc,kic->ijc)`. + layer_norm_dim (int): The last dimension length of the layer norm. + batch_size (int): The batch size of parameters in triangle multiplication. Default: ``None``. + + Inputs: + - **pair_act** (Tensor) - Tensor of pair_act. shape :math:`(N{res}, N{res}, layer\_norm\_dim)`. + - **pair_mask** (Tensor) - The mask for TriangleAttention matrix with shape. shape :math:`(N{res}, N{res})`. + - **index** (Tensor) - The index of while loop, only used in case of while control + flow. + + Outputs: + Tensor, the float tensor of the pair_act of the layer with shape :math:`(N{res}, N{res}, layer\_norm\_dim)`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindsponge.cell import TriangleMultiplication + >>> from mindspore import dtype as mstype + >>> from mindspore import Tensor + >>> model = TriangleMultiplication(num_intermediate_channel=64, + ... equation="ikc,jkc->ijc", layer_norm_dim=64, batch_size=0) + >>> input_0 = Tensor(np.ones((256, 256, 64)), mstype.float32) + >>> input_1 = Tensor(np.ones((256, 256)), mstype.float32) + >>> out = model(input_0, input_1, index=0) + >>> print(out.shape) + (256, 256, 64) + """ + + def __init__(self, config, global_config, num_intermediate_channel, equation, normalized_shape, + batch_size=None, dtype=ms.float32): + super().__init__() + self.config = config + self.global_config = global_config + self.num_intermediate_channel = num_intermediate_channel + self.left_norm_input = LayerNorm(normalized_shape, dtype=ms.float32) + self.center_norm = LayerNorm(normalized_shape, dtype=ms.float32) + self.projection = nn.Dense( + normalized_shape[-1], num_intermediate_channel * 2, has_bias=False, dtype=dtype) + self.gate = nn.Dense(normalized_shape[-1], num_intermediate_channel * 2, + weight_init=self.global_config.final_init, has_bias=False, dtype=dtype) + self.output_projection = CustomDense( + normalized_shape[-1], num_intermediate_channel, weight_init=self.global_config.final_init, + ndim=3, dtype=dtype) + self.gating_linear = CustomDense( + num_intermediate_channel, num_intermediate_channel, weight_init=self.global_config.final_init, + ndim=3, dtype=dtype) + self.weight_glu = mint.stack( + [self.gate.weight.T, self.projection.weight.T], dim=1) + if self.config.equation == "ikc,jkc->ijc": + ncon_list = [[-1, -2, 1], [-1, -3, 1]] + elif self.config.equation == "kjc,kic->ijc": + ncon_list = [[-1, 1, -3], [-1, 1, -2]] + else: + raise ValueError("Not support this equation.") + self.ncon = Ncon(ncon_list) + + def construct(self, act, mask, use_glu=True): + r""" + Builds triangle multiplication module. + + Args: + act(Tensor): Pair activations. Data type is float. + mask(Tensor): Pair mask. Data type is float. + + Returns: + act(Tensor), the shape is same as act_shape[:-1]. + """ + self.weight_glu = mint.stack( + [self.gate.weight.T, self.projection.weight.T], dim=1) + + mask = mask[None, ...] + act = self.left_norm_input(act) + input_act = act + + if use_glu is True: + projection = gated_linear_unit.gated_linear_unit( + x=act, + weight=self.weight_glu, + activation=ms.mint.sigmoid, + implementation=None, + precision=None, + ) + projection = ops.transpose(projection, (2, 0, 1)) + projection *= mask + else: + projection = self.projection(act) + projection = ops.transpose(projection, (2, 0, 1)) + projection *= mask + gate = self.gate(act) + gate = ops.transpose(gate, (2, 0, 1)) + projection *= ms.mint.sigmoid(gate) + projection = projection.reshape( + self.num_intermediate_channel, 2, *projection.shape[1:]) + a, b = projection[:, 0], projection[:, 1] + act = self.ncon([a, b]) + act = self.center_norm(act.transpose((1, 2, 0))) + act = self.output_projection(act) + gate_out = self.gating_linear(input_act) + act *= mint.sigmoid(gate_out) + return act + + +class OuterProductMean(nn.Cell): + r""" + Computing the correlation of the input tensor along its second dimension, the computed correlation + could be used to update the correlation features(e.g. the Pair representation). + + .. math:: + OuterProductMean(\mathbf{act}) = Linear(flatten(mean(\mathbf{act}\otimes\mathbf{act}))) + + Args: + num_outer_channel (float): The last dimension size of intermediate layer in OuterProductMean. + act_dim (int): The last dimension size of the input act. + num_output_channel (int): The last dimension size of output. + batch_size(int): The batch size of parameters in OuterProductMean, + used in while control flow. Default: "None". + slice_num (int): The slice num used in OuterProductMean layer + when the memory is overflow. Default: 0. + + Inputs: + - **act** (Tensor) - The input tensor with shape :math:`(dim_1, dim_2, act\_dim)`. + - **mask** (Tensor) - The mask for OuterProductMean with shape :math:`(dim_1, dim_2)`. + - **mask_norm** (Tensor) - Squared L2-norm along the first dimension of **mask**, + pre-computed to avoid re-computing, its shape is :math:`(dim_2, dim_2, 1)`. + - **index** (Tensor) - The index of while loop, only used in case of while control + flow. Default: "None". + + Outputs: + Tensor, the float tensor of the output of OuterProductMean layer with + shape :math:`(dim_2, dim_2, num\_output\_channel)`. + + Supported Platforms: + ``Ascend`` ``GPU`` + + Examples: + >>> import numpy as np + >>> from mindsponge.cell import OuterProductMean + >>> from mindspore import dtype as mstype + >>> from mindspore import Tensor + >>> from mindspore.ops import operations as P + >>> model = OuterProductMean(num_outer_channel=32, act_dim=128, num_output_channel=256) + >>> act = Tensor(np.ones((32, 64, 128)), mstype.float32) + >>> mask = Tensor(np.ones((32, 64)), mstype.float32) + >>> mask_norm = P.ExpandDims()(P.MatMul(transpose_a=True)(mask, mask), -1) + >>> output= model(act, mask, mask_norm) + >>> print(output.shape) + (64, 64, 256) + """ + + def __init__(self, num_outer_channel, act_dim, num_output_channel, batch_size=None, slice_num=0, dtype=ms.float32): + super(OuterProductMean, self).__init__() + self.dtype = dtype + self.num_output_channel = num_output_channel + self.num_outer_channel = num_outer_channel + self.layer_norm_input = MaskedLayerNorm() + self.matmul_trans_b = P.MatMul(transpose_b=True) + self.matmul = P.MatMul() + self.batch_matmul_trans_b = P.BatchMatMul(transpose_b=True) + self.act_dim = act_dim + self.batch_size = batch_size + self.slice_num = slice_num + self.idx = Tensor(0, mstype.int32) + self._init_parameter() + + def construct(self, act, mask, mask_norm, index=None): + """Compute outer product mean.""" + mask = P.ExpandDims()(mask, -1) + act = self.layer_norm_input( + act, self.layer_norm_input_gamma, self.layer_norm_input_beta) + act_shape = P.Shape()(act) + if len(act_shape) != 2: + act = P.Reshape()(act, (-1, act_shape[-1])) + out_shape = act_shape[:-1] + (-1,) + left_act = mask * P.Reshape()( + P.BiasAdd()(self.matmul_trans_b(act, self.left_projection_weight), self.left_projection_bias), out_shape) + right_act = mask * P.Reshape()( + P.BiasAdd()(self.matmul_trans_b(act, self.right_projection_weight), self.right_projection_bias), out_shape) + _, d, e = right_act.shape + batched_inputs = (left_act,) + nonbatched_inputs = (right_act, self.linear_output_weight, + self.o_biases, d, e) + act = _memory_reduce(self._compute, batched_inputs, + nonbatched_inputs, self.slice_num, 1) + epsilon = 1e-3 + act = P.RealDiv()(act, epsilon + mask_norm) + return act + + def _init_parameter(self): + '''init parameter''' + self.layer_norm_input_gamma = Parameter( + Tensor(np.ones((self.act_dim)), self.dtype)) + self.layer_norm_input_beta = Parameter( + Tensor(np.zeros((self.act_dim)), self.dtype)) + self.left_projection_weight = Parameter( + initializer(lecun_init(self.act_dim), [self.num_outer_channel, self.act_dim], self.dtype)) + self.left_projection_bias = Tensor( + np.zeros((self.num_outer_channel)), self.dtype) + self.right_projection_weight = Parameter( + initializer(lecun_init(self.act_dim), [self.num_outer_channel, self.act_dim], self.dtype)) + self.right_projection_bias = Tensor( + np.zeros((self.num_outer_channel)), self.dtype) + self.linear_output_weight = Parameter( + Tensor(np.zeros((self.num_outer_channel, self.num_outer_channel, self.num_output_channel)), + self.dtype)) + self.o_biases = Parameter( + Tensor(np.zeros((self.num_output_channel)), self.dtype)) + + def _compute(self, left_act, right_act, linear_output_weight, linear_output_bias, d, e): + '''compute outer product mean''' + + left_act = left_act.transpose((0, 2, 1)) + act = Ncon([[1, -2, -4], [1, -1, -3]])([left_act, right_act]) + act = Ncon([[-1, 1, 2, -2], [1, 2, -3]] + )([act, linear_output_weight]) + linear_output_bias + act = P.Transpose()(act, (1, 0, 2)) + return act diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/feat_batch.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/feat_batch.py new file mode 100644 index 000000000..bc69b9d3e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/feat_batch.py @@ -0,0 +1,180 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Batch dataclass.""" + +import dataclasses +from typing import Self +import mindspore as ms +from mindspore import Tensor +from alphafold3.model import features + + +@dataclasses.dataclass +class Batch: + """Dataclass containing batch.""" + + msa: features.MSA + templates: features.Templates + token_features: features.TokenFeatures + ref_structure: features.RefStructure + predicted_structure_info: features.PredictedStructureInfo + polymer_ligand_bond_info: features.PolymerLigandBondInfo + ligand_ligand_bond_info: features.LigandLigandBondInfo + pseudo_beta_info: features.PseudoBetaInfo + atom_cross_att: features.AtomCrossAtt + convert_model_output: features.ConvertModelOutput + frames: features.Frames + + @property + def num_res(self) -> int: + return self.token_features.aatype.shape[-1] + + @staticmethod + def gather_to_tensor(input_feat): + input_feat.gather_idxs = Tensor(input_feat.gather_idxs) + input_feat.gather_mask = Tensor(input_feat.gather_mask) + input_feat.input_shape = Tensor(input_feat.input_shape) + + @classmethod + def from_data_dict(cls, batch: features.BatchDict) -> Self: + """Construct batch object from dictionary.""" + return cls( + msa=features.MSA.from_data_dict(batch), + templates=features.Templates.from_data_dict(batch), + token_features=features.TokenFeatures.from_data_dict(batch), + ref_structure=features.RefStructure.from_data_dict(batch), + predicted_structure_info=features.PredictedStructureInfo.from_data_dict( + batch + ), + polymer_ligand_bond_info=features.PolymerLigandBondInfo.from_data_dict( + batch + ), + ligand_ligand_bond_info=features.LigandLigandBondInfo.from_data_dict( + batch + ), + pseudo_beta_info=features.PseudoBetaInfo.from_data_dict(batch), + atom_cross_att=features.AtomCrossAtt.from_data_dict(batch), + convert_model_output=features.ConvertModelOutput.from_data_dict( + batch), + frames=features.Frames.from_data_dict(batch), + ) + + def as_data_dict(self) -> features.BatchDict: + """Converts batch object to dictionary.""" + output = { + **self.msa.as_data_dict(), + **self.templates.as_data_dict(), + **self.token_features.as_data_dict(), + **self.ref_structure.as_data_dict(), + **self.predicted_structure_info.as_data_dict(), + **self.polymer_ligand_bond_info.as_data_dict(), + **self.ligand_ligand_bond_info.as_data_dict(), + **self.pseudo_beta_info.as_data_dict(), + **self.atom_cross_att.as_data_dict(), + **self.convert_model_output.as_data_dict(), + **self.frames.as_data_dict(), + } + return output + + def convert_to_tensor(self, dtype=ms.float32): + # msa: features.MSA + self.msa.rows = Tensor(self.msa.rows, dtype=ms.int32) + self.msa.mask = Tensor(self.msa.mask, dtype=ms.int32) + self.msa.deletion_matrix = Tensor( + self.msa.deletion_matrix, dtype=dtype) + self.msa.deletion_mean = Tensor(self.msa.deletion_mean, dtype=dtype) + self.msa.profile = Tensor(self.msa.profile, dtype=dtype) + self.msa.num_alignments = Tensor( + self.msa.num_alignments, dtype=ms.int32) + # templates: features.Templates + self.templates.aatype = Tensor(self.templates.aatype, dtype=ms.int32) + self.templates.atom_mask = Tensor( + self.templates.atom_mask, dtype=ms.int32) + self.templates.atom_positions = Tensor( + self.templates.atom_positions, dtype=dtype) + # token_features: features.TokenFeatures + self.token_features.mask = Tensor( + self.token_features.mask, dtype=ms.int32) + self.token_features.token_index = Tensor( + self.token_features.mask, dtype=ms.int32) + self.token_features.asym_id = Tensor( + self.token_features.asym_id, dtype=ms.int32) + self.token_features.aatype = Tensor( + self.token_features.aatype, dtype=ms.int32) + self.token_features.residue_index = Tensor( + self.token_features.residue_index, dtype=ms.int32) + self.token_features.entity_id = Tensor( + self.token_features.entity_id, dtype=ms.int32) + self.token_features.sym_id = Tensor( + self.token_features.sym_id, dtype=ms.int32) + # ref_structure: features.RefStructure + self.ref_structure.positions = Tensor( + self.ref_structure.positions, dtype=dtype) + self.ref_structure.mask = Tensor(self.ref_structure.mask, dtype=dtype) + self.ref_structure.element = Tensor( + self.ref_structure.element, dtype=ms.int32) + self.ref_structure.charge = Tensor( + self.ref_structure.charge, dtype=dtype) + self.ref_structure.atom_name_chars = Tensor( + self.ref_structure.atom_name_chars, dtype=ms.int32) + self.ref_structure.ref_space_uid = Tensor( + self.ref_structure.ref_space_uid, dtype=dtype) + + # predicted_structure_info: features.PredictedStructureInfo + self.predicted_structure_info.atom_mask = Tensor( + self.predicted_structure_info.atom_mask, dtype=dtype) + + # polymer_ligand_bond_info: features.PolymerLigandBondInfo + self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_idxs = Tensor( + self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_idxs, dtype=ms.int32 + ) + self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_mask = Tensor( + self.polymer_ligand_bond_info.tokens_to_polymer_ligand_bonds.gather_mask, dtype=ms.int32 + ) + # ligand_ligand_bond_info: features.LigandLigandBondInfo + self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_idxs = Tensor( + self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_idxs, dtype=ms.int32 + ) + self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_mask = Tensor( + self.ligand_ligand_bond_info.tokens_to_ligand_ligand_bonds.gather_mask, dtype=ms.int32 + ) + + self.gather_to_tensor(self.pseudo_beta_info.token_atoms_to_pseudo_beta) + self.gather_to_tensor(self.atom_cross_att.queries_to_keys) + self.gather_to_tensor(self.atom_cross_att.tokens_to_queries) + self.gather_to_tensor(self.atom_cross_att.tokens_to_keys) + self.gather_to_tensor(self.atom_cross_att.token_atoms_to_queries) + self.gather_to_tensor(self.atom_cross_att.queries_to_token_atoms) + + # frames: features.Frames + + def astype(self, dtype=ms.float32): + # change dtype of float + # msa: features.MSA + self.msa.deletion_matrix = self.msa.deletion_matrix.astype(dtype) + self.msa.deletion_mean = self.msa.deletion_mean.astype(dtype) + self.msa.profile = self.msa.profile.astype(dtype) + # templates: features.Templates + self.templates.atom_positions = self.templates.atom_positions.astype( + dtype) + # ref_structure: features.RefStructure + self.ref_structure.positions = self.ref_structure.positions.astype( + dtype) + self.ref_structure.mask = self.ref_structure.mask.astype(dtype) + self.ref_structure.charge = self.ref_structure.charge.astype(dtype) + self.ref_structure.ref_space_uid = self.ref_structure.ref_space_uid.astype( + dtype) + + # predicted_structure_info: features.PredictedStructureInfo + self.predicted_structure_info.atom_mask = self.predicted_structure_info.atom_mask.astype( + dtype) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/features.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/features.py new file mode 100644 index 000000000..da9f13069 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/features.py @@ -0,0 +1,2103 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Data-side of the input features processing.""" + +import dataclasses +import datetime +import itertools +import numpy as np +from typing_extensions import Any, Self, TypeAlias +from rdkit import Chem +from rdkit.Chem import AllChem +from absl import logging +from alphafold3 import structure +from alphafold3.common import folding_input +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.constants import periodic_table +from alphafold3.constants import residue_names +from alphafold3.data import msa as msa_module +from alphafold3.data import templates +from alphafold3.data.tools import rdkit_utils +from alphafold3.model import data3 +from alphafold3.model import data_constants +from alphafold3.model import merging_features +from alphafold3.model import msa_pairing +from alphafold3.model.atom_layout import atom_layout +from alphafold3.structure import chemical_components as struc_chem_comps + + +xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name +BatchDict: TypeAlias = dict[str, xnp_ndarray] + +_STANDARD_RESIDUES = frozenset({ + *residue_names.PROTEIN_TYPES_WITH_UNKNOWN, + *residue_names.NUCLEIC_TYPES_WITH_2_UNKS, +}) + + +@dataclasses.dataclass +class PaddingShapes: + num_tokens: int + msa_size: int + num_chains: int + num_templates: int + num_atoms: int + + +def _pad_to( + arr: np.ndarray, shape: tuple[int | None, ...], **kwargs +) -> np.ndarray: + """Pads an array to a given shape. Wrapper around np.pad(). + + Args: + arr: numpy array to pad + shape: target shape, use None for axes that should stay the same + **kwargs: additional args for np.pad, e.g. constant_values=-1 + + Returns: + the padded array + + Raises: + ValueError if arr and shape have a different number of axes. + """ + if arr.ndim != len(shape): + raise ValueError( + f'arr and shape have different number of axes. {arr.shape=}, {shape=}' + ) + + num_pad = [] + for axis, width in enumerate(shape): + if width is None: + num_pad.append((0, 0)) + else: + if width >= arr.shape[axis]: + num_pad.append((0, width - arr.shape[axis])) + else: + raise ValueError( + f'Can not pad to a smaller shape. {arr.shape=}, {shape=}' + ) + padded_arr = np.pad(arr, pad_width=num_pad, **kwargs) + return padded_arr + + +def _unwrap(obj): + """Unwrap an object from a zero-dim np.ndarray.""" + if isinstance(obj, np.ndarray) and obj.ndim == 0: + return obj.item() + else: + return obj + + +@dataclasses.dataclass +class Chains: + chain_id: np.ndarray + asym_id: np.ndarray + entity_id: np.ndarray + sym_id: np.ndarray + + +def _compute_asym_entity_and_sym_id( + all_tokens: atom_layout.AtomLayout, +) -> Chains: + """Compute asym_id, entity_id and sym_id. + + Args: + all_tokens: atom layout containing a representative atom for each token. + + Returns: + A Chains object + """ + + # Find identical sequences and assign entity_id and sym_id to every chain. + seq_to_entity_id_sym_id = {} + seen_chain_ids = set() + chain_ids = [] + asym_ids = [] + entity_ids = [] + sym_ids = [] + for chain_id in all_tokens.chain_id: + if chain_id not in seen_chain_ids: + asym_id = len(seen_chain_ids) + 1 + seen_chain_ids.add(chain_id) + seq = ','.join( + all_tokens.res_name[all_tokens.chain_id == chain_id]) + if seq not in seq_to_entity_id_sym_id: + entity_id = len(seq_to_entity_id_sym_id) + 1 + sym_id = 1 + else: + entity_id, sym_id = seq_to_entity_id_sym_id[seq] + sym_id += 1 + seq_to_entity_id_sym_id[seq] = (entity_id, sym_id) + + chain_ids.append(chain_id) + asym_ids.append(asym_id) + entity_ids.append(entity_id) + sym_ids.append(sym_id) + + return Chains( + chain_id=np.array(chain_ids), + asym_id=np.array(asym_ids), + entity_id=np.array(entity_ids), + sym_id=np.array(sym_ids), + ) + + +def tokenizer( + flat_output_layout: atom_layout.AtomLayout, + ccd: chemical_components.Ccd, + max_atoms_per_token: int, + flatten_non_standard_residues: bool, + logging_name: str, +) -> tuple[atom_layout.AtomLayout, atom_layout.AtomLayout, np.ndarray]: + """Maps a flat atom layout to tokens for evoformer. + + Creates the evoformer tokens as one token per polymer residue and one token + per ligand atom. The tokens are represented as AtomLayouts all_tokens + (1 representative atom per token) atoms per residue, and + all_token_atoms_layout (num_tokens, max_atoms_per_token). The atoms in a + residue token use the layout of the corresponding CCD entry + + Args: + flat_output_layout: flat AtomLayout containing all atoms that the model + wants to predict. + ccd: The chemical components dictionary. + max_atoms_per_token: number of slots per token. + flatten_non_standard_residues: whether to flatten non-standard residues, + i.e. whether to use one token per atom for non-standard residues. + logging_name: logging name for debugging (usually the mmcif_id). + + Returns: + A tuple (all_tokens, all_tokens_atoms_layout) with + all_tokens: AtomLayout shape (num_tokens,) containing one representative + atom per token. + all_token_atoms_layout: AtomLayout with shape + (num_tokens, max_atoms_per_token) containing all atoms per token. + standard_token_idxs: The token index that each token would have if not + flattening non standard resiudes. + """ + # Select the representative atom for each token. + token_idxs = [] + single_atom_token = [] + standard_token_idxs = [] + current_standard_token_id = 0 + # Iterate over residues, and provide a group_iter over the atoms of each + # residue. + for key, group_iter in itertools.groupby( + zip( + flat_output_layout.chain_type, + flat_output_layout.chain_id, + flat_output_layout.res_id, + flat_output_layout.res_name, + flat_output_layout.atom_name, + np.arange(flat_output_layout.shape[0]), + ), + key=lambda x: x[:3], + ): + + # Get chain type and chain id of this residue + chain_type, chain_id, _ = key + + # Get names and global idxs for all atoms of this residue + _, _, _, res_names, atom_names, idxs = zip(*group_iter) + + # As of March 2023, all OTHER CHAINs in pdb are artificial nucleics. + is_nucleic_backbone = ( + chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES + or chain_type == mmcif_names.OTHER_CHAIN + ) + if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: + res_name = res_names[0] + if ( + flatten_non_standard_residues + and res_name not in residue_names.PROTEIN_TYPES_WITH_UNKNOWN + and res_name != residue_names.MSE + ): + # For non-standard protein residues take all atoms. + # NOTE: This may get very large if we include hydrogens. + token_idxs.extend(idxs) + single_atom_token += [True] * len(idxs) + standard_token_idxs.extend( + [current_standard_token_id] * len(idxs)) + else: + # For standard protein residues take 'CA' if it exists, else first atom. + if 'CA' in atom_names: + token_idxs.append(idxs[atom_names.index('CA')]) + else: + token_idxs.append(idxs[0]) + single_atom_token += [False] + standard_token_idxs.append(current_standard_token_id) + current_standard_token_id += 1 + elif is_nucleic_backbone: + res_name = res_names[0] + if ( + flatten_non_standard_residues + and res_name not in residue_names.NUCLEIC_TYPES_WITH_2_UNKS + ): + # For non-standard nucleic residues take all atoms. + token_idxs.extend(idxs) + single_atom_token += [True] * len(idxs) + standard_token_idxs.extend( + [current_standard_token_id] * len(idxs)) + else: + # For standard nucleic residues take C1' if it exists, else first atom. + if "C1'" in atom_names: + token_idxs.append(idxs[atom_names.index("C1'")]) + else: + token_idxs.append(idxs[0]) + single_atom_token += [False] + standard_token_idxs.append(current_standard_token_id) + current_standard_token_id += 1 + elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: + # For non-polymers take all atoms + token_idxs.extend(idxs) + single_atom_token += [True] * len(idxs) + standard_token_idxs.extend([current_standard_token_id] * len(idxs)) + current_standard_token_id += len(idxs) + else: + # Chain type that we don't handle yet. + logging.warning( + '%s: ignoring chain %s with chain type %s.', + logging_name, + chain_id, + chain_type, + ) + + assert len(token_idxs) == len(single_atom_token) + assert len(token_idxs) == len(standard_token_idxs) + standard_token_idxs = np.array(standard_token_idxs, dtype=np.int32) + + # Create the list of all tokens, represented as a flat AtomLayout with 1 + # representative atom per token. + all_tokens = flat_output_layout[token_idxs] + + # Create the 2D atoms_per_token layout + num_tokens = all_tokens.shape[0] + + # Target lists. + target_atom_names = [] + target_atom_elements = [] + target_res_ids = [] + target_res_names = [] + target_chain_ids = [] + target_chain_types = [] + + # uids of all atoms in the flat layout, to check whether the dense atoms + # exist -- This is necessary for terminal atoms (e.g. 'OP3' or 'OXT') + all_atoms_uids = set( + zip( + flat_output_layout.chain_id, + flat_output_layout.res_id, + flat_output_layout.atom_name, + ) + ) + + for idx, single_atom in enumerate(single_atom_token): + if not single_atom: + # Standard protein and nucleic residues have many atoms per token + chain_id = all_tokens.chain_id[idx] + res_id = all_tokens.res_id[idx] + res_name = all_tokens.res_name[idx] + atom_names = [] + atom_elements = [] + + res_atoms = struc_chem_comps.get_all_atoms_in_entry( + ccd=ccd, res_name=res_name + ) + atom_names_elements = list( + zip( + res_atoms['_chem_comp_atom.atom_id'], + res_atoms['_chem_comp_atom.type_symbol'], + strict=True, + ) + ) + + for atom_name, atom_element in atom_names_elements: + # Remove hydrogens if they are not in flat layout. + if atom_element in ['H', 'D'] and ( + (chain_id, res_id, atom_name) not in all_atoms_uids + ): + continue + elif (chain_id, res_id, atom_name) in all_atoms_uids: + atom_names.append(atom_name) + atom_elements.append(atom_element) + # Leave spaces for OXT etc. + else: + atom_names.append('') + atom_elements.append('') + + if len(atom_names) > max_atoms_per_token: + logging.warning( + 'Atom list for chain %s ' + 'residue %s %s is too long and will be truncated: ' + '%s to the max atoms limit %s. Dropped atoms: %s', + chain_id, + res_id, + res_name, + len(atom_names), + max_atoms_per_token, + list( + zip( + atom_names[max_atoms_per_token:], + atom_elements[max_atoms_per_token:], + strict=True, + ) + ), + ) + atom_names = atom_names[:max_atoms_per_token] + atom_elements = atom_elements[:max_atoms_per_token] + + num_pad = max_atoms_per_token - len(atom_names) + atom_names.extend([''] * num_pad) + atom_elements.extend([''] * num_pad) + + else: + # ligands have only 1 atom per token + padding = [''] * (max_atoms_per_token - 1) + atom_names = [all_tokens.atom_name[idx]] + padding + atom_elements = [all_tokens.atom_element[idx]] + padding + + # Append the atoms to the target lists. + target_atom_names.append(atom_names) + target_atom_elements.append(atom_elements) + target_res_names.append( + [all_tokens.res_name[idx]] * max_atoms_per_token) + target_res_ids.append([all_tokens.res_id[idx]] * max_atoms_per_token) + target_chain_ids.append( + [all_tokens.chain_id[idx]] * max_atoms_per_token) + target_chain_types.append( + [all_tokens.chain_type[idx]] * max_atoms_per_token + ) + + # Make sure to get the right shape also for 0 tokens + trg_shape = (num_tokens, max_atoms_per_token) + all_token_atoms_layout = atom_layout.AtomLayout( + atom_name=np.array(target_atom_names, dtype=object).reshape(trg_shape), + atom_element=np.array(target_atom_elements, dtype=object).reshape( + trg_shape + ), + res_name=np.array(target_res_names, dtype=object).reshape(trg_shape), + res_id=np.array(target_res_ids, dtype=int).reshape(trg_shape), + chain_id=np.array(target_chain_ids, dtype=object).reshape(trg_shape), + chain_type=np.array(target_chain_types, + dtype=object).reshape(trg_shape), + ) + + return all_tokens, all_token_atoms_layout, standard_token_idxs + + +@dataclasses.dataclass +class MSA: + """Dataclass containing MSA.""" + + rows: xnp_ndarray + mask: xnp_ndarray + deletion_matrix: xnp_ndarray + # Occurrence of each residue type along the sequence, averaged over MSA rows. + profile: xnp_ndarray + # Occurrence of deletions along the sequence, averaged over MSA rows. + deletion_mean: xnp_ndarray + # Number of MSA alignments. + num_alignments: xnp_ndarray + + @classmethod + def compute_features( + cls, + *, + all_tokens: atom_layout.AtomLayout, + standard_token_idxs: np.ndarray, + padding_shapes: PaddingShapes, + fold_input: folding_input.Input, + logging_name: str, + max_paired_sequence_per_species: int, + ) -> Self: + """Compute the msa features.""" + seen_entities = {} + + substruct = atom_layout.make_structure( + flat_layout=all_tokens, + atom_coords=np.zeros(all_tokens.shape + (3,)), + name=logging_name, + ) + prot = substruct.filter_to_entity_type(protein=True) + num_unique_chains = len( + set(prot.chain_single_letter_sequence().values())) + need_msa_pairing = num_unique_chains > 1 + + np_chains_list = [] + input_chains_by_id = {chain.id: chain for chain in fold_input.chains} + nonempty_chain_ids = set(all_tokens.chain_id) + for asym_id, chain_info in enumerate(substruct.iter_chains(), start=1): + b_chain_id = chain_info['chain_id'] + chain_type = chain_info['chain_type'] + chain = input_chains_by_id[b_chain_id] + + # Generalised "sequence" for ligands (can't trust residue name) + chain_tokens = all_tokens[all_tokens.chain_id == b_chain_id] + assert chain_tokens.res_name is not None + three_letter_sequence = ','.join(chain_tokens.res_name.tolist()) + chain_num_tokens = len(chain_tokens.atom_name) + if chain_type in mmcif_names.POLYMER_CHAIN_TYPES: + sequence = substruct.chain_single_letter_sequence()[b_chain_id] + if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: + # Only allow nucleic residue types for nucleic chains (can have some + # protein residues in e.g. tRNA, but that causes MSA search failures). + # Replace non nucleic residue types by UNK_NUCLEIC. + nucleic_types_one_letter = ( + residue_names.DNA_TYPES_ONE_LETTER + + residue_names.RNA_TYPES_ONE_LETTER_WITH_UNKNOWN + ) + sequence = ''.join([ + base + if base in nucleic_types_one_letter + else residue_names.UNK_NUCLEIC_ONE_LETTER + for base in sequence + ]) + else: + sequence = 'X' * chain_num_tokens + + skip_chain = ( + chain_type not in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES + or len(sequence) <= 4 + or b_chain_id not in nonempty_chain_ids + ) + if three_letter_sequence in seen_entities: + entity_id = seen_entities[three_letter_sequence] + else: + entity_id = len(seen_entities) + 1 + + if chain_type in mmcif_names.STANDARD_POLYMER_CHAIN_TYPES: + unpaired_a3m = '' + paired_a3m = '' + if not skip_chain: + if need_msa_pairing and isinstance(chain, folding_input.ProteinChain): + paired_a3m = chain.paired_msa + if isinstance( + chain, folding_input.RnaChain | folding_input.ProteinChain + ): + unpaired_a3m = chain.unpaired_msa + unpaired_msa = msa_module.Msa.from_a3m( + query_sequence=sequence, + chain_poly_type=chain_type, + a3m=unpaired_a3m, + deduplicate=True, + ) + + paired_msa = msa_module.Msa.from_a3m( + query_sequence=sequence, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + a3m=paired_a3m, + deduplicate=False, + ) + else: + unpaired_msa = msa_module.Msa.from_empty( + query_sequence='-' * len(sequence), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + paired_msa = msa_module.Msa.from_empty( + query_sequence='-' * len(sequence), + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + ) + + msa_features = unpaired_msa.featurize() + all_seqs_msa_features = paired_msa.featurize() + + msa_features = data3.fix_features(msa_features) + all_seqs_msa_features = data3.fix_features(all_seqs_msa_features) + + msa_features = msa_features | { + f'{k}_all_seq': v for k, v in all_seqs_msa_features.items() + } + feats = msa_features + feats['chain_id'] = b_chain_id + feats['asym_id'] = np.full(chain_num_tokens, asym_id) + feats['entity_id'] = entity_id + np_chains_list.append(feats) + + # Add profile features to each chain. + for chain in np_chains_list: + chain.update( + data3.get_profile_features( + chain['msa'], chain['deletion_matrix']) + ) + + # Allow 50% of the MSA to come from MSA pairing. + max_paired_sequences = padding_shapes.msa_size // 2 + if need_msa_pairing: + np_chains_list = list(map(dict, np_chains_list)) + np_chains_list = msa_pairing.create_paired_features( + np_chains_list, + max_paired_sequences=max_paired_sequences, + nonempty_chain_ids=nonempty_chain_ids, + max_hits_per_species=max_paired_sequence_per_species, + ) + np_chains_list = msa_pairing.deduplicate_unpaired_sequences( + np_chains_list + ) + + # Remove all gapped rows from all seqs. + nonempty_asym_ids = [] + for chain in np_chains_list: + if chain['chain_id'] in nonempty_chain_ids: + nonempty_asym_ids.append(chain['asym_id'][0]) + if 'msa_all_seq' in np_chains_list[0]: + np_chains_list = msa_pairing.remove_all_gapped_rows_from_all_seqs( + np_chains_list, asym_ids=nonempty_asym_ids + ) + + # Crop MSA rows. + cropped_chains_list = [] + for chain in np_chains_list: + unpaired_msa_size, paired_msa_size = ( + msa_pairing.choose_paired_unpaired_msa_crop_sizes( + unpaired_msa=chain['msa'], + paired_msa=chain.get('msa_all_seq'), + total_msa_crop_size=padding_shapes.msa_size, + max_paired_sequences=max_paired_sequences, + ) + ) + cropped_chain = { + 'asym_id': chain['asym_id'], + 'chain_id': chain['chain_id'], + 'profile': chain['profile'], + 'deletion_mean': chain['deletion_mean'], + } + for feat in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: + if feat in chain: + cropped_chain[feat] = chain[feat][:unpaired_msa_size] + if feat + '_all_seq' in chain: + cropped_chain[feat + '_all_seq'] = chain[feat + '_all_seq'][ + :paired_msa_size + ] + cropped_chains_list.append(cropped_chain) + + # Merge Chains. + # Make sure the chain order is unaltered before slicing with tokens. + curr_chain_order = [chain['chain_id'] for chain in cropped_chains_list] + orig_chain_order = [chain['chain_id'] + for chain in substruct.iter_chains()] + assert curr_chain_order == orig_chain_order + np_example = { + 'asym_id': np.concatenate( + [c['asym_id'] for c in cropped_chains_list], axis=0 + ), + } + for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: + for feat in [feature, feature + '_all_seq']: + if feat in cropped_chains_list[0]: + np_example[feat] = merging_features.merge_msa_features( + feat, cropped_chains_list + ) + for feature in ['profile', 'deletion_mean']: + feature_list = [c[feature] for c in cropped_chains_list] + np_example[feature] = np.concatenate(feature_list, axis=0) + + # Crop MSA rows to maximum size given by chains participating in the crop. + max_allowed_unpaired = max([ + len(chain['msa']) + for chain in cropped_chains_list + if chain['asym_id'][0] in nonempty_asym_ids + ]) + np_example['msa'] = np_example['msa'][:max_allowed_unpaired] + if 'msa_all_seq' in np_example: + max_allowed_paired = max([ + len(chain['msa_all_seq']) + for chain in cropped_chains_list + if chain['asym_id'][0] in nonempty_asym_ids + ]) + np_example['msa_all_seq'] = np_example['msa_all_seq'][:max_allowed_paired] + + np_example = merging_features.merge_paired_and_unpaired_msa(np_example) + + # Crop MSA residues. Need to use the standard token indices, since msa does + # not expand non-standard residues. This means that for expanded residues, + # we get repeated msa columns. + new_cropping_idxs = standard_token_idxs + for feature in data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES: + if feature in np_example: + np_example[feature] = np_example[feature][:, + new_cropping_idxs].copy() + for feature in ['profile', 'deletion_mean']: + np_example[feature] = np_example[feature][new_cropping_idxs] + + # Make MSA mask. + np_example['msa_mask'] = np.ones_like( + np_example['msa'], dtype=np.float32) + + # Count MSA size before padding. + num_alignments = np_example['msa'].shape[0] + + # Pad: + msa_size, num_tokens = padding_shapes.msa_size, padding_shapes.num_tokens + + def safe_cast_int8(x): + return np.clip(x, np.iinfo(np.int8).min, np.iinfo(np.int8).max).astype( + np.int8 + ) + + return MSA( + rows=_pad_to(safe_cast_int8( + np_example['msa']), (msa_size, num_tokens)), + mask=_pad_to( + np_example['msa_mask'].astype(bool), (msa_size, num_tokens) + ), + # deletion_matrix may be out of int8 range, but we mostly care about + # small values since we arctan it in the model. + deletion_matrix=_pad_to( + safe_cast_int8(np_example['deletion_matrix']), + (msa_size, num_tokens), + ), + profile=_pad_to(np_example['profile'], (num_tokens, None)), + deletion_mean=_pad_to(np_example['deletion_mean'], (num_tokens,)), + num_alignments=np.array(num_alignments, dtype=np.int32), + ) + + def index_msa_rows(self, indices: xnp_ndarray) -> Self: + assert indices.ndim == 1 + + return MSA( + rows=self.rows[indices, :], + mask=self.mask[indices, :], + deletion_matrix=self.deletion_matrix[indices, :], + profile=self.profile, + deletion_mean=self.deletion_mean, + num_alignments=self.num_alignments, + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + output = cls( + rows=batch['msa'], + mask=batch['msa_mask'], + deletion_matrix=batch['deletion_matrix'], + profile=batch['profile'], + deletion_mean=batch['deletion_mean'], + num_alignments=batch['num_alignments'], + ) + return output + + def as_data_dict(self) -> BatchDict: + return { + 'msa': self.rows, + 'msa_mask': self.mask, + 'deletion_matrix': self.deletion_matrix, + 'profile': self.profile, + 'deletion_mean': self.deletion_mean, + 'num_alignments': self.num_alignments, + } + + +@dataclasses.dataclass +class Templates: + """Dataclass containing templates.""" + + # aatype of templates, int32 w shape [num_templates, num_res] + aatype: xnp_ndarray + # atom positions of templates, float32 w shape [num_templates, num_res, 24, 3] + atom_positions: xnp_ndarray + # atom mask of templates, bool w shape [num_templates, num_res, 24] + atom_mask: xnp_ndarray + def __getitem__(self, idx): + return Templates(self.aatype[idx], self.atom_positions[idx], self.atom_mask[idx]) + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + standard_token_idxs: np.ndarray, + padding_shapes: PaddingShapes, + fold_input: folding_input.Input, + max_templates: int, + logging_name: str, + ) -> Self: + """Compute the template features.""" + + seen_entities = {} + polymer_entity_features = {True: {}, False: {}} + + substruct = atom_layout.make_structure( + flat_layout=all_tokens, + atom_coords=np.zeros(all_tokens.shape + (3,)), + name=logging_name, + ) + np_chains_list = [] + + input_chains_by_id = {chain.id: chain for chain in fold_input.chains} + + nonempty_chain_ids = set(all_tokens.chain_id) + for chain_info in substruct.iter_chains(): + chain_id = chain_info['chain_id'] + chain_type = chain_info['chain_type'] + chain = input_chains_by_id[chain_id] + + # Generalised "sequence" for ligands (can't trust residue name) + chain_tokens = all_tokens[all_tokens.chain_id == chain_id] + assert chain_tokens.res_name is not None + three_letter_sequence = ','.join(chain_tokens.res_name.tolist()) + chain_num_tokens = len(chain_tokens.atom_name) + + # Don't compute features for chains not included in the crop, or ligands. + skip_chain = ( + chain_type != mmcif_names.PROTEIN_CHAIN + or chain_num_tokens <= 4 # not cache filled + or chain_id not in nonempty_chain_ids + ) + + if three_letter_sequence in seen_entities: + entity_id = seen_entities[three_letter_sequence] + else: + entity_id = len(seen_entities) + 1 + + if entity_id not in polymer_entity_features[skip_chain]: + if skip_chain: + template_features = data3.empty_template_features( + chain_num_tokens) + else: + assert isinstance(chain, folding_input.ProteinChain) + + sorted_features = [] + for template in chain.templates: + struct = structure.from_mmcif( + template.mmcif, + fix_mse_residues=True, + fix_arginines=True, + include_bonds=False, + include_water=False, + # For non-standard polymer chains. + include_other=True, + ) + hit_features = templates.get_polymer_features( + chain=struct, + chain_poly_type=mmcif_names.PROTEIN_CHAIN, + query_sequence_length=len(chain.sequence), + query_to_hit_mapping=dict( + template.query_to_template_map), + ) + sorted_features.append(hit_features) + + template_features = templates.package_template_features( + hit_features=sorted_features, + include_ligand_features=False, + ) + + template_features = data3.fix_template_features( + sequence=chain.sequence, + template_features=template_features, + ) + + template_features = _reduce_template_features( + template_features, max_templates + ) + polymer_entity_features[skip_chain][entity_id] = template_features + + seen_entities[three_letter_sequence] = entity_id + feats = polymer_entity_features[skip_chain][entity_id].copy() + feats['chain_id'] = chain_id + np_chains_list.append(feats) + + # We pad the num_templates dimension before merging, so that different + # chains can be concatenated on the num_res dimension. Masking will be + # applied so that each chains templates can't see each other. + for chain in np_chains_list: + chain['template_aatype'] = _pad_to( + chain['template_aatype'], (max_templates, None) + ) + chain['template_atom_positions'] = _pad_to( + chain['template_atom_positions'], ( + max_templates, None, None, None) + ) + chain['template_atom_mask'] = _pad_to( + chain['template_atom_mask'], (max_templates, None, None) + ) + + # Merge on token dimension. + np_example = { + ft: np.concatenate([c[ft] for c in np_chains_list], axis=1) + for ft in np_chains_list[0] + if ft in data_constants.TEMPLATE_FEATURES + } + + # Crop template data. Need to use the standard token indices, since msa does + # not expand non-standard residues. This means that for expanded residues, + # we get repeated template information. + for feature_name, v in np_example.items(): + np_example[feature_name] = v[:max_templates, + standard_token_idxs, ...] + + # Pad along the token dimension. + templates_features = Templates( + aatype=_pad_to( + np_example['template_aatype'], (None, + padding_shapes.num_tokens) + ), + atom_positions=_pad_to( + np_example['template_atom_positions'], + (None, padding_shapes.num_tokens, None, None), + ), + atom_mask=_pad_to( + np_example['template_atom_mask'].astype(bool), + (None, padding_shapes.num_tokens, None), + ), + ) + return templates_features + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + """Make Template from batch dictionary.""" + return cls( + aatype=batch['template_aatype'], + atom_positions=batch['template_atom_positions'], + atom_mask=batch['template_atom_mask'], + ) + + def as_data_dict(self) -> BatchDict: + return { + 'template_aatype': self.aatype, + 'template_atom_positions': self.atom_positions, + 'template_atom_mask': self.atom_mask, + } + + +def _reduce_template_features( + template_features: data3.FeatureDict, + max_templates: int, +) -> data3.FeatureDict: + """Reduces template features to max num templates and defined feature set.""" + num_templates = template_features['template_aatype'].shape[0] + template_keep_mask = np.arange(num_templates) < max_templates + template_fields = data_constants.TEMPLATE_FEATURES + ( + 'template_release_timestamp', + ) + template_features = { + k: v[template_keep_mask] + for k, v in template_features.items() + if k in template_fields + } + return template_features + + +@dataclasses.dataclass +class TokenFeatures: + """Dataclass containing features for tokens.""" + + residue_index: xnp_ndarray + token_index: xnp_ndarray + aatype: xnp_ndarray + mask: xnp_ndarray + seq_length: xnp_ndarray + + # Chain symmetry identifiers + # for an A3B2 stoichiometry the meaning of these features is as follows: + # asym_id: 1 2 3 4 5 + # entity_id: 1 1 1 2 2 + # sym_id: 1 2 3 1 2 + asym_id: xnp_ndarray + entity_id: xnp_ndarray + sym_id: xnp_ndarray + + # token type features + is_protein: xnp_ndarray + is_rna: xnp_ndarray + is_dna: xnp_ndarray + is_ligand: xnp_ndarray + is_nonstandard_polymer_chain: xnp_ndarray + is_water: xnp_ndarray + + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + padding_shapes: PaddingShapes, + ) -> Self: + """Compute the per-token features.""" + + residue_index = all_tokens.res_id.astype(np.int32) + + token_index = np.arange( + 1, len(all_tokens.atom_name) + 1).astype(np.int32) + + aatype = [] + for res_name, chain_type in zip(all_tokens.res_name, all_tokens.chain_type): + if chain_type in mmcif_names.POLYMER_CHAIN_TYPES: + res_name = mmcif_names.fix_non_standard_polymer_res( + res_name=res_name, chain_type=chain_type + ) + if ( + chain_type == mmcif_names.DNA_CHAIN + and res_name == residue_names.UNK_DNA + ): + res_name = residue_names.UNK_NUCLEIC_ONE_LETTER + elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: + res_name = residue_names.UNK + else: + raise ValueError( + f'Chain type {chain_type} not polymer or ligand.') + aa = residue_names.POLYMER_TYPES_ORDER_WITH_UNKNOWN_AND_GAP[res_name] + aatype.append(aa) + aatype = np.array(aatype, dtype=np.int32) + + mask = np.ones(all_tokens.shape[0], dtype=bool) + chains = _compute_asym_entity_and_sym_id(all_tokens) + m = dict(zip(chains.chain_id, chains.asym_id)) + asym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32) + + m = dict(zip(chains.chain_id, chains.entity_id)) + entity_id = np.array([m[c] + for c in all_tokens.chain_id], dtype=np.int32) + + m = dict(zip(chains.chain_id, chains.sym_id)) + sym_id = np.array([m[c] for c in all_tokens.chain_id], dtype=np.int32) + + seq_length = np.array(all_tokens.shape[0], dtype=np.int32) + + is_protein = all_tokens.chain_type == mmcif_names.PROTEIN_CHAIN + is_rna = all_tokens.chain_type == mmcif_names.RNA_CHAIN + is_dna = all_tokens.chain_type == mmcif_names.DNA_CHAIN + is_ligand = np.isin( + all_tokens.chain_type, list(mmcif_names.LIGAND_CHAIN_TYPES) + ) + standard_polymer_chain = list(mmcif_names.NON_POLYMER_CHAIN_TYPES) + list( + mmcif_names.STANDARD_POLYMER_CHAIN_TYPES + ) + is_nonstandard_polymer_chain = np.isin( + all_tokens.chain_type, standard_polymer_chain, invert=True + ) + is_water = all_tokens.chain_type == mmcif_names.WATER + + return TokenFeatures( + residue_index=_pad_to(residue_index, (padding_shapes.num_tokens,)), + token_index=_pad_to(token_index, (padding_shapes.num_tokens,)), + aatype=_pad_to(aatype, (padding_shapes.num_tokens,)), + mask=_pad_to(mask, (padding_shapes.num_tokens,)), + asym_id=_pad_to(asym_id, (padding_shapes.num_tokens,)), + entity_id=_pad_to(entity_id, (padding_shapes.num_tokens,)), + sym_id=_pad_to(sym_id, (padding_shapes.num_tokens,)), + seq_length=seq_length, + is_protein=_pad_to(is_protein, (padding_shapes.num_tokens,)), + is_rna=_pad_to(is_rna, (padding_shapes.num_tokens,)), + is_dna=_pad_to(is_dna, (padding_shapes.num_tokens,)), + is_ligand=_pad_to(is_ligand, (padding_shapes.num_tokens,)), + is_nonstandard_polymer_chain=_pad_to( + is_nonstandard_polymer_chain, (padding_shapes.num_tokens,) + ), + is_water=_pad_to(is_water, (padding_shapes.num_tokens,)), + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + residue_index=batch['residue_index'], + token_index=batch['token_index'], + aatype=batch['aatype'], + mask=batch['seq_mask'], + entity_id=batch['entity_id'], + asym_id=batch['asym_id'], + sym_id=batch['sym_id'], + seq_length=batch['seq_length'], + is_protein=batch['is_protein'], + is_rna=batch['is_rna'], + is_dna=batch['is_dna'], + is_ligand=batch['is_ligand'], + is_nonstandard_polymer_chain=batch['is_nonstandard_polymer_chain'], + is_water=batch['is_water'], + ) + + def as_data_dict(self) -> BatchDict: + return { + 'residue_index': self.residue_index, + 'token_index': self.token_index, + 'aatype': self.aatype, + 'seq_mask': self.mask, + 'entity_id': self.entity_id, + 'asym_id': self.asym_id, + 'sym_id': self.sym_id, + 'seq_length': self.seq_length, + 'is_protein': self.is_protein, + 'is_rna': self.is_rna, + 'is_dna': self.is_dna, + 'is_ligand': self.is_ligand, + 'is_nonstandard_polymer_chain': self.is_nonstandard_polymer_chain, + 'is_water': self.is_water, + } + + +@dataclasses.dataclass +class PredictedStructureInfo: + """Contains information necessary to work with predicted structure.""" + + atom_mask: xnp_ndarray + residue_center_index: xnp_ndarray + + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + all_token_atoms_layout: atom_layout.AtomLayout, + padding_shapes: PaddingShapes, + ) -> Self: + """Compute the PredictedStructureInfo features. + + Args: + all_tokens: flat AtomLayout with 1 representative atom per token, shape + (num_tokens,) + all_token_atoms_layout: AtomLayout for all atoms per token, shape + (num_tokens, max_atoms_per_token) + padding_shapes: padding shapes. + + Returns: + A PredictedStructureInfo object. + """ + atom_mask = _pad_to( + all_token_atoms_layout.atom_name.astype(bool), + (padding_shapes.num_tokens, None), + ) + residue_center_index = np.zeros( + padding_shapes.num_tokens, dtype=np.int32) + for idx in range(all_tokens.shape[0]): + repr_atom = all_tokens.atom_name[idx] + atoms = list(all_token_atoms_layout.atom_name[idx, :]) + if repr_atom in atoms: + residue_center_index[idx] = atoms.index(repr_atom) + else: + # Representative atoms can be missing if cropping the number of atoms + # per residue. + logging.warning( + 'The representative atom in all_tokens (%s) is not in ' + 'all_token_atoms_layout (%s)', + all_tokens[idx: idx + 1], + all_token_atoms_layout[idx, :], + ) + residue_center_index[idx] = 0 + return cls(atom_mask=atom_mask, residue_center_index=residue_center_index) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + atom_mask=batch['pred_dense_atom_mask'], + residue_center_index=batch['residue_center_index'], + ) + + def as_data_dict(self) -> BatchDict: + return { + 'pred_dense_atom_mask': self.atom_mask, + 'residue_center_index': self.residue_center_index, + } + + +@dataclasses.dataclass +class PolymerLigandBondInfo: + """Contains information about polymer-ligand bonds.""" + + tokens_to_polymer_ligand_bonds: atom_layout.GatherInfo + # Gather indices to convert from cropped dense atom layout to bonds layout + # (num_tokens, 2) + token_atoms_to_bonds: atom_layout.GatherInfo + + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + all_token_atoms_layout: atom_layout.AtomLayout, + bond_layout: atom_layout.AtomLayout | None, + padding_shapes: PaddingShapes, + ) -> Self: + """Computes the InterChainBondInfo features. + + Args: + all_tokens: AtomLayout for tokens; shape (num_tokens,). + all_token_atoms_layout: Atom Layout for all atoms (num_tokens, + max_atoms_per_token) + bond_layout: Bond layout for polymer-ligand bonds. + padding_shapes: Padding shapes. + + Returns: + A PolymerLigandBondInfo object. + """ + + if bond_layout is not None: + # Must convert to list before calling np.isin, will not work raw. + peptide_types = list(mmcif_names.PEPTIDE_CHAIN_TYPES) + nucleic_types = list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES) + [ + mmcif_names.OTHER_CHAIN + ] + # These atom renames are so that we can use the atom layout code with + # all_tokens, which only has a single atom per token. + atom_names = bond_layout.atom_name.copy() + atom_names[np.isin(bond_layout.chain_type, peptide_types)] = 'CA' + atom_names[np.isin(bond_layout.chain_type, nucleic_types)] = "C1'" + adjusted_bond_layout = atom_layout.AtomLayout( + atom_name=atom_names, + res_id=bond_layout.res_id, + chain_id=bond_layout.chain_id, + chain_type=bond_layout.chain_type, + ) + # Remove bonds that are not in the crop. + cropped_tokens_to_bonds = atom_layout.compute_gather_idxs( + source_layout=all_tokens, target_layout=adjusted_bond_layout + ) + bond_is_in_crop = np.all( + cropped_tokens_to_bonds.gather_mask, axis=1 + ).astype(bool) + adjusted_bond_layout = adjusted_bond_layout[bond_is_in_crop, :] + else: + # Create layout with correct shape when bond_layout is None. + s = (0, 2) + adjusted_bond_layout = atom_layout.AtomLayout( + atom_name=np.array([], dtype=object).reshape(s), + res_id=np.array([], dtype=int).reshape(s), + chain_id=np.array([], dtype=object).reshape(s), + ) + adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to( + (padding_shapes.num_tokens, 2) + ) + tokens_to_polymer_ligand_bonds = atom_layout.compute_gather_idxs( + source_layout=all_tokens, target_layout=adjusted_bond_layout + ) + + # Stuff for computing the bond loss. + if bond_layout is not None: + # Pad to num_tokens (hoping that there are never more bonds than tokens). + padded_bond_layout = bond_layout.copy_and_pad_to( + (padding_shapes.num_tokens, 2) + ) + token_atoms_to_bonds = atom_layout.compute_gather_idxs( + source_layout=all_token_atoms_layout, target_layout=padded_bond_layout + ) + else: + token_atoms_to_bonds = atom_layout.GatherInfo( + gather_idxs=np.zeros( + (padding_shapes.num_tokens, 2), dtype=int), + gather_mask=np.zeros( + (padding_shapes.num_tokens, 2), dtype=bool), + input_shape=np.array(( + padding_shapes.num_tokens, + all_token_atoms_layout.shape[1], + )), + ) + + return cls( + tokens_to_polymer_ligand_bonds=tokens_to_polymer_ligand_bonds, + token_atoms_to_bonds=token_atoms_to_bonds, + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + tokens_to_polymer_ligand_bonds=atom_layout.GatherInfo.from_dict( + batch, key_prefix='tokens_to_polymer_ligand_bonds' + ), + token_atoms_to_bonds=atom_layout.GatherInfo.from_dict( + batch, key_prefix='token_atoms_to_polymer_ligand_bonds' + ), + ) + + def as_data_dict(self) -> BatchDict: + return { + **self.tokens_to_polymer_ligand_bonds.as_dict( + key_prefix='tokens_to_polymer_ligand_bonds' + ), + **self.token_atoms_to_bonds.as_dict( + key_prefix='token_atoms_to_polymer_ligand_bonds' + ), + } + + +@dataclasses.dataclass +class LigandLigandBondInfo: + """Contains information about the location of ligand-ligand bonds.""" + + tokens_to_ligand_ligand_bonds: atom_layout.GatherInfo + + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + bond_layout: atom_layout.AtomLayout | None, + padding_shapes: PaddingShapes, + ) -> Self: + """Computes the InterChainBondInfo features. + + Args: + all_tokens: AtomLayout for tokens; shape (num_tokens,). + bond_layout: Bond layout for ligand-ligand bonds. + padding_shapes: Padding shapes. + + Returns: + A LigandLigandBondInfo object. + """ + + if bond_layout is not None: + # Discard any bonds that do not join to an existing atom. + keep_mask = [] + all_atom_ids = { + uid + for uid in zip( + all_tokens.chain_id, + all_tokens.res_id, + all_tokens.atom_name, + strict=True, + ) + } + for chain_id, res_id, atom_name in zip( + bond_layout.chain_id, + bond_layout.res_id, + bond_layout.atom_name, + strict=True, + ): + atom_a = (chain_id[0], res_id[0], atom_name[0]) + atom_b = (chain_id[1], res_id[1], atom_name[1]) + if atom_a in all_atom_ids and atom_b in all_atom_ids: + keep_mask.append(True) + else: + keep_mask.append(False) + keep_mask = np.array(keep_mask).astype(bool) + bond_layout = bond_layout[keep_mask] + # Remove any bonds to Hydrogen atoms. + bond_layout = bond_layout[ + ~np.char.startswith(bond_layout.atom_name.astype(str), 'H').any( + axis=1 + ) + ] + atom_names = bond_layout.atom_name + adjusted_bond_layout = atom_layout.AtomLayout( + atom_name=atom_names, + res_id=bond_layout.res_id, + chain_id=bond_layout.chain_id, + chain_type=bond_layout.chain_type, + ) + else: + # Create layout with correct shape when bond_layout is None. + s = (0, 2) + adjusted_bond_layout = atom_layout.AtomLayout( + atom_name=np.array([], dtype=object).reshape(s), + res_id=np.array([], dtype=int).reshape(s), + chain_id=np.array([], dtype=object).reshape(s), + ) + # 10 x num_tokens as max_inter_bonds_ratio + max_intra_bonds_ration = 2.061. + adjusted_bond_layout = adjusted_bond_layout.copy_and_pad_to( + (padding_shapes.num_tokens * 10, 2) + ) + gather_idx = atom_layout.compute_gather_idxs( + source_layout=all_tokens, target_layout=adjusted_bond_layout + ) + return cls(tokens_to_ligand_ligand_bonds=gather_idx) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + tokens_to_ligand_ligand_bonds=atom_layout.GatherInfo.from_dict( + batch, key_prefix='tokens_to_ligand_ligand_bonds' + ) + ) + + def as_data_dict(self) -> BatchDict: + return { + **self.tokens_to_ligand_ligand_bonds.as_dict( + key_prefix='tokens_to_ligand_ligand_bonds' + ) + } + + +@dataclasses.dataclass +class PseudoBetaInfo: + """Contains information for extracting pseudo-beta and equivalent atoms.""" + + token_atoms_to_pseudo_beta: atom_layout.GatherInfo + + @classmethod + def compute_features( + cls, + all_token_atoms_layout: atom_layout.AtomLayout, + ccd: chemical_components.Ccd, + padding_shapes: PaddingShapes, + logging_name: str, + ) -> Self: + """Compute the PseudoBetaInfo features. + + Args: + all_token_atoms_layout: AtomLayout for all atoms per token, shape + (num_tokens, max_atoms_per_token) + ccd: The chemical components dictionary. + padding_shapes: padding shapes. + logging_name: logging name for debugging (usually the mmcif_id) + + Returns: + A PseudoBetaInfo object. + """ + token_idxs = [] + atom_idxs = [] + for token_idx in range(all_token_atoms_layout.shape[0]): + chain_type = all_token_atoms_layout.chain_type[token_idx, 0] + atom_names = list(all_token_atoms_layout.atom_name[token_idx, :]) + atom_idx = None + is_nucleic_backbone = ( + chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES + or chain_type == mmcif_names.OTHER_CHAIN + ) + if chain_type == mmcif_names.PROTEIN_CHAIN: + # Protein chains + if 'CB' in atom_names: + atom_idx = atom_names.index('CB') + elif 'CA' in atom_names: + atom_idx = atom_names.index('CA') + elif is_nucleic_backbone: + # RNA / DNA chains + res_name = all_token_atoms_layout.res_name[token_idx, 0] + cifdict = ccd.get(res_name) + if cifdict: + parent = cifdict['_chem_comp.mon_nstd_parent_comp_id'][0] + if parent != '?': + res_name = parent + if res_name in {'A', 'G', 'DA', 'DG'}: + if 'C4' in atom_names: + atom_idx = atom_names.index('C4') + else: + if 'C2' in atom_names: + atom_idx = atom_names.index('C2') + elif chain_type in mmcif_names.NON_POLYMER_CHAIN_TYPES: + # Ligands: there is only one atom per token + atom_idx = 0 + else: + logging.warning( + '%s: Unknown chain type for token %i. (%s)', + logging_name, + token_idx, + all_token_atoms_layout[token_idx: token_idx + 1], + ) + atom_idx = 0 + if atom_idx is None: + (valid_atom_idxs,) = np.nonzero( + all_token_atoms_layout.atom_name[token_idx, :] + ) + if valid_atom_idxs.shape[0] > 0: + atom_idx = valid_atom_idxs[0] + else: + atom_idx = 0 + logging.warning( + '%s token %i (%s), does not contain a pseudo-beta atom.' + 'Using first valid atom (%s) instead.', + logging_name, + token_idx, + all_token_atoms_layout[token_idx: token_idx + 1], + all_token_atoms_layout.atom_name[token_idx, atom_idx], + ) + + token_idxs.append(token_idx) + atom_idxs.append(atom_idx) + + pseudo_beta_layout = all_token_atoms_layout[token_idxs, atom_idxs] + pseudo_beta_layout = pseudo_beta_layout.copy_and_pad_to(( + padding_shapes.num_tokens, + )) + token_atoms_to_pseudo_beta = atom_layout.compute_gather_idxs( + source_layout=all_token_atoms_layout, target_layout=pseudo_beta_layout + ) + + return cls( + token_atoms_to_pseudo_beta=token_atoms_to_pseudo_beta, + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + token_atoms_to_pseudo_beta=atom_layout.GatherInfo.from_dict( + batch, key_prefix='token_atoms_to_pseudo_beta' + ), + ) + + def as_data_dict(self) -> BatchDict: + return { + **self.token_atoms_to_pseudo_beta.as_dict( + key_prefix='token_atoms_to_pseudo_beta' + ), + } + + +_DEFAULT_BLANK_REF = { + 'positions': np.zeros(3), + 'mask': 0, + 'element': 0, + 'charge': 0, + 'atom_name_chars': np.zeros(4), +} + + +def random_rotation(random_state: np.random.RandomState) -> np.ndarray: + # Create a random rotation (Gram-Schmidt orthogonalization of two + # random normal vectors) + v0, v1 = random_state.normal(size=(2, 3)) + e0 = v0 / np.maximum(1e-10, np.linalg.norm(v0)) + v1 = v1 - e0 * np.dot(v1, e0) + e1 = v1 / np.maximum(1e-10, np.linalg.norm(v1)) + e2 = np.cross(e0, e1) + return np.stack([e0, e1, e2]) + + +def random_augmentation( + positions: np.ndarray, + random_state: np.random.RandomState, +) -> np.ndarray: + """Center then apply random translation and rotation.""" + + center = np.mean(positions, axis=0) + rot = random_rotation(random_state) + positions_target = np.einsum('ij,kj->ki', rot, positions - center) + + translation = random_state.normal(size=(3,)) + positions_target = positions_target + translation + return positions_target + + +def get_reference( + res_name: str, + chemical_components_data: struc_chem_comps.ChemicalComponentsData, + ccd: chemical_components.Ccd, + random_state: np.random.RandomState, + ref_max_modified_date: datetime.date, + intra_ligand_ptm_bonds: bool, +) -> tuple[dict[str, Any], Any, Any]: + """Reference structure for residue from CCD or SMILES. + + Args: + res_name: ccd code of the residue. + chemical_components_data: ChemicalComponentsData for making ref structure. + ccd: The chemical components dictionary. + random_state: Numpy RandomState + ref_max_modified_date: date beyond which reference structures must not be + modefied. + intra_ligand_ptm_bonds: Whether to return intra ligand/ ptm bonds. + + Returns: + Mapping from atom names to features, from_atoms, dest_atoms. + """ + ccd_cif = ccd.get(res_name) + non_ccd_with_smiles = False + if not ccd_cif: + # If res name is non-CCD try to get SMILES from chem comp dict. + has_smiles = ( + chemical_components_data.chem_comp + and res_name in chemical_components_data.chem_comp + and chemical_components_data.chem_comp[res_name].pdbx_smiles + ) + if has_smiles: + non_ccd_with_smiles = True + else: + # If no SMILES or CCD, return empty dictionary. + return dict(), None, None + + pos = [] + elements = [] + charges = [] + atom_names = [] + + mol_from_smiles = None # useless init to make pylint happy + if non_ccd_with_smiles: + smiles_string = chemical_components_data.chem_comp[res_name].pdbx_smiles + mol_from_smiles = Chem.MolFromSmiles(smiles_string) + if mol_from_smiles is None: + logging.warning( + 'Fail to construct RDKit Mol from the SMILES string: %s', + smiles_string, + ) + return dict(), None, None + # Note this does not contain ideal coordinates, just bonds. + ccd_cif = rdkit_utils.mol_to_ccd_cif( + mol_from_smiles, component_id='fake_cif' + ) + + # RDKit for non-CCD structure and if ref should be a random RDKit conformer. + try: + if non_ccd_with_smiles: + m = mol_from_smiles + m = Chem.AddHs(m) + m = rdkit_utils.assign_atom_names_from_graph( + m, keep_existing_names=True) + logging.info( + 'Success constructing SMILES reference structure for: %s', res_name + ) + else: + m = rdkit_utils.mol_from_ccd_cif(ccd_cif, remove_hydrogens=False) + # Stochastic conformer search method. + # V3 is the latest and supports macrocycles . + params = AllChem.ETKDGv3() + params.randomSeed = int(random_state.randint(1, 1 << 31)) + AllChem.EmbedMolecule(m, params) + conformer = m.GetConformer() + for i, atom in enumerate(m.GetAtoms()): + elements.append(atom.GetAtomicNum()) + charges.append(atom.GetFormalCharge()) + name = atom.GetProp('atom_name') + atom_names.append(name) + coords = conformer.GetAtomPosition(i) + pos.append([coords.x, coords.y, coords.z]) + pos = np.array(pos, dtype=np.float32) + except (rdkit_utils.MolFromMmcifError, ValueError): + logging.warning( + 'Failed to construct RDKit reference structure for: %s', res_name + ) + + if not atom_names: + # Get CCD ideal coordinates if RDKit fails. + atom_names = ccd_cif['_chem_comp_atom.atom_id'] + # If mol_from_smiles then it won't have ideal coordinates by default. + if '_chem_comp_atom.pdbx_model_Cartn_x_ideal' in ccd_cif: + atom_x = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_x_ideal'] + atom_y = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_y_ideal'] + atom_z = ccd_cif['_chem_comp_atom.pdbx_model_Cartn_z_ideal'] + else: + atom_x = np.array(['?'] * len(atom_names)) + atom_y = np.array(['?'] * len(atom_names)) + atom_z = np.array(['?'] * len(atom_names)) + type_symbols = ccd_cif['_chem_comp_atom.type_symbol'] + charges = ccd_cif['_chem_comp_atom.charge'] + elements = [ + periodic_table.ATOMIC_NUMBER.get(elem_type.capitalize(), 0) + for elem_type in type_symbols + ] + pos = np.array([[x, y, z] for x, y, z in zip(atom_x, atom_y, atom_z)]) + # Unknown reference coordinates are specified by '?' in chem comp dict. + # Replace unknown reference coords with 0. + if '?' in pos and '_chem_comp.pdbx_modified_date' in ccd_cif: + # Use reference coordinates if modified date is before cutoff. + modified_dates = [ + datetime.date.fromisoformat(date) + for date in ccd_cif['_chem_comp.pdbx_modified_date'] + ] + max_modified_date = max(modified_dates) + if max_modified_date < ref_max_modified_date: + atom_x = ccd_cif['_chem_comp_atom.model_Cartn_x'] + atom_y = ccd_cif['_chem_comp_atom.model_Cartn_y'] + atom_z = ccd_cif['_chem_comp_atom.model_Cartn_z'] + pos = np.array([[x, y, z] + for x, y, z in zip(atom_x, atom_y, atom_z)]) + if '?' in pos: + if np.all(pos == '?'): + logging.warning('All ref positions unknown for: %s', res_name) + else: + logging.warning('Some ref positions unknown for: %s', res_name) + pos[pos == '?'] = 0 + pos = np.array(pos, dtype=np.float32) + + pos = random_augmentation(pos, random_state) + + if intra_ligand_ptm_bonds: + assert ccd_cif is not None, 'CCD CIF is None' + from_atom = ccd_cif.get('_chem_comp_bond.atom_id_1', None) + dest_atom = ccd_cif.get('_chem_comp_bond.atom_id_2', None) + else: + from_atom = None + dest_atom = None + + features = {} + for atom_name in atom_names: + features[atom_name] = {} + idx = atom_names.index(atom_name) + charge = 0 if charges[idx] == '?' else int(charges[idx]) + atom_name_chars = np.array([ord(c) - 32 for c in atom_name], dtype=int) + atom_name_chars = _pad_to(atom_name_chars, (4,)) + features[atom_name]['positions'] = pos[idx] + features[atom_name]['mask'] = 1 + features[atom_name]['element'] = elements[idx] + features[atom_name]['charge'] = charge + features[atom_name]['atom_name_chars'] = atom_name_chars + return features, from_atom, dest_atom + + +@dataclasses.dataclass +class RefStructure: + """Contains ref structure information.""" + + # Array with positions, float32, shape [num_res, max_atoms_per_token, 3] + positions: xnp_ndarray + # Array with masks, bool, shape [num_res, max_atoms_per_token] + mask: xnp_ndarray + # Array with elements, int32, shape [num_res, max_atoms_per_token] + element: xnp_ndarray + # Array with charges, float32, shape [num_res, max_atoms_per_token] + charge: xnp_ndarray + # Array with atom name characters, int32, [num_res, max_atoms_per_token, 4] + atom_name_chars: xnp_ndarray + # Array with reference space uids, int32, [num_res, max_atoms_per_token] + ref_space_uid: xnp_ndarray + + @classmethod + def compute_features( + cls, + all_token_atoms_layout: atom_layout.AtomLayout, + ccd: chemical_components.Ccd, + padding_shapes: PaddingShapes, + chemical_components_data: struc_chem_comps.ChemicalComponentsData, + random_state: np.random.RandomState, + ref_max_modified_date: datetime.date, + intra_ligand_ptm_bonds: bool, + ligand_ligand_bonds: atom_layout.AtomLayout | None = None, + ) -> tuple[Self, Any]: + """Reference structure information for each residue.""" + + # Get features per atom + padded_shape = (padding_shapes.num_tokens, + all_token_atoms_layout.shape[1]) + result = { + 'positions': np.zeros((*padded_shape, 3), 'float32'), + 'mask': np.zeros(padded_shape, 'bool'), + 'element': np.zeros(padded_shape, 'int32'), + 'charge': np.zeros(padded_shape, 'float32'), + 'atom_name_chars': np.zeros((*padded_shape, 4), 'int32'), + 'ref_space_uid': np.zeros((*padded_shape,), 'int32'), + } + + atom_names_all = [] + chain_ids_all = [] + res_ids_all = [] + + # Cache reference conformations for each residue. + conformations = {} + ref_space_uids = {} + for idx in np.ndindex(all_token_atoms_layout.shape): + chain_id = all_token_atoms_layout.chain_id[idx] + res_id = all_token_atoms_layout.res_id[idx] + res_name = all_token_atoms_layout.res_name[idx] + is_non_standard = res_name not in _STANDARD_RESIDUES + atom_name = all_token_atoms_layout.atom_name[idx] + if not atom_name: + ref = _DEFAULT_BLANK_REF + else: + if (chain_id, res_id) not in conformations: + conf, from_atom, dest_atom = get_reference( + res_name=res_name, + chemical_components_data=chemical_components_data, + ccd=ccd, + random_state=random_state, + ref_max_modified_date=ref_max_modified_date, + intra_ligand_ptm_bonds=intra_ligand_ptm_bonds, + ) + conformations[(chain_id, res_id)] = conf + + if ( + is_non_standard + and (from_atom is not None) + and (dest_atom is not None) + ): + # Add intra-ligand bond graph + atom_names_ligand = np.stack( + [from_atom, dest_atom], axis=1, dtype=object + ) + atom_names_all.append(atom_names_ligand) + res_ids_all.append( + np.full_like(atom_names_ligand, res_id, dtype=int) + ) + chain_ids_all.append( + np.full_like(atom_names_ligand, + chain_id, dtype=object) + ) + + conformation = conformations.get( + (chain_id, res_id), {atom_name: _DEFAULT_BLANK_REF} + ) + if atom_name not in conformation: + logging.warning( + 'Missing atom "%s" for CCD "%s"', + atom_name, + all_token_atoms_layout.res_name[idx], + ) + ref = conformation.get(atom_name, _DEFAULT_BLANK_REF) + for k in ref: + result[k][idx] = ref[k] + + # Assign a unique reference space id to each component, to determine which + # reference positions live in the same reference space. + space_str_id = ( + all_token_atoms_layout.chain_id[idx], + all_token_atoms_layout.res_id[idx], + ) + if space_str_id not in ref_space_uids: + ref_space_uids[space_str_id] = len(ref_space_uids) + result['ref_space_uid'][idx] = ref_space_uids[space_str_id] + + if atom_names_all: + atom_names_all = np.concatenate(atom_names_all, axis=0) + res_ids_all = np.concatenate(res_ids_all, axis=0) + chain_ids_all = np.concatenate(chain_ids_all, axis=0) + if ligand_ligand_bonds is not None: + adjusted_ligand_ligand_bonds = atom_layout.AtomLayout( + atom_name=np.concatenate( + [ligand_ligand_bonds.atom_name, atom_names_all], axis=0 + ), + chain_id=np.concatenate( + [ligand_ligand_bonds.chain_id, chain_ids_all], axis=0 + ), + res_id=np.concatenate( + [ligand_ligand_bonds.res_id, res_ids_all], axis=0 + ), + ) + else: + adjusted_ligand_ligand_bonds = atom_layout.AtomLayout( + atom_name=atom_names_all, + chain_id=chain_ids_all, + res_id=res_ids_all, + ) + else: + adjusted_ligand_ligand_bonds = ligand_ligand_bonds + + return cls(**result), adjusted_ligand_ligand_bonds + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + positions=batch['ref_pos'], + mask=batch['ref_mask'], + element=batch['ref_element'], + charge=batch['ref_charge'], + atom_name_chars=batch['ref_atom_name_chars'], + ref_space_uid=batch['ref_space_uid'], + ) + + def as_data_dict(self) -> BatchDict: + return { + 'ref_pos': self.positions, + 'ref_mask': self.mask, + 'ref_element': self.element, + 'ref_charge': self.charge, + 'ref_atom_name_chars': self.atom_name_chars, + 'ref_space_uid': self.ref_space_uid, + } + + +@dataclasses.dataclass +class ConvertModelOutput: + """Contains atom layout info.""" + + cleaned_struc: structure.Structure + token_atoms_layout: atom_layout.AtomLayout + flat_output_layout: atom_layout.AtomLayout + empty_output_struc: structure.Structure + polymer_ligand_bonds: atom_layout.AtomLayout + ligand_ligand_bonds: atom_layout.AtomLayout + + @classmethod + def compute_features( + cls, + all_token_atoms_layout: atom_layout.AtomLayout, + padding_shapes: PaddingShapes, + cleaned_struc: structure.Structure, + flat_output_layout: atom_layout.AtomLayout, + empty_output_struc: structure.Structure, + polymer_ligand_bonds: atom_layout.AtomLayout, + ligand_ligand_bonds: atom_layout.AtomLayout, + ) -> Self: + """Pads the all_token_atoms_layout and stores other data.""" + # Crop and pad the all_token_atoms_layout. + token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( + (padding_shapes.num_tokens, all_token_atoms_layout.shape[1]) + ) + + return cls( + cleaned_struc=cleaned_struc, + token_atoms_layout=token_atoms_layout, + flat_output_layout=flat_output_layout, + empty_output_struc=empty_output_struc, + polymer_ligand_bonds=polymer_ligand_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + """Construct atom layout object from dictionary.""" + + return cls( + cleaned_struc=_unwrap(batch.get('cleaned_struc', None)), + token_atoms_layout=_unwrap(batch.get('token_atoms_layout', None)), + flat_output_layout=_unwrap(batch.get('flat_output_layout', None)), + empty_output_struc=_unwrap(batch.get('empty_output_struc', None)), + polymer_ligand_bonds=_unwrap( + batch.get('polymer_ligand_bonds', None)), + ligand_ligand_bonds=_unwrap( + batch.get('ligand_ligand_bonds', None)), + ) + + def as_data_dict(self) -> BatchDict: + return { + 'cleaned_struc': np.array(self.cleaned_struc, object), + 'token_atoms_layout': np.array(self.token_atoms_layout, object), + 'flat_output_layout': np.array(self.flat_output_layout, object), + 'empty_output_struc': np.array(self.empty_output_struc, object), + 'polymer_ligand_bonds': np.array(self.polymer_ligand_bonds, object), + 'ligand_ligand_bonds': np.array(self.ligand_ligand_bonds, object), + } + + +@dataclasses.dataclass +class AtomCrossAtt: + """Operate on flat atoms.""" + + token_atoms_to_queries: atom_layout.GatherInfo + tokens_to_queries: atom_layout.GatherInfo + tokens_to_keys: atom_layout.GatherInfo + queries_to_keys: atom_layout.GatherInfo + queries_to_token_atoms: atom_layout.GatherInfo + + @classmethod + def compute_features( + cls, + # (num_tokens, num_dense) + all_token_atoms_layout: atom_layout.AtomLayout, + queries_subset_size: int, + keys_subset_size: int, + padding_shapes: PaddingShapes, + ) -> Self: + """Computes gather indices and meta data to work with a flat atom list.""" + + token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( + (padding_shapes.num_tokens, all_token_atoms_layout.shape[1]) + ) + token_atoms_mask = token_atoms_layout.atom_name.astype(bool) + flat_layout = token_atoms_layout[token_atoms_mask] + num_atoms = flat_layout.shape[0] + + padded_flat_layout = flat_layout.copy_and_pad_to(( + padding_shapes.num_atoms, + )) + + # Create the layout for queries + num_subsets = padding_shapes.num_atoms // queries_subset_size + lay_arr = padded_flat_layout.to_array() + queries_layout = atom_layout.AtomLayout.from_array( + lay_arr.reshape((6, num_subsets, queries_subset_size)) + ) + + # Create the layout for the keys (the key subsets are centered around the + # query subsets) + # Create initial gather indices (contain out-of-bound indices) + subset_centers = np.arange( + queries_subset_size / 2, padding_shapes.num_atoms, queries_subset_size + ) + flat_to_key_gathers = ( + subset_centers[:, None] + + np.arange(-keys_subset_size / 2, keys_subset_size / 2)[None, :] + ) + flat_to_key_gathers = flat_to_key_gathers.astype(int) + # Shift subsets with out-of-bound indices, such that they are fully within + # the bounds. + for row in range(flat_to_key_gathers.shape[0]): + if flat_to_key_gathers[row, 0] < 0: + flat_to_key_gathers[row, :] -= flat_to_key_gathers[row, 0] + elif flat_to_key_gathers[row, -1] > num_atoms - 1: + overflow = flat_to_key_gathers[row, -1] - (num_atoms - 1) + flat_to_key_gathers[row, :] -= overflow + # Create the keys layout. + keys_layout = padded_flat_layout[flat_to_key_gathers] + + # Create gather indices for conversion between token atoms layout, + # queries layout and keys layout. + token_atoms_to_queries = atom_layout.compute_gather_idxs( + source_layout=token_atoms_layout, target_layout=queries_layout + ) + + token_atoms_to_keys = atom_layout.compute_gather_idxs( + source_layout=token_atoms_layout, target_layout=keys_layout + ) + + queries_to_keys = atom_layout.compute_gather_idxs( + source_layout=queries_layout, target_layout=keys_layout + ) + + queries_to_token_atoms = atom_layout.compute_gather_idxs( + source_layout=queries_layout, target_layout=token_atoms_layout + ) + + # Create gather indices for conversion of tokens layout to + # queries and keys layout + token_idxs = np.arange(padding_shapes.num_tokens).astype(np.int64) + token_idxs = np.broadcast_to( + token_idxs[:, None], token_atoms_layout.shape) + tokens_to_queries = atom_layout.GatherInfo( + gather_idxs=atom_layout.convert( + token_atoms_to_queries, token_idxs, layout_axes=(0, 1) + ), + gather_mask=atom_layout.convert( + token_atoms_to_queries, token_atoms_mask, layout_axes=(0, 1) + ), + input_shape=np.array((padding_shapes.num_tokens,)), + ) + + tokens_to_keys = atom_layout.GatherInfo( + gather_idxs=atom_layout.convert( + token_atoms_to_keys, token_idxs, layout_axes=(0, 1) + ), + gather_mask=atom_layout.convert( + token_atoms_to_keys, token_atoms_mask, layout_axes=(0, 1) + ), + input_shape=np.array((padding_shapes.num_tokens,)), + ) + + return cls( + token_atoms_to_queries=token_atoms_to_queries, + tokens_to_queries=tokens_to_queries, + tokens_to_keys=tokens_to_keys, + queries_to_keys=queries_to_keys, + queries_to_token_atoms=queries_to_token_atoms, + ) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls( + token_atoms_to_queries=atom_layout.GatherInfo.from_dict( + batch, key_prefix='token_atoms_to_queries' + ), + tokens_to_queries=atom_layout.GatherInfo.from_dict( + batch, key_prefix='tokens_to_queries' + ), + tokens_to_keys=atom_layout.GatherInfo.from_dict( + batch, key_prefix='tokens_to_keys' + ), + queries_to_keys=atom_layout.GatherInfo.from_dict( + batch, key_prefix='queries_to_keys' + ), + queries_to_token_atoms=atom_layout.GatherInfo.from_dict( + batch, key_prefix='queries_to_token_atoms' + ), + ) + + def as_data_dict(self) -> BatchDict: + return { + **self.token_atoms_to_queries.as_dict( + key_prefix='token_atoms_to_queries' + ), + **self.tokens_to_queries.as_dict(key_prefix='tokens_to_queries'), + **self.tokens_to_keys.as_dict(key_prefix='tokens_to_keys'), + **self.queries_to_keys.as_dict(key_prefix='queries_to_keys'), + **self.queries_to_token_atoms.as_dict( + key_prefix='queries_to_token_atoms' + ), + } + + +@dataclasses.dataclass +class Frames: + """Features for backbone frames.""" + + mask: xnp_ndarray + + @classmethod + def compute_features( + cls, + all_tokens: atom_layout.AtomLayout, + all_token_atoms_layout: atom_layout.AtomLayout, + ref_structure: RefStructure, + padding_shapes: PaddingShapes, + ) -> Self: + """Computes features for backbone frames.""" + num_tokens = padding_shapes.num_tokens + all_token_atoms_layout = all_token_atoms_layout.copy_and_pad_to( + (num_tokens, all_token_atoms_layout.shape[1]) + ) + + all_token_atoms_to_all_tokens = atom_layout.compute_gather_idxs( + source_layout=all_token_atoms_layout, target_layout=all_tokens + ) + ref_coordinates = atom_layout.convert( + all_token_atoms_to_all_tokens, + ref_structure.positions.astype(np.float32), + layout_axes=(0, 1), + ) + ref_mask = atom_layout.convert( + all_token_atoms_to_all_tokens, + ref_structure.mask.astype(bool), + layout_axes=(0, 1), + ) + ref_mask = ref_mask & all_token_atoms_to_all_tokens.gather_mask.astype( + bool) + + all_frame_mask = [] + + # Iterate over tokens + for idx, args in enumerate( + zip(all_tokens.chain_type, all_tokens.chain_id, all_tokens.res_id) + ): + + chain_type, chain_id, res_id = args + + if chain_type in list(mmcif_names.PEPTIDE_CHAIN_TYPES): + frame_mask = True + elif chain_type in list(mmcif_names.NUCLEIC_ACID_CHAIN_TYPES): + frame_mask = True + elif chain_type in list(mmcif_names.NON_POLYMER_CHAIN_TYPES): + # For ligands, build frames from closest atoms from the same molecule. + (local_token_idxs,) = np.where( + (all_tokens.chain_type == chain_type) + & (all_tokens.chain_id == chain_id) + & (all_tokens.res_id == res_id) + ) + + if len(local_token_idxs) < 3: + frame_mask = False + + else: + # [local_tokens] + local_dist = np.linalg.norm( + ref_coordinates[idx] - ref_coordinates[local_token_idxs], axis=-1 + ) + local_mask = ref_mask[local_token_idxs] + cost = local_dist + 1e8 * ~local_mask + cost = cost + 1e8 * (idx == local_token_idxs) + # [local_tokens] + closest_idxs = np.argsort(cost, axis=0) + + # The closest indices index an array of local tokens. Convert this + # to indices of the full (num_tokens,) array. + global_closest_idxs = local_token_idxs[closest_idxs] + + # Construct frame by placing the current token at the origin and two + # nearest atoms on either side. + global_frame_idxs = np.array( + (global_closest_idxs[0], idx, global_closest_idxs[1]) + ) + + # Check that the frame atoms are not colinear. + a, b, c = ref_coordinates[global_frame_idxs] + vec1 = a - b + vec2 = c - b + # Reference coordinates can be all zeros, in which case we have + # to explicitly set colinearity. + if np.isclose(np.linalg.norm(vec1, axis=-1), 0) or np.isclose( + np.linalg.norm(vec2, axis=-1), 0 + ): + is_colinear = True + logging.info( + 'Found identical coordinates: Assigning as colinear.') + else: + vec1 = vec1 / np.linalg.norm(vec1, axis=-1) + vec2 = vec2 / np.linalg.norm(vec2, axis=-1) + cos_angle = np.einsum('...k,...k->...', vec1, vec2) + # <25 degree deviation is considered colinear. + is_colinear = 1 - np.abs(cos_angle) < 0.0937 + + frame_mask = not is_colinear + else: + # No frame for other chain types. + frame_mask = False + + all_frame_mask.append(frame_mask) + + all_frame_mask = np.array(all_frame_mask, dtype=bool) + + mask = _pad_to(all_frame_mask, (padding_shapes.num_tokens,)) + + return cls(mask=mask) + + @classmethod + def from_data_dict(cls, batch: BatchDict) -> Self: + return cls(mask=batch['frames_mask']) + + def as_data_dict(self) -> BatchDict: + return {'frames_mask': self.mask} diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/load_batch.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/load_batch.py new file mode 100644 index 000000000..41cf2bf48 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/load_batch.py @@ -0,0 +1,22 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +"""load data 'batch' used in test""" +import pickle +import mindspore as ms +from alphafold3.model.feat_batch import Batch + + +def load_batch(dtype=ms.float32): + """Load batch data for test""" + with open('/data/zmmVol2/AF3/test/unit_tests/model/diffusion/example_np.pkl', 'rb') as f: + data = pickle.load(f) + batch = Batch.from_data_dict(data) + batch.convert_to_tensor(dtype=dtype) + return batch diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/merging_features.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/merging_features.py new file mode 100644 index 000000000..3c1fab899 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/merging_features.py @@ -0,0 +1,92 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Methods for merging existing features to create a new example. + +Covers: +- Merging features across chains. +- Merging the paired and unpaired parts of the MSA. +""" + +from typing import TypeAlias + +from alphafold3.model import data_constants +import numpy as np + +NUM_SEQ_NUM_RES_MSA_FEATURES = data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES +NUM_SEQ_MSA_FEATURES = data_constants.NUM_SEQ_MSA_FEATURES +MSA_PAD_VALUES = data_constants.MSA_PAD_VALUES + + +xnp_ndarray: TypeAlias = np.ndarray # pylint: disable=invalid-name +BatchDict: TypeAlias = dict[str, xnp_ndarray] + + +def _pad_features_to_max(feat_name: str, chains: list[BatchDict], axis: int): + """Pad a set of features to the maximum size amongst all chains. + + Args: + feat_name: The feature name to pad. + chains: A list of chains with associated features. + axis: Which axis to pad to the max. + + Returns: + A list of features, all with the same size on the given axis. + """ + max_num_seq = np.max([chain[feat_name].shape[axis] for chain in chains]) + + padded_feats = [] + for chain in chains: + feat = chain[feat_name] + + padding = np.zeros_like(feat.shape) # pytype: disable=attribute-error + # pytype: disable=attribute-error + padding[axis] = max_num_seq - feat.shape[axis] + padding = [(0, p) for p in padding] + padded_feats.append( + np.pad( + feat, + padding, + mode='constant', + constant_values=MSA_PAD_VALUES[feat_name], + ) + ) + return padded_feats + + +def merge_msa_features(feat_name: str, chains: list[BatchDict]) -> np.ndarray: + """Merges MSA features with shape (NUM_SEQ, NUM_RES) across chains.""" + expected_dtype = chains[0][feat_name].dtype + if '_all_seq' in feat_name: + return np.concatenate( + [c.get(feat_name, np.array([], expected_dtype)) for c in chains], axis=1 + ) + else: + # Since each MSA can be of different lengths, we first need to pad them + # all to the size of the largest MSA before concatenating. + padded_feats = _pad_features_to_max(feat_name, chains, axis=0) + return np.concatenate(padded_feats, axis=1) + + +def merge_paired_and_unpaired_msa(example: BatchDict) -> BatchDict: + """Concatenates the paired (all_seq) MSA features with the unpaired ones.""" + new_example = dict(example) + + for feature_name in NUM_SEQ_NUM_RES_MSA_FEATURES + NUM_SEQ_MSA_FEATURES: + if feature_name in example and feature_name + '_all_seq' in example: + feat = example[feature_name] + feat_all_seq = example[feature_name + '_all_seq'] + merged_feat = np.concatenate([feat_all_seq, feat], axis=0) + new_example[feature_name] = merged_feat + + new_example['num_alignments'] = np.array( + new_example['msa'].shape[0], dtype=np.int32 + ) + return new_example diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc new file mode 100644 index 000000000..663e7f303 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc @@ -0,0 +1,63 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include "alphafold3/model/mkdssp_pybind.h" + +#include + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" + +namespace alphafold3 { +namespace py = pybind11; + +void RegisterModuleMkdssp(pybind11::module m) { + py::module site = py::module::import("site"); + py::list paths = py::cast(site.attr("getsitepackages")()); + // Find the first path that contains the libcifpp components.cif file. + bool found = false; + for (const auto& py_path : paths) { + auto path_str = + std::filesystem::path(py::cast(py_path)) / + "share/libcifpp/components.cif"; + if (std::filesystem::exists(path_str)) { + setenv("LIBCIFPP_DATA_DIR", path_str.parent_path().c_str(), 0); + found = true; + break; + } + } + if (!found) { + throw py::type_error("Could not find the libcifpp components.cif file."); + } + m.def( + "get_dssp", + [](absl::string_view mmcif, int model_no, + int min_poly_proline_stretch_length, + bool calculate_surface_accessibility) { + cif::file cif_file(mmcif.data(), mmcif.size()); + dssp result(cif_file.front(), model_no, min_poly_proline_stretch_length, + calculate_surface_accessibility); + std::stringstream sstream; + result.write_legacy_output(sstream); + return sstream.str(); + }, + py::arg("mmcif"), py::arg("model_no") = 1, + py::arg("min_poly_proline_stretch_length") = 3, + py::arg("calculate_surface_accessibility") = false, + py::doc("Gets secondary structure from an mmCIF file.")); +} + +} // namespace alphafold3 \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.h new file mode 100644 index 000000000..a1e4832b8 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.h @@ -0,0 +1,26 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ + + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMkdssp(pybind11::module m); + +} + + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_MODEL_MKDSSP_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py new file mode 100644 index 000000000..31784589a --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py @@ -0,0 +1,202 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Adds mmCIF metadata (to be ModelCIF-conformant) and author and legal info.""" + +from typing import Final + +from alphafold3.structure import mmcif +import numpy as np + +_LICENSE_URL: Final[str] = ( + 'https://github.com/google-deepmind/alphafold3/blob/main/OUTPUT_TERMS_OF_USE.md' +) + +_LICENSE: Final[str] = f"""\ +Non-commercial use only, by using this file you agree to the terms of use found +at {_LICENSE_URL}. +To request access to the AlphaFold 3 model parameters, follow the process set +out at https://github.com/google-deepmind/alphafold3. You may only use these if +received directly from Google. Use is subject to terms of use available at +https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md. +""" + +_DISCLAIMER: Final[str] = """\ +AlphaFold 3 and its output are not intended for, have not been validated for, +and are not approved for clinical use. They are provided "as-is" without any +warranty of any kind, whether expressed or implied. No warranty is given that +use shall not infringe the rights of any third party. +""" + +_MMCIF_PAPER_AUTHORS: Final[tuple[str, ...]] = ( + 'Google DeepMind', + 'Isomorphic Labs', +) + +# Authors of the mmCIF - we set them to be equal to the authors of the paper. +_MMCIF_AUTHORS: Final[tuple[str, ...]] = _MMCIF_PAPER_AUTHORS + + +def add_metadata_to_mmcif( + old_cif: mmcif.Mmcif, model_id: bytes +) -> mmcif.Mmcif: + """Adds metadata to a mmCIF to make it ModelCIF-conformant.""" + cif = {} + + # ModelCIF conformation dictionary. + cif['_audit_conform.dict_name'] = ['mmcif_ma.dic'] +# cif['_audit_conform.dict_version'] = ['1.4.5'] + cif['_audit_conform.dict_location'] = [ + 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist/mmcif_ma.dic' + ] + + cif['_pdbx_data_usage.id'] = ['1', '2'] + cif['_pdbx_data_usage.type'] = ['license', 'disclaimer'] + cif['_pdbx_data_usage.details'] = [_LICENSE, _DISCLAIMER] + cif['_pdbx_data_usage.url'] = [_LICENSE_URL, '?'] + + # Structure author details. + cif['_audit_author.name'] = [] + cif['_audit_author.pdbx_ordinal'] = [] + for author_index, author_name in enumerate(_MMCIF_AUTHORS, start=1): + cif['_audit_author.name'].append(author_name) + cif['_audit_author.pdbx_ordinal'].append(str(author_index)) + + # Paper author details. + cif['_citation_author.citation_id'] = [] + cif['_citation_author.name'] = [] + cif['_citation_author.ordinal'] = [] + for author_index, author_name in enumerate(_MMCIF_PAPER_AUTHORS, start=1): + cif['_citation_author.citation_id'].append('primary') + cif['_citation_author.name'].append(author_name) + cif['_citation_author.ordinal'].append(str(author_index)) + + # Paper citation details. + cif['_citation.id'] = ['primary'] + cif['_citation.title'] = [ + 'Accurate structure prediction of biomolecular interactions with' + ' AlphaFold 3' + ] + cif['_citation.journal_full'] = ['Nature'] + cif['_citation.journal_volume'] = ['630'] + cif['_citation.page_first'] = ['493'] + cif['_citation.page_last'] = ['500'] + cif['_citation.year'] = ['2024'] + cif['_citation.journal_id_ASTM'] = ['NATUAS'] + cif['_citation.country'] = ['UK'] + cif['_citation.journal_id_ISSN'] = ['0028-0836'] + cif['_citation.journal_id_CSD'] = ['0006'] + cif['_citation.book_publisher'] = ['?'] + cif['_citation.pdbx_database_id_PubMed'] = ['38718835'] + cif['_citation.pdbx_database_id_DOI'] = ['10.1038/s41586-024-07487-w'] + + # Type of data in the dataset including data used in the model generation. + cif['_ma_data.id'] = ['1'] + cif['_ma_data.name'] = ['Model'] + cif['_ma_data.content_type'] = ['model coordinates'] + + # Description of number of instances for each entity. + cif['_ma_target_entity_instance.asym_id'] = old_cif['_struct_asym.id'] + cif['_ma_target_entity_instance.entity_id'] = old_cif[ + '_struct_asym.entity_id' + ] + cif['_ma_target_entity_instance.details'] = ['.'] * len( + cif['_ma_target_entity_instance.entity_id'] + ) + + # Details about the target entities. + cif['_ma_target_entity.entity_id'] = cif[ + '_ma_target_entity_instance.entity_id' + ] + cif['_ma_target_entity.data_id'] = ['1'] * len( + cif['_ma_target_entity.entity_id'] + ) + cif['_ma_target_entity.origin'] = ['.'] * len( + cif['_ma_target_entity.entity_id'] + ) + + # Details of the models being deposited. + cif['_ma_model_list.ordinal_id'] = ['1'] + cif['_ma_model_list.model_id'] = ['1'] + cif['_ma_model_list.model_group_id'] = ['1'] + cif['_ma_model_list.model_name'] = ['Top ranked model'] + + cif['_ma_model_list.model_group_name'] = [ + f'AlphaFold-beta-20231127' + ] + cif['_ma_model_list.data_id'] = ['1'] + cif['_ma_model_list.model_type'] = ['Ab initio model'] + + # Software used. + cif['_software.pdbx_ordinal'] = ['1'] + cif['_software.name'] = ['AlphaFold'] +# cif['_software.version'] = [ +# f'AlphaFold-beta-20231127 ({model_id.decode("ascii")})' +# ] + cif['_software.type'] = ['package'] + cif['_software.description'] = ['Structure prediction'] + cif['_software.classification'] = ['other'] + cif['_software.date'] = ['?'] + + # Collection of software into groups. + cif['_ma_software_group.ordinal_id'] = ['1'] + cif['_ma_software_group.group_id'] = ['1'] + cif['_ma_software_group.software_id'] = ['1'] + + # Method description to conform with ModelCIF. + cif['_ma_protocol_step.ordinal_id'] = ['1', '2', '3'] + cif['_ma_protocol_step.protocol_id'] = ['1', '1', '1'] + cif['_ma_protocol_step.step_id'] = ['1', '2', '3'] + cif['_ma_protocol_step.method_type'] = [ + 'coevolution MSA', + 'template search', + 'modeling', + ] + + # Details of the metrics use to assess model confidence. + cif['_ma_qa_metric.id'] = ['1', '2'] + cif['_ma_qa_metric.name'] = ['pLDDT', 'pLDDT'] + # Accepted values are distance, energy, normalised score, other, zscore. + cif['_ma_qa_metric.type'] = ['pLDDT', 'pLDDT'] + cif['_ma_qa_metric.mode'] = ['global', 'local'] + cif['_ma_qa_metric.software_group_id'] = ['1', '1'] + + # Global model confidence metric value. + cif['_ma_qa_metric_global.ordinal_id'] = ['1'] + cif['_ma_qa_metric_global.model_id'] = ['1'] + cif['_ma_qa_metric_global.metric_id'] = ['1'] + global_plddt = np.mean( + [float(v) for v in old_cif['_atom_site.B_iso_or_equiv']] + ) + cif['_ma_qa_metric_global.metric_value'] = [f'{global_plddt:.2f}'] + + cif['_atom_type.symbol'] = sorted(set(old_cif['_atom_site.type_symbol'])) + + return old_cif.copy_and_update(cif) + + +def add_legal_comment(cif: str) -> str: + """Adds legal comment at the top of the mmCIF.""" + # fmt: off + # pylint: disable=line-too-long + comment = ( + '# By using this file you agree to the legally binding terms of use found at\n' + f'# {_LICENSE_URL}.\n' + '# To request access to the AlphaFold 3 model parameters, follow the process set\n' + '# out at https://github.com/google-deepmind/alphafold3. You may only use these if\n' + '# received directly from Google. Use is subject to terms of use available at\n' + '# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md.' + ) + # pylint: enable=line-too-long + # fmt: on + return f'{comment}\n{cif}' diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/model_config.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/model_config.py new file mode 100644 index 000000000..83cf9ce75 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/model_config.py @@ -0,0 +1,32 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Config for the protein folding model and experiment.""" + +from collections.abc import Sequence +from typing import Literal, TypeAlias + +from alphafold3.model import base_config +from alphafold3.utils.attention import attention + + +_Shape2DType: TypeAlias = tuple[int | None, int | None] + + +class GlobalConfig(base_config.BaseConfig): + bfloat16: Literal['all', 'none', 'intermediate'] = 'none' + final_init: Literal['zeros', 'linear'] = 'zeros' + pair_attention_chunk_size: Sequence[_Shape2DType] = ( + (1536, 128), (None, 32)) + pair_transition_shard_spec: Sequence[_Shape2DType] = ( + (2048, None), + (None, 1024), + ) + flash_attention_implementation: attention.Implementation = 'ms' diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/msa_pairing.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/msa_pairing.py new file mode 100644 index 000000000..6d563eabd --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/msa_pairing.py @@ -0,0 +1,316 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Functions for producing "paired" and "unpaired" MSA features for each chain. + +The paired MSA: +- Is made from the result of the all_seqs MSA query. +- Is ordered such that you can concatenate features across chains and related + sequences will end up on the same row. Related here means "from the same + species". Gaps are added to facilitate this whenever a sequence has no + suitable pair. + +The unpaired MSA: +- Is made from the results of the remaining MSA queries. +- Has no special ordering properties. +- Is deduplicated such that it doesn't contain any sequences in the paired MSA. +""" + +from typing import Mapping, MutableMapping, Sequence +from alphafold3.model import data_constants +import numpy as np + + +def _align_species( + all_species: Sequence[bytes], + chains_species_to_rows: Sequence[Mapping[bytes, np.ndarray]], + min_hits_per_species: Mapping[bytes, int], +) -> np.ndarray: + """Aligns MSA row indices based on species. + + Within a species, MSAs are aligned based on their original order (the first + sequence for a species in the first chain's MSA is aligned to the first + sequence for the same species in the second chain's MSA). + + Args: + all_species: A list of all unique species identifiers. + chains_species_to_rows: A dictionary for each chain, that maps species to + the set of MSA row indices from that species in that chain. + min_hits_per_species: A mapping from species id, to the minimum MSA size + across chains for that species (ignoring chains with zero hits). + + Returns: + A matrix of size [num_msa_rows, num_chains], where the i,j element is an + index into the jth chains MSA. Each row consists of sequences from each + chain for the same species (or -1 if that chain has no sequences for that + species). + """ + # Each species block is of size [num_seqs x num_chains] and consists of + # indices into the respective MSAs that have been aligned and are all for the + # same species. + species_blocks = [] + for species in all_species: + chain_row_indices = [] + for species_to_rows in chains_species_to_rows: + min_msa_size = min_hits_per_species[species] + if species not in species_to_rows: + # If a given chain has no hits for a species then we pad it with -1's, + # later on these values are used to make sure each feature is padded + # with its appropriate pad value. + row_indices = np.full( + min_msa_size, fill_value=-1, dtype=np.int32) + else: + # We crop down to the smallest MSA for a given species across chains. + row_indices = species_to_rows[species][:min_msa_size] + chain_row_indices.append(row_indices) + species_block = np.stack(chain_row_indices, axis=1) + species_blocks.append(species_block) + aligned_matrix = np.concatenate(species_blocks, axis=0) + return aligned_matrix + + +def create_paired_features( + chains: Sequence[MutableMapping[str, np.ndarray]], + max_paired_sequences: int, + nonempty_chain_ids: set[str], + max_hits_per_species: int, +) -> Sequence[MutableMapping[str, np.ndarray]]: + """Creates per-chain MSA features where the MSAs have been aligned. + + Args: + chains: A list of feature dicts, one for each chain. + max_paired_sequences: No more than this many paired sequences will be + returned from this function. + nonempty_chain_ids: A set of chain ids (str) that are included in the crop + there is no reason to process chains not in this list. + max_hits_per_species: No more than this number of sequences will be returned + for a given species. + + Returns: + An updated feature dictionary for each chain, where the {}_all_seq features + have been aligned so that the nth row in chain 1 is aligned to the nth row + in chain 2's features. + """ + # The number of chains that the given species appears in - we rank hits + # across more chains higher. + species_num_chains = {} + + # For each chain we keep a mapping from species to the row indices in the + # original MSA for that chain. + chains_species_to_rows = [] + + # Keep track of the minimum number of hits across chains for a given species. + min_hits_per_species = {} + + for chain in chains: + species_ids = chain['msa_species_identifiers_all_seq'] + + # The query gets an empty species_id, so no pairing happens for this row. + if ( + species_ids.size == 0 + or (species_ids.size == 1 and not species_ids[0]) + or chain['chain_id'] not in nonempty_chain_ids + ): + chains_species_to_rows.append({}) + continue + + # For each species keep track of which row indices in the original MSA are + # from this species. + row_indices = np.arange(len(species_ids)) + # The grouping np.split code requires that the input is already clustered + # by species id. + sort_idxs = species_ids.argsort() + species_ids = species_ids[sort_idxs] + row_indices = row_indices[sort_idxs] + + species, unique_row_indices = np.unique(species_ids, return_index=True) + grouped_row_indices = np.split(row_indices, unique_row_indices[1:]) + species_to_rows = dict(zip(species, grouped_row_indices, strict=True)) + chains_species_to_rows.append(species_to_rows) + + for s in species: + species_num_chains[s] = species_num_chains.get(s, 0) + 1 + + for species, row_indices in species_to_rows.items(): + min_hits_per_species[species] = min( + min_hits_per_species.get(species, max_hits_per_species), + len(row_indices), + ) + + # Construct a mapping from the number of chains a species appears in to + # the list of species with that count. + num_chains_to_species = {} + for species, num_chains in species_num_chains.items(): + if not species or num_chains <= 1: + continue + if num_chains not in num_chains_to_species: + num_chains_to_species[num_chains] = [] + num_chains_to_species[num_chains].append(species) + + num_rows_seen = 0 + # We always keep the first row as it is the query sequence. + all_rows = [np.array([[0] * len(chains)], dtype=np.int32)] + + # We prioritize species that have hits across more chains. + for num_chains in sorted(num_chains_to_species, reverse=True): + all_species = num_chains_to_species[num_chains] + + # Align all the per-chain row indices by species, so every paired row is + # for a single species. + rows = _align_species( + all_species, chains_species_to_rows, min_hits_per_species + ) + # Sort rows by the product of the original indices in the respective chain + # MSAS, so as to rank hits that appear higher in the original MSAs higher. + rank_metric = np.abs(np.prod(rows.astype(np.float32), axis=1)) + sorted_rows = rows[np.argsort(rank_metric), :] + all_rows.append(sorted_rows) + num_rows_seen += rows.shape[0] + if num_rows_seen >= max_paired_sequences: + break + + all_rows = np.concatenate(all_rows, axis=0) + all_rows = all_rows[:max_paired_sequences, :] + + # Now we just have to select the relevant rows from the original msa and + # deletion matrix features + paired_chains = [] + for chain_idx, chain in enumerate(chains): + out_chain = {k: v for k, v in chain.items() if 'all_seq' not in k} + selected_row_indices = all_rows[:, chain_idx] + for feat_name in {'msa', 'deletion_matrix'}: + all_seq_name = f'{feat_name}_all_seq' + feat_value = chain[all_seq_name] + + # The selected row indices are padded to be the same shape for each chain, + # they are padded with -1's, so we add a single row onto the feature with + # the appropriate pad value. This has the effect that we correctly pad + # each feature since all padded indices will select this padding row. + pad_value = data_constants.MSA_PAD_VALUES[feat_name] + feat_value = np.concatenate([ + feat_value, + np.full((1, feat_value.shape[1]), pad_value, feat_value.dtype), + ]) + + feat_value = feat_value[selected_row_indices, :] + out_chain[all_seq_name] = feat_value + out_chain['num_alignments_all_seq'] = np.array( + out_chain['msa_all_seq'].shape[0] + ) + paired_chains.append(out_chain) + return paired_chains + + +def deduplicate_unpaired_sequences( + np_chains: Sequence[MutableMapping[str, np.ndarray]], +) -> Sequence[MutableMapping[str, np.ndarray]]: + """Deduplicates unpaired sequences based on paired sequences.""" + + feature_names = np_chains[0].keys() + msa_features = ( + data_constants.NUM_SEQ_MSA_FEATURES + + data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES + ) + + for chain in np_chains: + sequence_set = set( + hash(s.data.tobytes()) for s in chain['msa_all_seq'].astype(np.int8) + ) + keep_rows = [] + # Go through unpaired MSA seqs and remove any rows that correspond to the + # sequences that are already present in the paired MSA. + for row_num, seq in enumerate(chain['msa'].astype(np.int8)): + if hash(seq.data.tobytes()) not in sequence_set: + keep_rows.append(row_num) + for feature_name in feature_names: + if feature_name in msa_features: + chain[feature_name] = chain[feature_name][keep_rows] + chain['num_alignments'] = np.array( + chain['msa'].shape[0], dtype=np.int32) + return np_chains + + +def choose_paired_unpaired_msa_crop_sizes( + unpaired_msa: np.ndarray, + paired_msa: np.ndarray | None, + total_msa_crop_size: int, + max_paired_sequences: int, +) -> tuple[int, int | None]: + """Returns the sizes of the MSA crop and MSA_all_seq crop. + + NOTE: Unpaired + paired MSA sizes can exceed total_msa_size when + there are lots of gapped rows. Through the pairing logic another chain(s) + will have fewer than total_msa_size. + + Args: + unpaired_msa: The unpaired MSA array (not all_seq). + paired_msa: The paired MSA array (all_seq). + total_msa_crop_size: The maximum total number of sequences to crop to. + max_paired_sequences: The maximum number of sequences that can come from + MSA pairing. + + Returns: + A tuple of: + The size of the reduced MSA crop (not all_seq features). + The size of the unreduced MSA crop (for all_seq features) or None, if + paired_msa is None. + """ + if paired_msa is not None: + paired_crop_size = np.minimum( + paired_msa.shape[0], max_paired_sequences) + + # We reduce the number of un-paired sequences, by the number of times a + # sequence from this chains MSA is included in the paired MSA. This keeps + # the MSA size for each chain roughly constant. + cropped_all_seq_msa = paired_msa[:max_paired_sequences] + num_non_gapped_pairs = cropped_all_seq_msa.shape[0] + + assert num_non_gapped_pairs <= max_paired_sequences + unpaired_crop_size = np.minimum( + unpaired_msa.shape[0], total_msa_crop_size - num_non_gapped_pairs + ) + assert unpaired_crop_size >= 0 + else: + unpaired_crop_size = np.minimum( + unpaired_msa.shape[0], total_msa_crop_size) + paired_crop_size = None + return unpaired_crop_size, paired_crop_size + + +def remove_all_gapped_rows_from_all_seqs( + chains_list: Sequence[dict[str, np.ndarray]], asym_ids: Sequence[float] +) -> Sequence[dict[str, np.ndarray]]: + """Removes all gapped rows from all_seq feat based on selected asym_ids.""" + + merged_msa_all_seq = np.concatenate( + [ + chain['msa_all_seq'] + for chain in chains_list + if chain['asym_id'][0] in asym_ids + ], + axis=1, + ) + + non_gapped_keep_rows = np.any( + merged_msa_all_seq != data_constants.MSA_GAP_IDX, axis=1 + ) + for chain in chains_list: + for feat_name in list(chains_list)[0]: + if '_all_seq' in feat_name: + feat_name_split = feat_name.split('_all_seq')[0] + if feat_name_split in ( + data_constants.NUM_SEQ_NUM_RES_MSA_FEATURES + + data_constants.NUM_SEQ_MSA_FEATURES + ): + # For consistency we do this for all chains even though the + # gapped rows are based on a selected set asym_ids. + chain[feat_name] = chain[feat_name][non_gapped_keep_rows] + chain['num_alignments_all_seq'] = np.sum(non_gapped_keep_rows) + return chains_list diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/params.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/params.py new file mode 100644 index 000000000..3c1d22df6 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/params.py @@ -0,0 +1,218 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Model param loading.""" + +import bisect +import collections +from collections.abc import Iterator +import contextlib +import io +import os +import pathlib +import re +import struct +import sys +from typing import IO +import numpy as np + + +class RecordError(Exception): + """Error reading a record.""" + + +def encode_record(scope: str, name: str, arr: np.ndarray) -> bytes: + """Encodes a single haiku param as bytes, preserving non-numpy dtypes.""" + scope = scope.encode('utf-8') + name = name.encode('utf-8') + shape = arr.shape + dtype = str(arr.dtype).encode('utf-8') + arr = np.ascontiguousarray(arr) + if sys.byteorder == 'big': + arr = arr.byteswap() + arr_buffer = arr.tobytes('C') + header = struct.pack( + '<5i', len(scope), len(name), len(dtype), len(shape), len(arr_buffer) + ) + return header + b''.join( + (scope, name, dtype, struct.pack(f'{len(shape)}i', *shape), arr_buffer) + ) + + +def _read_record(stream: IO[bytes]) -> tuple[str, str, np.ndarray] | None: + """Reads a record encoded by `_encode_record` from a byte stream.""" + header_size = struct.calcsize('<5i') + header = stream.read(header_size) + if not header: + return None + if len(header) < header_size: + raise RecordError( + f'Incomplete header: {len(header)=} < {header_size=}') + (scope_len, name_len, dtype_len, shape_len, arr_buffer_len) = struct.unpack( + '<5i', header + ) + fmt = f'<{scope_len}s{name_len}s{dtype_len}s{shape_len}i' + payload_size = struct.calcsize(fmt) + arr_buffer_len + payload = stream.read(payload_size) + if len(payload) < payload_size: + raise RecordError( + f'Incomplete payload: {len(payload)=} < {payload_size=}') + scope, name, dtype, *shape = struct.unpack_from(fmt, payload) + scope = scope.decode('utf-8') + name = name.decode('utf-8') + dtype = dtype.decode('utf-8') + if dtype == 'bfloat16': + buffer = payload[-arr_buffer_len:] + if sys.byteorder == 'big': + buffer = buffer[::-1] + arr_uint16 = np.frombuffer(buffer, dtype=np.uint16) + arr_bf16 = arr_uint16.view('bfloat16') + arr = arr_bf16.astype(np.float32) + else: + arr = np.frombuffer(payload[-arr_buffer_len:], dtype=dtype) + if sys.byteorder == 'big': + arr = arr.byteswap() + arr = np.reshape(arr, shape) + if sys.byteorder == 'big': + arr = arr.byteswap() + return scope, name, arr + + +def read_records(stream: IO[bytes]) -> Iterator[tuple[str, str, np.ndarray]]: + """Fully reads the contents of a byte stream.""" + while record := _read_record(stream): + yield record + + +class _MultiFileIO(io.RawIOBase): + """A file-like object that presents a concatenated view of multiple files.""" + + def __init__(self, files: list[pathlib.Path]): + self._files = files + self._stack = contextlib.ExitStack() + self._handles = [ + self._stack.enter_context(file.open('rb')) for file in files + ] + self._sizes = [] + for handle in self._handles: + handle.seek(0, os.SEEK_END) + self._sizes.append(handle.tell()) + self._length = sum(self._sizes) + self._offsets = [0] + for s in self._sizes[:-1]: + self._offsets.append(self._offsets[-1] + s) + self._abspos = 0 + self._relpos = (0, 0) + + def _abs_to_rel(self, pos: int) -> tuple[int, int]: + idx = bisect.bisect_right(self._offsets, pos) - 1 + return idx, pos - self._offsets[idx] + + def close(self): + self._stack.close() + + def closed(self) -> bool: + return all(handle.closed for handle in self._handles) + + def fileno(self) -> int: + return -1 + + def readable(self) -> bool: + return True + + def tell(self) -> int: + return self._abspos + + def seek(self, pos: int, whence: int = os.SEEK_SET, /): + match whence: + case os.SEEK_SET: + pass + case os.SEEK_CUR: + pos += self._abspos + case os.SEEK_END: + pos = self._length - pos + case _: + raise ValueError(f'Invalid whence: {whence}') + self._abspos = pos + self._relpos = self._abs_to_rel(pos) + + def readinto(self, b: bytearray | memoryview) -> int: + result = 0 + mem = memoryview(b) + while mem: + self._handles[self._relpos[0]].seek(self._relpos[1]) + count = self._handles[self._relpos[0]].readinto(mem) + result += count + self._abspos += count + self._relpos = self._abs_to_rel(self._abspos) + mem = mem[count:] + if self._abspos == self._length: + break + return result + + +@contextlib.contextmanager +def open_for_reading(model_files: list[pathlib.Path], is_compressed: bool): + with contextlib.closing(_MultiFileIO(model_files)) as f: + yield f + + +def _match_model( + paths: list[pathlib.Path], pattern: re.Pattern[str] +) -> dict[str, list[pathlib.Path]]: + """Match files in a directory with a pattern, and group by model name.""" + models = collections.defaultdict(list) + for path in paths: + match = pattern.fullmatch(path.name) + if match: + models[match.group('model_name')].append(path) + return {k: sorted(v) for k, v in models.items()} + + +def select_model_files( + model_dir: pathlib.Path, model_name: str | None = None +) -> tuple[list[pathlib.Path], bool]: + """Select the model files from a model directory.""" + files = [file for file in model_dir.iterdir() if file.is_file()] + + for pattern, is_compressed in ( + (r'(?P.*)\.[0-9]+\.bin\.zst$', True), + (r'(?P.*)\.bin\.zst\.[0-9]+$', True), + (r'(?P.*)\.[0-9]+\.bin$', False), + (r'(?P.*)\.bin]\.[0-9]+$', False), + (r'(?P.*)\.bin\.zst$', True), + (r'(?P.*)\.bin$', False), + ): + models = _match_model(files, re.compile(pattern)) + if model_name is not None: + if model_name in models: + return models[model_name], is_compressed + else: + if models: + if len(models) > 1: + raise RuntimeError( + f'Multiple models matched in {model_dir}') + _, model_files = models.popitem() + return model_files, is_compressed + raise FileNotFoundError(f'No models matched in {model_dir}') + + +def get_model_af3_params(model_dir: pathlib.Path): + """Get the Haiku parameters from a model name.""" + params: dict[str, dict[str, np.array]] = {} + model_files, is_compressed = select_model_files(model_dir) + with open_for_reading(model_files, is_compressed) as stream: + for scope, name, arr in read_records(stream): + params.setdefault(scope, {})[name] = np.array(arr) + if not params: + raise FileNotFoundError(f'Model missing from "{model_dir}"') + return params diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/inter_chain_bonds.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/inter_chain_bonds.py new file mode 100644 index 000000000..536c8b0ca --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/inter_chain_bonds.py @@ -0,0 +1,348 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Functions for handling inter-chain bonds.""" + +from collections.abc import Collection +import functools +from typing import Final, NamedTuple +import numpy as np +from alphafold3 import structure +from alphafold3.constants import chemical_component_sets +from alphafold3.constants import mmcif_names +from alphafold3.model.atom_layout import atom_layout + + + +BOND_THRESHOLD_GLYCANS_ANGSTROM: Final[float] = 1.7 +# See https://pubs.acs.org/doi/10.1021/ja010331r for P-P atom bond distances. +BOND_THRESHOLD_ALL_ANGSTROM: Final[float] = 2.4 + + +class BondAtomArrays(NamedTuple): + chain_id: np.ndarray + chain_type: np.ndarray + res_id: np.ndarray + res_name: np.ndarray + atom_name: np.ndarray + coords: np.ndarray + + +def _get_bond_atom_arrays( + struct: structure.Structure, bond_atom_indices: np.ndarray +) -> BondAtomArrays: + return BondAtomArrays( + chain_id=struct.chain_id[bond_atom_indices], + chain_type=struct.chain_type[bond_atom_indices], + res_id=struct.res_id[bond_atom_indices], + res_name=struct.res_name[bond_atom_indices], + atom_name=struct.atom_name[bond_atom_indices], + coords=struct.coords[..., bond_atom_indices, :], + ) + + +@functools.lru_cache(maxsize=1) +def get_polymer_ligand_and_ligand_ligand_bonds( + struct: structure.Structure, + only_glycan_ligands: bool, + allow_multiple_bonds_per_atom: bool, +) -> tuple[atom_layout.AtomLayout, atom_layout.AtomLayout]: + """Return polymer-ligand & ligand-ligand inter-residue bonds. + + Args: + struct: Structure object to extract bonds from. + only_glycan_ligands: Whether to only include glycans in ligand category. + allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first + bond seen per atom and discard the remaining on each atom.. + + Returns: + polymer_ligand, ligand_ligand_bonds: Each object is an AtomLayout object + [num_bonds, 2] for the bond-defining atoms. + """ + if only_glycan_ligands: + allowed_res_names = list({ + *chemical_component_sets.GLYCAN_OTHER_LIGANDS, + *chemical_component_sets.GLYCAN_LINKING_LIGANDS, + }) + else: + allowed_res_names = None + all_bonds = get_bond_layout( + bond_threshold=BOND_THRESHOLD_GLYCANS_ANGSTROM + if only_glycan_ligands + else BOND_THRESHOLD_ALL_ANGSTROM, + struct=struct, + allowed_chain_types1=list({ + *mmcif_names.LIGAND_CHAIN_TYPES, + *mmcif_names.POLYMER_CHAIN_TYPES, + }), + allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), + allowed_res_names=allowed_res_names, + allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, + ) + ligand_ligand_bonds_mask = np.isin( + all_bonds.chain_type, list(mmcif_names.LIGAND_CHAIN_TYPES) + ) + polymer_ligand_bonds_mask = np.isin( + all_bonds.chain_type, list(mmcif_names.POLYMER_CHAIN_TYPES) + ) + polymer_ligand_bonds_mask = np.logical_and( + ligand_ligand_bonds_mask.any(axis=1), + polymer_ligand_bonds_mask.any(axis=1), + ) + ligand_ligand_bonds = all_bonds[ligand_ligand_bonds_mask.all(axis=1)] + polymer_ligand_bonds = all_bonds[polymer_ligand_bonds_mask] + return polymer_ligand_bonds, ligand_ligand_bonds + + +def _remove_multi_bonds( + bond_layout: atom_layout.AtomLayout, +) -> atom_layout.AtomLayout: + """Remove instances greedily.""" + uids = {} + keep_indx = [] + for chain_id, res_id, atom_name in zip( + bond_layout.chain_id, + bond_layout.res_id, + bond_layout.atom_name, + strict=True, + ): + key1 = (chain_id[0], res_id[0], atom_name[0]) + key2 = (chain_id[1], res_id[1], atom_name[1]) + keep_indx.append(bool(key1 not in uids) and bool(key2 not in uids)) + if key1 not in uids: + uids[key1] = None + if key2 not in uids: + uids[key2] = None + return bond_layout[np.array(keep_indx, dtype=bool)] + + +@functools.lru_cache(maxsize=1) +def get_ligand_ligand_bonds( + struct: structure.Structure, + only_glycan_ligands: bool, + allow_multiple_bonds_per_atom: bool = False, +) -> atom_layout.AtomLayout: + """Return ligand-ligand inter-residue bonds. + + Args: + struct: Structure object to extract bonds from. + only_glycan_ligands: Whether to only include glycans in ligand category. + allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first + bond seen per atom and discard the remaining on each atom. + + Returns: + bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. + """ + if only_glycan_ligands: + allowed_res_names = list({ + *chemical_component_sets.GLYCAN_OTHER_LIGANDS, + *chemical_component_sets.GLYCAN_LINKING_LIGANDS, + }) + else: + allowed_res_names = None + return get_bond_layout( + bond_threshold=BOND_THRESHOLD_GLYCANS_ANGSTROM + if only_glycan_ligands + else BOND_THRESHOLD_ALL_ANGSTROM, + struct=struct, + allowed_chain_types1=list(mmcif_names.LIGAND_CHAIN_TYPES), + allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), + allowed_res_names=allowed_res_names, + allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, + ) + + +@functools.lru_cache(maxsize=1) +def get_polymer_ligand_bonds( + struct: structure.Structure, + only_glycan_ligands: bool, + allow_multiple_bonds_per_atom: bool = False, + bond_threshold: float | None = None, +) -> atom_layout.AtomLayout: + """Return polymer-ligand interchain bonds. + + Args: + struct: Structure object to extract bonds from. + only_glycan_ligands: Whether to only include glycans in ligand category. + allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first + bond seen per atom and discard the remaining on each atom. + bond_threshold: Euclidean distance of max allowed bond. + + Returns: + bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. + """ + if only_glycan_ligands: + allowed_res_names = list({ + *chemical_component_sets.GLYCAN_OTHER_LIGANDS, + *chemical_component_sets.GLYCAN_LINKING_LIGANDS, + }) + else: + allowed_res_names = None + if bond_threshold is None: + if only_glycan_ligands: + bond_threshold = BOND_THRESHOLD_GLYCANS_ANGSTROM + else: + bond_threshold = BOND_THRESHOLD_ALL_ANGSTROM + return get_bond_layout( + bond_threshold=bond_threshold, + struct=struct, + allowed_chain_types1=list(mmcif_names.POLYMER_CHAIN_TYPES), + allowed_chain_types2=list(mmcif_names.LIGAND_CHAIN_TYPES), + allowed_res_names=allowed_res_names, + allow_multiple_bonds_per_atom=allow_multiple_bonds_per_atom, + ) + + +def get_bond_layout( + bond_threshold: float = BOND_THRESHOLD_ALL_ANGSTROM, + *, + struct: structure.Structure, + allowed_chain_types1: Collection[str], + allowed_chain_types2: Collection[str], + include_bond_types: Collection[str] = ('covale',), + allowed_res_names: Collection[str] | None = None, + allow_multiple_bonds_per_atom: bool, +) -> atom_layout.AtomLayout: + """Get bond_layout for all bonds between two sets of chain types. + + There is a mask (all_mask) that runs through this script, and each bond pair + needs to maintain a True across all conditions in order to be preserved at the + end, otherwise the bond pair has invalidated a condition with a False and is + removed entirely. Note, we remove oxygen atom bonds as they are an edge case + that causes issues with scoring, due to multiple waters bonding with single + residues. + + Args: + bond_threshold: Maximum bond distance in Angstrom. + struct: Structure object to extract bonds from. + allowed_chain_types1: One end of the bonds must be an atom with one of these + chain types. + allowed_chain_types2: The other end of the bond must be an atom with one of + these chain types. + include_bond_types: Only include bonds with specified type e.g. hydrog, + metalc, covale, disulf. + allowed_res_names: Further restricts from chain_types. Either end of the + bonds must be an atom part of these res_names. If none all will be + accepted after chain and bond type filtering. + allow_multiple_bonds_per_atom: If not allowed, we greedily choose the first + bond seen per atom and discard the remaining on each atom. + + Returns: + bond_layout: AtomLayout object [num_bonds, 2] for the bond-defining atoms. + """ + if not struct.bonds: + return atom_layout.AtomLayout( + atom_name=np.empty((0, 2), dtype=object), + res_id=np.empty((0, 2), dtype=int), + res_name=np.empty((0, 2), dtype=object), + chain_id=np.empty((0, 2), dtype=object), + chain_type=np.empty((0, 2), dtype=object), + atom_element=np.empty((0, 2), dtype=object), + ) + from_atom_idxs, dest_atom_idxs = struct.bonds.get_atom_indices( + struct.atom_key + ) + from_atoms = _get_bond_atom_arrays(struct, from_atom_idxs) + dest_atoms = _get_bond_atom_arrays(struct, dest_atom_idxs) + # Chain type + chain_mask = np.logical_or( + np.logical_and( + np.isin( + from_atoms.chain_type, + allowed_chain_types1, + ), + np.isin( + dest_atoms.chain_type, + allowed_chain_types2, + ), + ), + np.logical_and( + np.isin( + from_atoms.chain_type, + allowed_chain_types2, + ), + np.isin( + dest_atoms.chain_type, + allowed_chain_types1, + ), + ), + ) + if allowed_res_names: + # Res type + res_mask = np.logical_or( + np.isin(from_atoms.res_name, allowed_res_names), + np.isin(dest_atoms.res_name, allowed_res_names), + ) + # All mask + all_mask = np.logical_and(chain_mask, res_mask) + else: + all_mask = chain_mask + # Bond type mask + type_mask = np.isin(struct.bonds.type, list(include_bond_types)) + np.logical_and(all_mask, type_mask, out=all_mask) + # Bond length check. Work in square length to avoid taking many square roots. + bond_length_squared = np.square(from_atoms.coords - dest_atoms.coords).sum( + axis=1 + ) + bond_threshold_squared = bond_threshold * bond_threshold + np.logical_and( + all_mask, bond_length_squared < bond_threshold_squared, out=all_mask + ) + # Inter-chain and inter-residue bonds for ligands + ligand_types = list(mmcif_names.LIGAND_CHAIN_TYPES) + is_ligand = np.logical_or( + np.isin( + from_atoms.chain_type, + ligand_types, + ), + np.isin( + dest_atoms.chain_type, + ligand_types, + ), + ) + res_id_differs = from_atoms.res_id != dest_atoms.res_id + chain_id_differs = from_atoms.chain_id != dest_atoms.chain_id + is_inter_res = np.logical_or(res_id_differs, chain_id_differs) + is_inter_ligand_res = np.logical_and(is_inter_res, is_ligand) + is_inter_chain_not_ligand = np.logical_and(chain_id_differs, ~is_ligand) + # If ligand then inter-res & inter-chain bonds, otherwise inter-chain only. + combined_allowed_bonds = np.logical_or( + is_inter_chain_not_ligand, is_inter_ligand_res + ) + np.logical_and(all_mask, combined_allowed_bonds, out=all_mask) + bond_layout = atom_layout.AtomLayout( + atom_name=np.stack( + [ + from_atoms.atom_name[all_mask], + dest_atoms.atom_name[all_mask], + ], + axis=1, + dtype=object, + ), + res_id=np.stack( + [from_atoms.res_id[all_mask], dest_atoms.res_id[all_mask]], + axis=1, + dtype=int, + ), + chain_id=np.stack( + [ + from_atoms.chain_id[all_mask], + dest_atoms.chain_id[all_mask], + ], + axis=1, + dtype=object, + ), + ) + if not allow_multiple_bonds_per_atom: + bond_layout = _remove_multi_bonds(bond_layout) + return atom_layout.fill_in_optional_fields( + bond_layout, + reference_atoms=atom_layout.atom_layout_from_structure(struct), + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/pipeline.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/pipeline.py new file mode 100644 index 000000000..1c08dccab --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/pipeline.py @@ -0,0 +1,446 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""The main featurizer.""" + +import bisect +from collections.abc import Sequence +import datetime +import itertools + +from absl import logging +from alphafold3.common import base_config +from alphafold3.common import folding_input +from alphafold3.constants import chemical_components +from alphafold3.model import feat_batch +from alphafold3.model import features +from alphafold3.model.pipeline import inter_chain_bonds +from alphafold3.model.pipeline import structure_cleaning +from alphafold3.structure import chemical_components as struc_chem_comps +import numpy as np +from alphafold3.common.folding_input import Template + + +_DETERMINISTIC_FRAMES_RANDOM_SEED = 12312837 + + +def calculate_bucket_size( + num_tokens: int, buckets: Sequence[int] | None +) -> int: + """Calculates the bucket size to pad the data to.""" + if buckets is None: + return num_tokens + + if not buckets: + raise ValueError('Buckets must be non-empty.') + + if not all(prev < curr for prev, curr in itertools.pairwise(buckets)): + raise ValueError( + f'Buckets must be in strictly increasing order. Got {buckets=}.' + ) + + bucket_idx = bisect.bisect_left(buckets, num_tokens) + + if bucket_idx == len(buckets): + logging.warning( + 'Creating a new bucket of size %d since the input has more tokens than' + ' the largest bucket size %d. This may trigger a re-compilation of the' + ' model. Consider additional large bucket sizes to avoid excessive' + ' re-compilation.', + num_tokens, + buckets[-1], + ) + return num_tokens + + return buckets[bucket_idx] + + +class NanDataError(Exception): + """Raised if the data pipeline produces data containing nans.""" + + +class TotalNumResOutOfRangeError(Exception): + """Raised if total number of residues for all chains outside allowed range.""" + + +class MmcifNumChainsError(Exception): + """Raised if the mmcif file contains too many / too few chains.""" + + +class WholePdbPipeline: + """Processes an entire mmcif entity and merges the content.""" + + class Config(base_config.BaseConfig): + """Configuration object for `WholePdbPipeline`. + + Properties: + max_atoms_per_token: number of atom slots in one token (was called + num_dense, and semi-hardcoded to 24 before) + pad_num_chains: Size to pad NUM_CHAINS feature dimensions to, only for + protein chains. + buckets: Bucket sizes to pad the data to, to avoid excessive + re-compilation of the model. If None, calculate the appropriate bucket + size from the number of tokens. If not None, must be a sequence of at + least one integer, in strictly increasing order. Will raise an error if + the number of tokens is more than the largest bucket size. + max_total_residues: Any mmCIF with more total residues will be rejected. + If none, then no limit is applied. + min_total_residues: Any mmCIF with less total residues will be rejected. + msa_crop_size: Maximum size of MSA to take across all chains. + max_template_date: Optional max template date to prevent data leakage in + validation. + max_templates: The maximum number of templates to send through the network + set to 0 to switch off templates. + filter_clashes: If true then will remove clashing chains. + filter_crystal_aids: If true ligands in the cryal aid list are removed. + max_paired_sequence_per_species: The maximum number of sequences per + species that will be used for MSA pairing. + drop_ligand_leaving_atoms: Flag for handling leaving atoms for ligands. + intra_ligand_ptm_bonds: Whether to embed intra ligand covalent bond graph. + average_num_atoms_per_token: Target average number of atoms per token to + compute the padding size for flat atoms. + atom_cross_att_queries_subset_size: queries subset size in atom cross + attention + atom_cross_att_keys_subset_size: keys subset size in atom cross attention + flatten_non_standard_residues: Whether to expand non-standard polymer + residues into flat-atom format. + remove_nonsymmetric_bonds: Whether to remove nonsymmetric bonds from + symmetric polymer chains. + deterministic_frames: Whether to use fixed-seed reference positions to + construct deterministic frames. + """ + + max_atoms_per_token: int = 24 + pad_num_chains: int = 1000 + buckets: list[int] | None = None + max_total_residues: int | None = None + min_total_residues: int | None = None + msa_crop_size: int = 16384 + max_template_date: datetime.date | None = None + max_templates: int = 4 + filter_clashes: bool = False + filter_crystal_aids: bool = False + max_paired_sequence_per_species: int = 600 + drop_ligand_leaving_atoms: bool = True + intra_ligand_ptm_bonds: bool = True + average_num_atoms_per_token: int = 24 + atom_cross_att_queries_subset_size: int = 32 + atom_cross_att_keys_subset_size: int = 128 + flatten_non_standard_residues: bool = True + remove_nonsymmetric_bonds: bool = False + deterministic_frames: bool = True + + def __init__( + self, + *, + config: Config, + ): + """Init WholePdb. + + Args: + config: Pipeline configuration. + """ + self._config = config + + def process_item( + self, + fold_input: folding_input.Input, + random_state: np.random.RandomState, + ccd: chemical_components.Ccd, + random_seed: int | None = None, + ) -> features.BatchDict: + """Takes requests from in_queue, adds (key, serialized ex) to out_queue.""" + if random_seed is None: + random_seed = random_state.randint(2**31) + + random_state = np.random.RandomState(seed=random_seed) + + logging_name = f'{fold_input.name}, random_seed={random_seed}' + logging.info('processing %s', logging_name) + struct = fold_input.to_structure(ccd=ccd) + + # Clean structure. + cleaned_struc, cleaning_metadata = structure_cleaning.clean_structure( + struct, + ccd=ccd, + drop_non_standard_atoms=True, + drop_missing_sequence=True, + filter_clashes=self._config.filter_clashes, + filter_crystal_aids=self._config.filter_crystal_aids, + filter_waters=True, + filter_hydrogens=True, + filter_leaving_atoms=self._config.drop_ligand_leaving_atoms, + only_glycan_ligands_for_leaving_atoms=True, + covalent_bonds_only=True, + remove_polymer_polymer_bonds=True, + remove_bad_bonds=True, + remove_nonsymmetric_bonds=self._config.remove_nonsymmetric_bonds, + ) + + num_clashing_chains_removed = cleaning_metadata[ + 'num_clashing_chains_removed' + ] + + if num_clashing_chains_removed: + logging.info( + 'Removed %d clashing chains from %s', + num_clashing_chains_removed, + logging_name, + ) + + # No chains after fixes + # if cleaned_struc.num_chains == 0: + # raise MmcifNumChainsError(f'{logging_name}: No chains in structure!') + + polymer_ligand_bonds, ligand_ligand_bonds = ( + inter_chain_bonds.get_polymer_ligand_and_ligand_ligand_bonds( + cleaned_struc, + only_glycan_ligands=False, + allow_multiple_bonds_per_atom=True, + ) + ) + + # If empty replace with None as this causes errors downstream. + if ligand_ligand_bonds and not ligand_ligand_bonds.atom_name.size: + ligand_ligand_bonds = None + if polymer_ligand_bonds and not polymer_ligand_bonds.atom_name.size: + polymer_ligand_bonds = None + + # Create the flat output AtomLayout + empty_output_struc, flat_output_layout = ( + structure_cleaning.create_empty_output_struct_and_layout( + struct=cleaned_struc, + ccd=ccd, + polymer_ligand_bonds=polymer_ligand_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + drop_ligand_leaving_atoms=self._config.drop_ligand_leaving_atoms, + ) + ) + + # Select the tokens for Evoformer. + # Each token (e.g. a residue) is encoded as one representative atom. This + # is flexible enough to allow the 1-token-per-atom ligand representation + # in the future. + all_tokens, all_token_atoms_layout, standard_token_idxs = ( + features.tokenizer( + flat_output_layout, + ccd=ccd, + max_atoms_per_token=self._config.max_atoms_per_token, + flatten_non_standard_residues=self._config.flatten_non_standard_residues, + logging_name=logging_name, + ) + ) + total_tokens = len(all_tokens.atom_name) + if ( + self._config.max_total_residues + and total_tokens > self._config.max_total_residues + ): + raise TotalNumResOutOfRangeError( + 'Total Number of Residues > max_total_residues: ' + f'({total_tokens} > {self._config.max_total_residues})' + ) + + if ( + self._config.min_total_residues + and total_tokens < self._config.min_total_residues + ): + raise TotalNumResOutOfRangeError( + 'Total Number of Residues < min_total_residues: ' + f'({total_tokens} < {self._config.min_total_residues})' + ) + + logging.info( + 'Calculating bucket size for input with %d tokens.', total_tokens + ) + padded_token_length = calculate_bucket_size( + total_tokens, self._config.buckets + ) + logging.info( + 'Got bucket size %d for input with %d tokens, resulting in %d padded' + ' tokens.', + padded_token_length, + total_tokens, + padded_token_length - total_tokens, + ) + + # Padding shapes for all features. + num_atoms = padded_token_length * self._config.average_num_atoms_per_token + # Round up to next multiple of subset size. + num_atoms = int( + np.ceil(num_atoms / self._config.atom_cross_att_queries_subset_size) + * self._config.atom_cross_att_queries_subset_size + ) + padding_shapes = features.PaddingShapes( + num_tokens=padded_token_length, + msa_size=self._config.msa_crop_size, + num_chains=self._config.pad_num_chains, + num_templates=self._config.max_templates, + num_atoms=num_atoms, + ) + + # Create the atom layouts for flat atom cross attention + batch_atom_cross_att = features.AtomCrossAtt.compute_features( + all_token_atoms_layout=all_token_atoms_layout, + queries_subset_size=self._config.atom_cross_att_queries_subset_size, + keys_subset_size=self._config.atom_cross_att_keys_subset_size, + padding_shapes=padding_shapes, + ) + + # Extract per-token features + batch_token_features = features.TokenFeatures.compute_features( + all_tokens=all_tokens, + padding_shapes=padding_shapes, + ) + + # Create reference structure features + chemical_components_data = struc_chem_comps.populate_missing_ccd_data( + ccd=ccd, + chemical_components_data=cleaned_struc.chemical_components_data, + populate_pdbx_smiles=True, + ) + + # Add smiles info to empty_output_struc. + empty_output_struc = empty_output_struc.copy_and_update_globals( + chemical_components_data=chemical_components_data + ) + # Create layouts and store structures for model output conversion. + batch_convert_model_output = features.ConvertModelOutput.compute_features( + all_token_atoms_layout=all_token_atoms_layout, + padding_shapes=padding_shapes, + cleaned_struc=cleaned_struc, + flat_output_layout=flat_output_layout, + empty_output_struc=empty_output_struc, + polymer_ligand_bonds=polymer_ligand_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + ) + + # Create the PredictedStructureInfo + batch_predicted_structure_info = ( + features.PredictedStructureInfo.compute_features( + all_tokens=all_tokens, + all_token_atoms_layout=all_token_atoms_layout, + padding_shapes=padding_shapes, + ) + ) + + # Create MSA features + batch_msa = features.MSA.compute_features( + all_tokens=all_tokens, + standard_token_idxs=standard_token_idxs, + padding_shapes=padding_shapes, + fold_input=fold_input, + logging_name=logging_name, + max_paired_sequence_per_species=self._config.max_paired_sequence_per_species, + ) + + # Create template features + batch_templates = features.Templates.compute_features( + all_tokens=all_tokens, + standard_token_idxs=standard_token_idxs, + padding_shapes=padding_shapes, + fold_input=fold_input, + max_templates=self._config.max_templates, + logging_name=logging_name, + ) + + ref_max_modified_date = self._config.max_template_date + batch_ref_structure, ligand_ligand_bonds = ( + features.RefStructure.compute_features( + all_token_atoms_layout=all_token_atoms_layout, + ccd=ccd, + padding_shapes=padding_shapes, + chemical_components_data=chemical_components_data, + random_state=random_state, + ref_max_modified_date=ref_max_modified_date, + intra_ligand_ptm_bonds=self._config.intra_ligand_ptm_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + ) + ) + deterministic_ref_structure = None + if self._config.deterministic_frames: + deterministic_ref_structure, _ = features.RefStructure.compute_features( + all_token_atoms_layout=all_token_atoms_layout, + ccd=ccd, + padding_shapes=padding_shapes, + chemical_components_data=chemical_components_data, + random_state=( + np.random.RandomState(_DETERMINISTIC_FRAMES_RANDOM_SEED) + ), + ref_max_modified_date=ref_max_modified_date, + intra_ligand_ptm_bonds=self._config.intra_ligand_ptm_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + ) + + # Create ligand-polymer bond features. + polymer_ligand_bond_info = features.PolymerLigandBondInfo.compute_features( + all_tokens=all_tokens, + all_token_atoms_layout=all_token_atoms_layout, + bond_layout=polymer_ligand_bonds, + padding_shapes=padding_shapes, + ) + # Create ligand-ligand bond features. + ligand_ligand_bond_info = features.LigandLigandBondInfo.compute_features( + all_tokens, + ligand_ligand_bonds, + padding_shapes, + ) + + # Create the Pseudo-beta layout for distogram head and distance error head. + batch_pseudo_beta_info = features.PseudoBetaInfo.compute_features( + all_token_atoms_layout=all_token_atoms_layout, + ccd=ccd, + padding_shapes=padding_shapes, + logging_name=logging_name, + ) + + # Frame construction. + batch_frames = features.Frames.compute_features( + all_tokens=all_tokens, + all_token_atoms_layout=all_token_atoms_layout, + ref_structure=( + deterministic_ref_structure + if self._config.deterministic_frames + else batch_ref_structure + ), + padding_shapes=padding_shapes, + ) + + # Assemble the Batch object. + batch = feat_batch.Batch( + msa=batch_msa, + templates=batch_templates, + token_features=batch_token_features, + ref_structure=batch_ref_structure, + predicted_structure_info=batch_predicted_structure_info, + polymer_ligand_bond_info=polymer_ligand_bond_info, + ligand_ligand_bond_info=ligand_ligand_bond_info, + pseudo_beta_info=batch_pseudo_beta_info, + atom_cross_att=batch_atom_cross_att, + convert_model_output=batch_convert_model_output, + frames=batch_frames, + ) + + np_example = batch.as_data_dict() + if 'num_iter_recycling' in np_example: + del np_example['num_iter_recycling'] # that does not belong here + + for name, value in np_example.items(): + if ( + value.dtype.kind not in {'U', 'S'} + and value.dtype.name != 'object' + and np.isnan(np.sum(value)) + ): + raise NanDataError( + 'The output of the data pipeline contained nans. ' + f'nan feature: {name}, fold input name: {fold_input.name}, ' + f'random_seed {random_seed}' + ) + + return np_example diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/structure_cleaning.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/structure_cleaning.py new file mode 100644 index 000000000..0ca2505a3 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/structure_cleaning.py @@ -0,0 +1,370 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Prepare PDB structure for training or inference.""" + +from typing import Any +import numpy as np +from absl import logging +from alphafold3 import structure +from alphafold3.constants import chemical_component_sets +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.model.atom_layout import atom_layout +from alphafold3.model.pipeline import inter_chain_bonds +from alphafold3.model.scoring import covalent_bond_cleaning +from alphafold3.structure import sterics + + +def _get_leaving_atom_mask( + struct: structure.Structure, + polymer_ligand_bonds: atom_layout.AtomLayout | None, + ligand_ligand_bonds: atom_layout.AtomLayout | None, + chain_id: str, + chain_type: str, + res_id: int, + res_name: str, +) -> np.ndarray: + """Updates a drop_leaving_atoms mask with new leaving atom locations.""" + bonded_atoms = atom_layout.get_bonded_atoms( + polymer_ligand_bonds, + ligand_ligand_bonds, + res_id, + chain_id, + ) + # Connect the amino-acids, i.e. remove OXT, HXT and H2. + drop_atoms = atom_layout.get_link_drop_atoms( + res_name=res_name, + chain_type=chain_type, + is_start_terminus=False, + is_end_terminus=False, + bonded_atoms=bonded_atoms, + drop_ligand_leaving_atoms=True, + ) + # Default mask where everything is false, which equates to being kept. + drop_atom_filter_atoms = struct.chain_id != struct.chain_id + for drop_atom in drop_atoms: + drop_atom_filter_atom = np.logical_and( + np.logical_and( + struct.atom_name == drop_atom, + struct.chain_id == chain_id, + ), + struct.res_id == res_id, + ) + drop_atom_filter_atoms = np.logical_or( + drop_atom_filter_atoms, drop_atom_filter_atom + ) + return drop_atom_filter_atoms + + +def clean_structure( + struct: structure.Structure, + ccd: chemical_components.Ccd, + *, + drop_missing_sequence: bool, + filter_clashes: bool, + drop_non_standard_atoms: bool, + filter_crystal_aids: bool, + filter_waters: bool, + filter_hydrogens: bool, + filter_leaving_atoms: bool, + only_glycan_ligands_for_leaving_atoms: bool, + covalent_bonds_only: bool, + remove_polymer_polymer_bonds: bool, + remove_bad_bonds: bool, + remove_nonsymmetric_bonds: bool, +) -> tuple[structure.Structure, dict[str, Any]]: + """Cleans structure. + + Args: + struct: Structure to clean. + ccd: The chemical components dictionary. + drop_missing_sequence: Whether to drop chains without specified sequences. + filter_clashes: Whether to drop clashing chains. + drop_non_standard_atoms: Whether to drop non CCD standard atoms. + filter_crystal_aids: Whether to drop ligands in the crystal aid set. + filter_waters: Whether to drop water chains. + filter_hydrogens: Whether to drop hyrdogen atoms. + filter_leaving_atoms: Whether to drop leaving atoms based on heuristics. + only_glycan_ligands_for_leaving_atoms: Whether to only include glycan + ligands when filtering leaving atoms. + covalent_bonds_only: Only include covalent bonds. + remove_polymer_polymer_bonds: Remove polymer-polymer bonds. + remove_bad_bonds: Whether to remove badly bonded ligands. + remove_nonsymmetric_bonds: Whether to remove nonsymmetric polymer-ligand + bonds from symmetric polymer chains. + + Returns: + Tuple of structure and metadata dict. The metadata dict has + information about what was cleaned from the original. + """ + + metadata = {} + # Crop crystallization aids. + if ( + filter_crystal_aids + and struct.structure_method in mmcif_names.CRYSTALLIZATION_METHODS + ): + struct = struct.filter_out( + res_name=chemical_component_sets.COMMON_CRYSTALLIZATION_AIDS + ) + + # Drop chains without specified sequences. + if drop_missing_sequence: + chains_with_unk_sequence = struct.find_chains_with_unknown_sequence() + num_with_unk_sequence = len(chains_with_unk_sequence) + if chains_with_unk_sequence: + struct = struct.filter_out(chain_id=chains_with_unk_sequence) + else: + num_with_unk_sequence = 0 + metadata['num_with_unk_sequence'] = num_with_unk_sequence + + # Remove intersecting chains. + if filter_clashes and struct.num_chains > 1: + clashing_chains = sterics.find_clashing_chains(struct) + if clashing_chains: + struct = struct.filter_out(chain_id=clashing_chains) + else: + clashing_chains = [] + metadata['num_clashing_chains_removed'] = len(clashing_chains) + metadata['chains_removed'] = clashing_chains + + # Drop non-standard atoms + if drop_non_standard_atoms: + struct = struct.drop_non_standard_atoms( + ccd=ccd, drop_unk=False, drop_non_ccd=False + ) + + # Sort chains in "reverse-spreadsheet" order. + struct = struct.with_sorted_chains + + if filter_hydrogens: + struct = struct.without_hydrogen() + + if filter_waters: + struct = struct.filter_out(chain_type=mmcif_names.WATER) + + if filter_leaving_atoms: + drop_leaving_atoms_all = struct.chain_id != struct.chain_id + polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds( + struct, + only_glycan_ligands=only_glycan_ligands_for_leaving_atoms, + ) + ligand_ligand_bonds = inter_chain_bonds.get_ligand_ligand_bonds( + struct, + only_glycan_ligands=only_glycan_ligands_for_leaving_atoms, + ) + all_glycans = { + *chemical_component_sets.GLYCAN_OTHER_LIGANDS, + *chemical_component_sets.GLYCAN_LINKING_LIGANDS, + } + # If only glycan ligands and no O1 atoms, we can do parallel drop. + if ( + only_glycan_ligands_for_leaving_atoms + and (not (ligand_ligand_bonds.atom_name == 'O1').any()) + and (not (polymer_ligand_bonds.atom_name == 'O1').any()) + ): + drop_leaving_atoms_all = np.logical_and( + np.isin(struct.atom_name, 'O1'), + np.isin(struct.res_name, list(all_glycans)), + ) + else: + substruct = struct.group_by_residue + glycan_mask = np.isin(substruct.res_name, list(all_glycans)) + substruct = substruct.filter(glycan_mask) + # We need to iterate over all glycan residues for this. + for res in substruct.iter_residues(): + # Only need to do drop leaving atoms for glycans depending on bonds. + if (res_name := res['res_name']) in all_glycans: + drop_atom_filter = _get_leaving_atom_mask( + struct=struct, + polymer_ligand_bonds=polymer_ligand_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + chain_id=res['chain_id'], + chain_type=res['chain_type'], + res_id=res['res_id'], + res_name=res_name, + ) + drop_leaving_atoms_all = np.logical_or( + drop_leaving_atoms_all, drop_atom_filter + ) + + num_atoms_before = struct.num_atoms + struct = struct.filter_out(drop_leaving_atoms_all) + num_atoms_after = struct.num_atoms + + if num_atoms_before > num_atoms_after: + logging.error( + 'Dropped %s atoms from GT struct: chain_id %s res_id %s res_name %s', + num_atoms_before - num_atoms_after, + struct.chain_id, + struct.res_id, + struct.res_name, + ) + + # Can filter by bond type without having to iterate over bonds. + if struct.bonds and covalent_bonds_only: + is_covalent = np.isin(struct.bonds.type, ['covale']) + if sum(is_covalent) > 0: + new_bonds = struct.bonds[is_covalent] + else: + new_bonds = structture.Bonds.make_empty() + struct = struct.copy_and_update(bonds=new_bonds) + + # Other bond filters require iterating over individual bonds. + if struct.bonds and (remove_bad_bonds or remove_polymer_polymer_bonds): + include_bond = [] + num_pp_bonds = 0 + num_bad_bonds = 0 + for bond in struct.iter_bonds(): + dest_atom = bond.dest_atom + from_atom = bond.from_atom + if remove_polymer_polymer_bonds: + if ( + from_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES + and dest_atom['chain_type'] in mmcif_names.POLYMER_CHAIN_TYPES + ): + num_pp_bonds += 1 + include_bond.append(False) + continue + if remove_bad_bonds: + dest_coords = np.array( + [dest_atom['atom_x'], dest_atom['atom_y'], dest_atom['atom_z']] + ) + from_coords = np.array( + [from_atom['atom_x'], from_atom['atom_y'], from_atom['atom_z']] + ) + squared_dist = np.sum(np.square(dest_coords - from_coords)) + squared_threshold = 2.4 * 2.4 + if squared_dist > squared_threshold: + num_bad_bonds += 1 + include_bond.append(False) + continue + include_bond.append(True) + if sum(include_bond) < len(struct.bonds): + logging.info( + 'Reducing number of bonds for %s from %s to %s, of which %s are' + ' polymer-polymer bonds and %s are bad bonds.', + struct.name, + len(struct.bonds), + sum(include_bond), + num_pp_bonds, + num_bad_bonds, + ) + if sum(include_bond) > 0: + # Need to index bonds with bond keys or arrays of bools with same length + # as num bonds. In this case, we use array of bools (as elsewhere in the + # cleaning code). + new_bonds = struct.bonds[np.array(include_bond, dtype=bool)] + else: + new_bonds = structure.Bonds.make_empty() + struct = struct.copy_and_update(bonds=new_bonds) + + if struct.bonds and remove_nonsymmetric_bonds: + # Check for asymmetric polymer-ligand bonds and remove if these exist. + polymer_ligand_bonds = inter_chain_bonds.get_polymer_ligand_bonds( + struct, + only_glycan_ligands=False, + ) + if polymer_ligand_bonds: + if covalent_bond_cleaning.has_nonsymmetric_bonds_on_symmetric_polymer_chains( + struct, polymer_ligand_bonds + ): + from_atom_idxs, dest_atom_idxs = struct.bonds.get_atom_indices( + struct.atom_key + ) + poly_chain_types = list(mmcif_names.POLYMER_CHAIN_TYPES) + is_polymer_bond = np.logical_or( + np.isin( + struct.chain_type[from_atom_idxs], poly_chain_types), + np.isin( + struct.chain_type[dest_atom_idxs], poly_chain_types), + ) + struct = struct.copy_and_update( + bonds=struct.bonds[~is_polymer_bond]) + + return struct, metadata + + +def create_empty_output_struct_and_layout( + struct: structure.Structure, + ccd: chemical_components.Ccd, + *, + with_hydrogens: bool = False, + skip_unk: bool = False, + polymer_ligand_bonds: atom_layout.AtomLayout | None = None, + ligand_ligand_bonds: atom_layout.AtomLayout | None = None, + drop_ligand_leaving_atoms: bool = False, +) -> tuple[structure.Structure, atom_layout.AtomLayout]: + """Make zero-coordinate structure from all physical residues. + + Args: + struct: Structure object. + ccd: The chemical components dictionary. + with_hydrogens: Whether to keep hydrogen atoms in structure. + skip_unk: Whether to remove unknown residues from structure. + polymer_ligand_bonds: Bond information for polymer-ligand pairs. + ligand_ligand_bonds: Bond information for ligand-ligand pairs. + drop_ligand_leaving_atoms: Flag for handling leaving atoms for ligands. + + Returns: + Tuple of structure with all bonds, physical residues and coordinates set to + 0 and a flat atom layout of empty structure. + """ + bonded_atom_pairs = [] + if polymer_ligand_bonds: + for chain_ids, res_ids, atom_names in zip( + polymer_ligand_bonds.chain_id, + polymer_ligand_bonds.res_id, + polymer_ligand_bonds.atom_name, + strict=True, + ): + bonded_atom_pairs.append(( + (chain_ids[0], res_ids[0], atom_names[0]), + (chain_ids[1], res_ids[1], atom_names[1]), + )) + if ligand_ligand_bonds: + for chain_ids, res_ids, atom_names in zip( + ligand_ligand_bonds.chain_id, + ligand_ligand_bonds.res_id, + ligand_ligand_bonds.atom_name, + strict=True, + ): + bonded_atom_pairs.append(( + (chain_ids[0], res_ids[0], atom_names[0]), + (chain_ids[1], res_ids[1], atom_names[1]), + )) + residues = atom_layout.residues_from_structure( + struct, include_missing_residues=True + ) + + flat_output_layout = atom_layout.make_flat_atom_layout( + residues, + ccd=ccd, + with_hydrogens=with_hydrogens, + skip_unk_residues=skip_unk, + polymer_ligand_bonds=polymer_ligand_bonds, + ligand_ligand_bonds=ligand_ligand_bonds, + drop_ligand_leaving_atoms=drop_ligand_leaving_atoms, + ) + + empty_output_struct = atom_layout.make_structure( + flat_layout=flat_output_layout, + atom_coords=np.zeros((flat_output_layout.shape[0], 3)), + name=struct.name, + atom_b_factors=None, + all_physical_residues=residues, + ) + if bonded_atom_pairs: + empty_output_struct = empty_output_struct.add_bonds( + bonded_atom_pairs, bond_type=mmcif_names.COVALENT_BOND + ) + + return empty_output_struct, flat_output_layout diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py new file mode 100644 index 000000000..c56ed20fc --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py @@ -0,0 +1,114 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Post-processing utilities for AlphaFold inference results.""" + +import dataclasses +import datetime +import os + +# from alphafold3 import version +from alphafold3.model import confidence_types +from alphafold3.model import mmcif_metadata +from alphafold3.model.components import base_model +import numpy as np + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class ProcessedInferenceResult: + """Stores attributes of a processed inference result. + + Attributes: + cif: CIF file containing an inference result. + mean_confidence_1d: Mean 1D confidence calculated from confidence_1d. + ranking_score: Ranking score extracted from CIF metadata. + structure_confidence_summary_json: Content of JSON file with structure + confidences summary calculated from CIF file. + structure_full_data_json: Content of JSON file with structure full + confidences calculated from CIF file. + model_id: Identifier of the model that produced the inference result. + """ + + cif: bytes + mean_confidence_1d: float + ranking_score: float + structure_confidence_summary_json: bytes + structure_full_data_json: bytes + model_id: bytes + + +def post_process_inference_result( + inference_result: base_model.InferenceResult, +) -> ProcessedInferenceResult: + """Returns cif, confidence_1d_json, confidence_2d_json, mean_confidence_1d, and ranking confidence.""" + + # Add mmCIF metadata fields. + timestamp = datetime.datetime.now().isoformat(sep=' ', timespec='seconds') + cif_with_metadata = mmcif_metadata.add_metadata_to_mmcif( + old_cif=inference_result.predicted_structure.to_mmcif_dict(), + # version=f'{version.__version__} @ {timestamp}', + # version=None, + model_id=inference_result.model_id, + ) + cif = mmcif_metadata.add_legal_comment(cif_with_metadata.to_string()) + cif = cif.encode('utf-8') + confidence_1d = confidence_types.AtomConfidence.from_inference_result( + inference_result + ) + mean_confidence_1d = np.mean(confidence_1d.confidence) + structure_confidence_summary_json = ( + confidence_types.StructureConfidenceSummary.from_inference_result( + inference_result + ) + .to_json() + .encode('utf-8') + ) + structure_full_data_json = ( + confidence_types.StructureConfidenceFull.from_inference_result( + inference_result + ) + .to_json() + .encode('utf-8') + ) + return ProcessedInferenceResult( + cif=cif, + mean_confidence_1d=mean_confidence_1d, + ranking_score=float(inference_result.metadata['ranking_score']), + structure_confidence_summary_json=structure_confidence_summary_json, + structure_full_data_json=structure_full_data_json, + model_id=inference_result.model_id, + ) + + +def write_output( + inference_result: base_model.InferenceResult, + output_dir: os.PathLike[str] | str, + terms_of_use: str | None = None, + name: str | None = None, +) -> None: + """Writes processed inference result to a directory.""" + processed_result = post_process_inference_result(inference_result) + + prefix = f'{name}_' if name is not None else '' + + with open(os.path.join(output_dir, f'{prefix}model.cif'), 'wb') as f: + f.write(processed_result.cif) + + with open( + os.path.join(output_dir, f'{prefix}summary_confidences.json'), 'wb' + ) as f: + f.write(processed_result.structure_confidence_summary_json) + + with open(os.path.join(output_dir, f'{prefix}confidences.json'), 'wb') as f: + f.write(processed_result.structure_full_data_json) + + if terms_of_use is not None: + with open(os.path.join(output_dir, 'TERMS_OF_USE.md'), 'wt') as f: + f.write(terms_of_use) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/protein_data_processing.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/protein_data_processing.py new file mode 100644 index 000000000..195db4c27 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/protein_data_processing.py @@ -0,0 +1,128 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Process Structure Data.""" + +from alphafold3.constants import atom_types +from alphafold3.constants import residue_names +from alphafold3.constants import side_chains +import numpy as np + + +NUM_DENSE = atom_types.DENSE_ATOM_NUM +NUM_AA = len(residue_names.PROTEIN_TYPES) +NUM_AA_WITH_UNK_AND_GAP = len( + residue_names.PROTEIN_TYPES_ONE_LETTER_WITH_UNKNOWN_AND_GAP +) +NUM_RESTYPES_WITH_UNK_AND_GAP = ( + residue_names.POLYMER_TYPES_NUM_WITH_UNKNOWN_AND_GAP +) + + +def _make_restype_rigidgroup_dense_atom_idx(): + """Create Mapping from rigid_groups to dense_atom indices.""" + # Create an array with the atom names. + # shape (num_restypes, num_rigidgroups, 3_atoms): + # (31, 8, 3) + base_atom_indices = np.zeros( + (NUM_RESTYPES_WITH_UNK_AND_GAP, 8, 3), dtype=np.int32 + ) + + # 4,5,6,7: 'chi1,2,3,4-group' + for restype, restype_letter in enumerate( + residue_names.PROTEIN_TYPES_ONE_LETTER + ): + resname = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter] + + dense_atom_names = atom_types.ATOM14[resname] + # 0: backbone frame + base_atom_indices[restype, 0, :] = [ + dense_atom_names.index(atom) for atom in ['C', 'CA', 'N'] + ] + + # 3: 'psi-group' + base_atom_indices[restype, 3, :] = [ + dense_atom_names.index(atom) for atom in ['CA', 'C', 'O'] + ] + for chi_idx in range(4): + if side_chains.CHI_ANGLES_MASK[restype][chi_idx]: + atom_names = side_chains.CHI_ANGLES_ATOMS[resname][chi_idx] + base_atom_indices[restype, chi_idx + 4, :] = [ + dense_atom_names.index(atom) for atom in atom_names[1:] + ] + dense_atom_names = atom_types.DENSE_ATOM['A'] + nucleic_rigid_atoms = [ + dense_atom_names.index(atom) for atom in ["C1'", "C3'", "C4'"] + ] + for nanum, _ in enumerate(residue_names.NUCLEIC_TYPES): + # 0: backbone frame only. + # we have aa + unk + gap, so we want to start after those + resnum = nanum + NUM_AA_WITH_UNK_AND_GAP + base_atom_indices[resnum, 0, :] = nucleic_rigid_atoms + + return base_atom_indices + + +RESTYPE_RIGIDGROUP_DENSE_ATOM_IDX = _make_restype_rigidgroup_dense_atom_idx() + + +def _make_restype_pseudobeta_idx(): + """Returns indices of residue's pseudo-beta.""" + restype_pseudobeta_index = np.zeros( + (NUM_RESTYPES_WITH_UNK_AND_GAP,), dtype=np.int32 + ) + for restype, restype_letter in enumerate( + residue_names.PROTEIN_TYPES_ONE_LETTER + ): + restype_name = residue_names.PROTEIN_COMMON_ONE_TO_THREE[restype_letter] + atom_names = list(atom_types.ATOM14[restype_name]) + if restype_name in {'GLY'}: + restype_pseudobeta_index[restype] = atom_names.index('CA') + else: + restype_pseudobeta_index[restype] = atom_names.index('CB') + for nanum, resname in enumerate(residue_names.NUCLEIC_TYPES): + atom_names = list(atom_types.DENSE_ATOM[resname]) + # 0: backbone frame only. + # we have aa + unk , so we want to start after those + restype = nanum + NUM_AA_WITH_UNK_AND_GAP + if resname in {'A', 'G', 'DA', 'DG'}: + restype_pseudobeta_index[restype] = atom_names.index('C4') + else: + restype_pseudobeta_index[restype] = atom_names.index('C2') + return restype_pseudobeta_index + + +RESTYPE_PSEUDOBETA_INDEX = _make_restype_pseudobeta_idx() + + +def _make_aatype_dense_atom_to_atom37(): + """Map from dense_atom to atom37 per residue type.""" + restype_dense_atom_to_atom37 = [ + ] # mapping (restype, dense_atom) --> atom37 + for rt in residue_names.PROTEIN_TYPES_ONE_LETTER: + atom_names = list( + atom_types.ATOM14_PADDED[residue_names.PROTEIN_COMMON_ONE_TO_THREE[rt]] + ) + atom_names.extend([''] * (NUM_DENSE - len(atom_names))) + restype_dense_atom_to_atom37.append( + [(atom_types.ATOM37_ORDER[name] if name else 0) + for name in atom_names] + ) + # Add dummy mapping for restype 'UNK', '-' (gap), and nucleics [but not DN]. + for _ in range(2 + len(residue_names.NUCLEIC_TYPES_WITH_UNKNOWN)): + restype_dense_atom_to_atom37.append([0] * NUM_DENSE) + + restype_dense_atom_to_atom37 = np.array( + restype_dense_atom_to_atom37, dtype=np.int32 + ) + return restype_dense_atom_to_atom37 + + +PROTEIN_AATYPE_DENSE_ATOM_TO_ATOM37 = _make_aatype_dense_atom_to_atom37() diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/alignment.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/alignment.py new file mode 100644 index 000000000..a4a8d225e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/alignment.py @@ -0,0 +1,146 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Alignment based metrics.""" + +import numpy as np + + +def transform_ls( + x: np.ndarray, + b: np.ndarray, + *, + allow_reflection: bool = False, +) -> np.ndarray: + """Find the least squares best fit rotation between two sets of N points. + + Solve Ax = b for A. Where A is the transform rotating x^T into b^T. + + Args: + x: NxD numpy array of coordinates. Usually dimension D is 3. + b: NxD numpy array of coordinates. Usually dimension D is 3. + allow_reflection: Whether the returned transformation can reflect as well as + rotate. + + Returns: + Matrix A transforming x into b, i.e. s.t. Ax^T = b^T. + """ + assert x.shape[1] >= b.shape[1] + assert b.shape[0] == x.shape[0], '%d, %d' % (b.shape[0], x.shape[0]) + # First postmultiply by x.; + # Axx^t = b x^t + bxt = np.dot(b.transpose(), x) / b.shape[0] + + u, _, v = np.linalg.svd(bxt) + + r = np.dot(u, v) + if not allow_reflection: + flip = np.ones((v.shape[1], 1)) + flip[v.shape[1] - 1, 0] = np.sign(np.linalg.det(r)) + r = np.dot(u, v * flip) + + return r + + +def align( + *, + x: np.ndarray, + y: np.ndarray, + x_indices: np.ndarray, + y_indices: np.ndarray, +) -> np.ndarray: + """Align x to y considering only included_idxs. + + Args: + x: NxD np array of coordinates. + y: NxD np array of coordinates. + x_indices: An np array of indices for `x` that will be used in the + alignment. Must be of the same length as `y_included_idxs`. + y_indices: An np array of indices for `y` that will be used in the + alignment. Must be of the same length as `x_included_idxs`. + + Returns: + NxD np array of points obtained by applying a rigid transformation to x. + These points are aligned to y and the alignment is the optimal alignment + over the points in included_idxs. + + Raises: + ValueError: If the number of included indices is not the same for both + input arrays. + """ + if len(x_indices) != len(y_indices): + raise ValueError( + 'Number of included indices must be the same for both input arrays,' + f' but got for x: {len(x_indices)}, and for y: {len(y_indices)}.' + ) + + x_mean = np.mean(x[x_indices, :], axis=0) + y_mean = np.mean(y[y_indices, :], axis=0) + + centered_x = x - x_mean + centered_y = y - y_mean + t = transform_ls(centered_x[x_indices, :], centered_y[y_indices, :]) + transformed_x = np.dot(centered_x, t.transpose()) + y_mean + + return transformed_x + + +def deviations_from_coords( + decoy_coords: np.ndarray, + gt_coords: np.ndarray, + align_idxs: np.ndarray | None = None, + include_idxs: np.ndarray | None = None, +) -> np.ndarray: + """Returns the raw per-atom deviations used in RMSD computation.""" + if decoy_coords.shape != gt_coords.shape: + raise ValueError( + 'decoy_coords.shape and gt_coords.shape must match.Found: %s and %s.' + % (decoy_coords.shape, gt_coords.shape) + ) + # Include and align all residues unless specified otherwise. + if include_idxs is None: + include_idxs = np.arange(decoy_coords.shape[0]) + if align_idxs is None: + align_idxs = include_idxs + aligned_decoy_coords = align( + x=decoy_coords, + y=gt_coords, + x_indices=align_idxs, + y_indices=align_idxs, + ) + deviations = np.linalg.norm( + aligned_decoy_coords[include_idxs] - gt_coords[include_idxs], axis=1 + ) + return deviations + + +def rmsd_from_coords( + decoy_coords: np.ndarray | str, + gt_coords: np.ndarray | str, + align_idxs: np.ndarray | None = None, + include_idxs: np.ndarray | None = None, +) -> float: + """Computes the *aligned* RMSD of two Mx3 np arrays of coordinates. + + Args: + decoy_coords: [M, 3] np array of decoy atom coordinates. + gt_coords: [M, 3] np array of gt atom coordinates. + align_idxs: [M] np array of indices specifying coordinates to align on. + Defaults to None, in which case all the include_idx (see after) are used. + include_idxs: [M] np array of indices specifying coordinates to score. + Defaults to None, in which case all indices are used for scoring. + + Returns: + rmsd value of the aligned decoy and gt coordinates. + """ + deviations = deviations_from_coords( + decoy_coords, gt_coords, align_idxs, include_idxs + ) + return np.sqrt(np.mean(np.square(deviations))) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/covalent_bond_cleaning.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/covalent_bond_cleaning.py new file mode 100644 index 000000000..abc38ce6e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/covalent_bond_cleaning.py @@ -0,0 +1,265 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Some methods to compute metrics for PTMs.""" + +import collections +from collections.abc import Mapping +import dataclasses +import numpy as np +from alphafold3 import structure +from alphafold3.constants import mmcif_names +from alphafold3.model.atom_layout import atom_layout + + +@dataclasses.dataclass(frozen=True) +class ResIdMapping: + old_res_ids: np.ndarray + new_res_ids: np.ndarray + + +def _count_symmetric_chains(struct: structure.Structure) -> Mapping[str, int]: + """Returns a dict with each chain ID and count.""" + chain_res_name_sequence_from_chain_id = struct.chain_res_name_sequence( + include_missing_residues=True, fix_non_standard_polymer_res=False + ) + counts_for_chain_res_name_sequence = collections.Counter( + chain_res_name_sequence_from_chain_id.values() + ) + chain_symmetric_count = {} + for chain_id, chain_res_name in chain_res_name_sequence_from_chain_id.items(): + chain_symmetric_count[chain_id] = counts_for_chain_res_name_sequence[ + chain_res_name + ] + return chain_symmetric_count + + +def has_nonsymmetric_bonds_on_symmetric_polymer_chains( + struct: structure.Structure, polymer_ligand_bonds: atom_layout.AtomLayout +) -> bool: + """Returns true if nonsymmetric bonds found on polymer chains.""" + try: + _get_polymer_dim(polymer_ligand_bonds) + except ValueError: + return True + if _has_non_polymer_ligand_ptm_bonds(polymer_ligand_bonds): + return True + if _has_multiple_polymers_bonded_to_one_ligand(polymer_ligand_bonds): + return True + combined_struct, _ = _combine_polymer_ligand_ptm_chains( + struct, polymer_ligand_bonds + ) + struct = struct.filter(chain_type=mmcif_names.POLYMER_CHAIN_TYPES) + combined_struct = combined_struct.filter( + chain_type=mmcif_names.POLYMER_CHAIN_TYPES + ) + return _count_symmetric_chains(struct) != _count_symmetric_chains( + combined_struc + ) + + +def _has_non_polymer_ligand_ptm_bonds( + polymer_ligand_bonds: atom_layout.AtomLayout, +): + """Checks if all bonds are between a polymer chain and a ligand chain type.""" + for start_chain_type, end_chain_type in polymer_ligand_bonds.chain_type: + if ( + start_chain_type in mmcif_names.POLYMER_CHAIN_TYPES + and end_chain_type in mmcif_names.LIGAND_CHAIN_TYPES + ): + continue + elif ( + start_chain_type in mmcif_names.LIGAND_CHAIN_TYPES + and end_chain_type in mmcif_names.POLYMER_CHAIN_TYPES + ): + continue + else: + return True + return False + + +def _combine_polymer_ligand_ptm_chains( + struct: structure.Structure, + polymer_ligand_bonds: atom_layout.AtomLayout, +) -> tuple[structure.Structure, dict[tuple[str, str], ResIdMapping]]: + """Combines the ptm polymer-ligand chains together. + + This will prevent them from being permuted away from each other when chains + are matched to the ground truth. This function also returns the res_id mapping + from the separate ligand res_ids to their res_ids in the combined + polymer-ligand chain; this information is needed to later separate the + combined polymer-ligand chain. + + Args: + struct: Structure to be modified. + polymer_ligand_bonds: AtomLayout with polymer-ligand bond info. + + Returns: + A tuple of a Structure with each ptm polymer-ligand chain relabelled as one + chain and a dict from bond chain pair to the res_id mapping. + """ + if not _has_only_single_bond_from_each_chain(polymer_ligand_bonds): + if _has_multiple_ligands_bonded_to_one_polymer(polymer_ligand_bonds): + # For structures where a polymer chain is connected to multiple ligands, + # we need to sort the multiple bonds from the same chain by res_id to + # ensure that the combined polymer-ligand chain will always be the same + # when you have repeated symmetric polymer-ligand chains. + polymer_ligand_bonds = ( + _sort_polymer_ligand_bonds_by_polymer_chain_and_res_id( + polymer_ligand_bonds + ) + ) + else: + raise ValueError( + 'Code cannot handle multiple bonds from one chain unless' + ' its several ligands bonded to a polymer.' + ) + res_id_mappings_for_bond_chain_pair = dict() + for (start_chain_id, end_chain_id), (start_chain_type, end_chain_type) in zip( + polymer_ligand_bonds.chain_id, polymer_ligand_bonds.chain_type + ): + poly_info, ligand_info = _get_polymer_and_ligand_chain_ids_and_types( + start_chain_id, end_chain_id, start_chain_type, end_chain_type + ) + polymer_chain_id, polymer_chain_type = poly_info + ligand_chain_id, _ = ligand_info + + # Join the ligand chain to the polymer chain. + ligand_res_ids = struct.filter(chain_id=ligand_chain_id).res_id + new_res_ids = ligand_res_ids + \ + len(struct.all_residues[polymer_chain_id]) + res_id_mappings_for_bond_chain_pair[(polymer_chain_id, ligand_chain_id)] = ( + ResIdMapping(old_res_ids=ligand_res_ids, new_res_ids=new_res_ids) + ) + chain_groups = [] + chain_group_ids = [] + chain_group_types = [] + for chain_id, chain_type in zip( + struct.chains_table.id, struct.chains_table.type + ): + if chain_id == ligand_chain_id: + continue + elif chain_id == polymer_chain_id: + chain_groups.append([polymer_chain_id, ligand_chain_id]) + chain_group_ids.append(polymer_chain_id) + chain_group_types.append(polymer_chain_type) + else: + chain_groups.append([chain_id]) + chain_group_ids.append(chain_id) + chain_group_types.append(chain_type) + + struct = struct.merge_chains( + chain_groups=chain_groups, + chain_group_ids=chain_group_ids, + chain_group_types=chain_group_types, + ) + + return struct, res_id_mappings_for_bond_chain_pair + + +def _has_only_single_bond_from_each_chain( + polymer_ligand_bonds: atom_layout.AtomLayout, +) -> bool: + """Checks that there is at most one bond from each chain.""" + chain_ids = [] + for chains in polymer_ligand_bonds.chain_id: + chain_ids.extend(chains) + if len(chain_ids) != len(set(chain_ids)): + return False + return True + + +def _get_polymer_and_ligand_chain_ids_and_types( + start_chain_id: str, + end_chain_id: str, + start_chain_type: str, + end_chain_type: str, +) -> tuple[tuple[str, str], tuple[str, str]]: + """Finds polymer and ligand chain ids from chain types.""" + if ( + start_chain_type in mmcif_names.POLYMER_CHAIN_TYPES + and end_chain_type in mmcif_names.LIGAND_CHAIN_TYPES + ): + return (start_chain_id, start_chain_type), (end_chain_id, end_chain_type) + elif ( + start_chain_type in mmcif_names.LIGAND_CHAIN_TYPES + and end_chain_type in mmcif_names.POLYMER_CHAIN_TYPES + ): + return (end_chain_id, end_chain_type), (start_chain_id, start_chain_type) + else: + raise ValueError( + 'This code only handles PTM-bonds from polymer chain to ligands.' + ) + + +def _get_polymer_dim(polymer_ligand_bonds: atom_layout.AtomLayout) -> int: + """Gets polymer dimension from the polymer-ligand bond layout.""" + start_chain_types = [] + end_chain_types = [] + for start_chain_type, end_chain_type in polymer_ligand_bonds.chain_type: + start_chain_types.append(start_chain_type) + end_chain_types.append(end_chain_type) + if set(start_chain_types).issubset( + set(mmcif_names.POLYMER_CHAIN_TYPES) + ) and set(end_chain_types).issubset(set(mmcif_names.LIGAND_CHAIN_TYPES)): + return 0 + elif set(start_chain_types).issubset(mmcif_names.LIGAND_CHAIN_TYPES) and set( + end_chain_types + ).issubset(set(mmcif_names.POLYMER_CHAIN_TYPES)): + return 1 + else: + raise ValueError( + 'Polymer and ligand dimensions are not consistent within the structure.' + ) + + +def _has_multiple_ligands_bonded_to_one_polymer(polymer_ligand_bonds): + """Checks if there are multiple ligands bonded to one polymer.""" + polymer_dim = _get_polymer_dim(polymer_ligand_bonds) + polymer_chain_ids = [ + chains[polymer_dim] for chains in polymer_ligand_bonds.chain_id + ] + if len(polymer_chain_ids) != len(set(polymer_chain_ids)): + return True + return False + + +def _has_multiple_polymers_bonded_to_one_ligand(polymer_ligand_bonds): + """Checks if there are multiple polymer chains bonded to one ligand.""" + polymer_dim = _get_polymer_dim(polymer_ligand_bonds) + ligand_dim = 1 - polymer_dim + ligand_chain_ids = [ + chains[ligand_dim] for chains in polymer_ligand_bonds.chain_id + ] + if len(ligand_chain_ids) != len(set(ligand_chain_ids)): + return True + return False + + +def _sort_polymer_ligand_bonds_by_polymer_chain_and_res_id( + polymer_ligand_bonds, +): + """Sorts bonds by res_id (for when a polymer chain has multiple bonded ligands).""" + + polymer_dim = _get_polymer_dim(polymer_ligand_bonds) + + polymer_chain_ids = [ + chains[polymer_dim] for chains in polymer_ligand_bonds.chain_id + ] + polymer_res_ids = [res[polymer_dim] for res in polymer_ligand_bonds.res_id] + + polymer_chain_and_res_id = zip(polymer_chain_ids, polymer_res_ids) + sorted_indices = [ + idx + for idx, _ in sorted( + enumerate(polymer_chain_and_res_id), key=lambda x: x[1] + ) + ] + return polymer_ligand_bonds[sorted_indices] diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py new file mode 100644 index 000000000..017210f92 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py @@ -0,0 +1,67 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Library of scoring methods of the model outputs.""" + +from alphafold3.model import protein_data_processing +import numpy as np + + +Array = np.ndarray + + +def pseudo_beta_fn( + aatype: Array, + dense_atom_positions: Array, + dense_atom_masks: Array, + is_ligand: Array | None = None, + use_jax: bool | None = True, + ) -> tuple[Array, Array] | Array: + """Create pseudo beta atom positions and optionally mask. + + Args: + aatype: [num_res] amino acid types. + dense_atom_positions: [num_res, NUM_DENSE, 3] vector of all atom positions. + dense_atom_masks: [num_res, NUM_DENSE] mask. + is_ligand: [num_res] flag if something is a ligand. + use_jax: whether to use jax for the computations. + + Returns: + Pseudo beta dense atom positions and the corresponding mask. + """ + + if is_ligand is None: + is_ligand = np.zeros_like(aatype) + + pseudobeta_index_polymer = np.take( + protein_data_processing.RESTYPE_PSEUDOBETA_INDEX, aatype, axis=0 + ).astype(np.int32) + + pseudobeta_index = np.where( + is_ligand, + np.zeros_like(pseudobeta_index_polymer), + pseudobeta_index_polymer, + ) + + if not isinstance(dense_atom_positions, Array): + dense_atom_positions = dense_atom_positions.asnumpy() + if not isinstance(dense_atom_masks, Array): + dense_atom_masks = dense_atom_masks.asnumpy() + pseudo_beta = np.take_along_axis( + dense_atom_positions, pseudobeta_index[..., None, None], axis=-2 + ) + pseudo_beta = np.squeeze(pseudo_beta, axis=-2) + + pseudo_beta_mask = np.take_along_axis( + dense_atom_masks, pseudobeta_index[..., None], axis=-1 + ).astype(np.float32) + pseudo_beta_mask = np.squeeze(pseudo_beta_mask, axis=-1) + + return pseudo_beta, pseudo_beta_mask diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict.pyi new file mode 100644 index 000000000..09d915c84 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict.pyi @@ -0,0 +1,125 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from typing import Any, ClassVar, Iterable, Iterator, TypeVar, overload + +import numpy as np + +_T = TypeVar('_T') + +class CifDict: + class ItemView: + def __iter__(self) -> Iterator[tuple[str, list[str]]]: ... + def __len__(self) -> int: ... + + class KeyView: + @overload + def __contains__(self, key: str) -> bool: ... + @overload + def __contains__(self, key: object) -> bool: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + + class ValueView: + def __iter__(self) -> Iterator[list[str]]: ... + def __len__(self) -> int: ... + + def __init__(self, d: dict[str, Iterable[str]]) -> None: ... + def copy_and_update(self, d: dict[str, Iterable[str]]) -> CifDict: ... + def extract_loop_as_dict(self, prefix: str, index: str) -> dict: + """Extracts loop associated with a prefix from mmCIF data as a dict. + + For instance for an mmCIF with these fields: + '_a.ix': ['1', '2', '3'] + '_a.1': ['a.1.1', 'a.1.2', 'a.1.3'] + '_a.2': ['a.2.1', 'a.2.2', 'a.2.3'] + + this function called with prefix='_a.', index='_a.ix' extracts: + {'1': {'a.ix': '1', 'a.1': 'a.1.1', 'a.2': 'a.2.1'} + '2': {'a.ix': '2', 'a.1': 'a.1.2', 'a.2': 'a.2.2'} + '3': {'a.ix': '3', 'a.1': 'a.1.3', 'a.2': 'a.2.3'}} + + Args: + prefix: Prefix shared by each of the data items in the loop. The prefix + should include the trailing period. + index: Which item of loop data should serve as the key. + + Returns: + Dict of dicts; each dict represents 1 entry from an mmCIF loop, + indexed by the index column. + """ + + def extract_loop_as_list(self, prefix: str) -> list: + """Extracts loop associated with a prefix from mmCIF data as a list. + + Reference for loop_ in mmCIF: + http://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html + + For instance for an mmCIF with these fields: + '_a.1': ['a.1.1', 'a.1.2', 'a.1.3'] + '_a.2': ['a.2.1', 'a.2.2', 'a.2.3'] + + this function called with prefix='_a.' extracts: + [{'_a.1': 'a.1.1', '_a.2': 'a.2.1'} + {'_a.1': 'a.1.2', '_a.2': 'a.2.2'} + {'_a.1': 'a.1.3', '_a.2': 'a.2.3'}] + + Args: + prefix: Prefix shared by each of the data items in the loop. The prefix + should include the trailing period. + + Returns: + A list of dicts; each dict represents 1 entry from an mmCIF loop. + """ + + def get(self, key: str, default_value: _T = ...) -> list[str] | _T: ... + def get_array( + self, key: str, dtype: object = ..., gather: object = ... + ) -> np.ndarray: + """Returns values looked up in dict converted to a NumPy array. + + Args: + key: Key in dictionary. + dtype: Optional (default `object`) Specifies output dtype of array. One of + [object, np.{int,uint}{8,16,32,64} np.float{32,64}]. As with NumPy use + `object` to return a NumPy array of strings. + gather: Optional one of [slice, np.{int,uint}{32,64}] non-intermediate + version of get_array(key, dtype)[gather]. + + Returns: + A NumPy array of given dtype. An optimised equivalent to + np.array(cif[key]).astype(dtype). With support of '.' being treated + as np.nan if dtype is one of np.float{32,64}. + Identical strings will all reference the same object to save space. + + Raises: + KeyError - if key is not found. + TypeError - if dtype is not valid or supported. + ValueError - if string cannot convert to dtype. + """ + + def get_data_name(self) -> str: ... + def items(self) -> CifDict.ItemView: ... + def keys(self) -> CifDict.KeyView: ... + def to_string(self) -> str: ... + def value_length(self, key: str) -> int: ... + def values(self) -> CifDict.ValueView: ... + def __bool__(self) -> bool: ... + def __contains__(self, key: str) -> bool: ... + def __getitem__(self, key: str) -> list[str]: ... + def __getstate__(self) -> tuple: ... + def __iter__(self) -> Iterator[str]: ... + def __len__(self) -> int: ... + def __setstate__(self, state: tuple) -> None: ... + +def tokenize(cif_string: str) -> list[str]: ... +def split_line(line: str) -> list[str]: ... +def from_string(mmcif_string: str | bytes) -> CifDict: ... +def parse_multi_data_cif(cif_string: str | bytes) -> dict[str, CifDict]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc new file mode 100644 index 000000000..2d2675c75 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc @@ -0,0 +1,648 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include "alphafold3/parsers/cpp/cif_dict_lib.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/btree_map.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/container/node_hash_map.h" +#include "absl/log/check.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/strings/strip.h" + +namespace alphafold3 { +namespace { + +bool IsQuote(const char symbol) { return symbol == '\'' || symbol == '"'; } +bool IsWhitespace(const char symbol) { return symbol == ' ' || symbol == '\t'; } + +// Splits line into tokens, returns whether successful. +bool SplitLineInline(absl::string_view line, + std::vector* tokens) { + // See https://www.iucr.org/resources/cif/spec/version1.1/cifsyntax + for (int i = 0, line_length = line.length(); i < line_length;) { + // Skip whitespace (spaces or tabs). + while (IsWhitespace(line[i])) { + if (++i == line_length) { + break; + } + } + if (i == line_length) { + break; + } + + // Skip comments (from # until the end of the line). If # is a non-comment + // character, it must be inside a quoted token. + if (line[i] == '#') { + break; + } + + int start_index; + int end_index; + if (IsQuote(line[i])) { + // Token in single or double quotes. CIF v1.1 specification considers a + // quote to be an opening quote only if it is at the beginning of a token. + // So e.g. A' B has tokens A' and B. Also, ""A" is a token "A. + const char quote_char = line[i++]; + start_index = i; + + // Find matching quote. The double loop is not strictly necessary, but + // optimises a bit better. + while (true) { + while (i < line_length && line[i] != quote_char) { + ++i; + } + if (i == line_length) { + // Reached the end of the line while still being inside a token. + return false; + } + if (i + 1 == line_length || IsWhitespace(line[i + 1])) { + break; + } + ++i; + } + end_index = i++; + } else { + // Non-quoted token. Read until reaching whitespace. + start_index = i++; + while (i < line_length && !IsWhitespace(line[i])) { + ++i; + } + end_index = i; + } + + tokens->push_back(line.substr(start_index, end_index - start_index)); + } + + return true; +} + +using HeapStrings = std::vector>; + +// The majority of strings can be viewed on original cif_string. +// heap_strings store multi-line tokens that have internal white-space stripped. +absl::StatusOr> TokenizeInternal( + absl::string_view cif_string, HeapStrings* heap_strings) { + const std::vector lines = absl::StrSplit(cif_string, '\n'); + std::vector tokens; + // Heuristic: Most lines in an mmCIF are _atom_site lines with 21 tokens. + tokens.reserve(lines.size() * 21); + int line_num = 0; + while (line_num < lines.size()) { + auto line = lines[line_num]; + line_num++; + + if (line.empty() || line[0] == '#') { + // Skip empty lines or lines that contain only comments. + continue; + } else if (line[0] == ';') { + // Leading whitespace on each line must be preserved while trailing + // whitespace may be stripped. + std::vector multiline_tokens; + // Strip the leading ";". + multiline_tokens.push_back( + absl::StripTrailingAsciiWhitespace(line.substr(1))); + while (line_num < lines.size()) { + auto multiline = absl::StripTrailingAsciiWhitespace(lines[line_num]); + line_num++; + if (!multiline.empty() && multiline[0] == ';') { + break; + } + multiline_tokens.push_back(multiline); + } + heap_strings->push_back( + std::make_unique(absl::StrJoin(multiline_tokens, "\n"))); + tokens.emplace_back(*heap_strings->back()); + } else { + if (!SplitLineInline(line, &tokens)) { + return absl::InvalidArgumentError( + absl::StrCat("Line ended with quote open: ", line)); + } + } + } + return tokens; +} + +absl::string_view GetEscapeQuote(const absl::string_view value) { + // Empty values should not happen, but if so, they should be quoted. + if (value.empty()) { + return "\""; + } + + // Shortcut for the most common cases where no quoting needed. + if (std::all_of(value.begin(), value.end(), [](char c) { + return absl::ascii_isalnum(c) || c == '.' || c == '?' || c == '-'; + })) { + return ""; + } + + // The value must not start with one of these CIF keywords. + if (absl::StartsWithIgnoreCase(value, "data_") || + absl::StartsWithIgnoreCase(value, "loop_") || + absl::StartsWithIgnoreCase(value, "save_") || + absl::StartsWithIgnoreCase(value, "stop_") || + absl::StartsWithIgnoreCase(value, "global_")) { + return "\""; + } + + // The first character must not be a special character. + const char first = value.front(); + if (first == '_' || first == '#' || first == '$' || first == '[' || + first == ']' || first == ';') { + return "\""; + } + + // No quotes or whitespace allowed inside. + for (const char c : value) { + if (c == '"') { + return "'"; + } else if (c == '\'' || c == ' ' || c == '\t') { + return "\""; + } + } + return ""; +} + +int RecordIndex(absl::string_view record) { + if (record == "_entry") { + return 0; // _entry is always first. + } + if (record == "_atom_site") { + return 2; // _atom_site is always last. + } + return 1; // other records are between _entry and _atom_site. +} + +struct RecordOrder { + using is_transparent = void; // Enable heterogeneous lookup. + bool operator()(absl::string_view lhs, absl::string_view rhs) const { + std::size_t lhs_index = RecordIndex(lhs); + std::size_t rhs_index = RecordIndex(rhs); + return std::tie(lhs_index, lhs) < std::tie(rhs_index, rhs); + } +}; + +// Make sure the _atom_site loop columns are sorted in the PDB-standard way. +constexpr absl::string_view kAtomSiteSortOrder[] = { + "_atom_site.group_PDB", + "_atom_site.id", + "_atom_site.type_symbol", + "_atom_site.label_atom_id", + "_atom_site.label_alt_id", + "_atom_site.label_comp_id", + "_atom_site.label_asym_id", + "_atom_site.label_entity_id", + "_atom_site.label_seq_id", + "_atom_site.pdbx_PDB_ins_code", + "_atom_site.Cartn_x", + "_atom_site.Cartn_y", + "_atom_site.Cartn_z", + "_atom_site.occupancy", + "_atom_site.B_iso_or_equiv", + "_atom_site.pdbx_formal_charge", + "_atom_site.auth_seq_id", + "_atom_site.auth_comp_id", + "_atom_site.auth_asym_id", + "_atom_site.auth_atom_id", + "_atom_site.pdbx_PDB_model_num", +}; + +size_t AtomSiteIndex(absl::string_view atom_site) { + return std::distance(std::begin(kAtomSiteSortOrder), + absl::c_find(kAtomSiteSortOrder, atom_site)); +} + +struct AtomSiteOrder { + bool operator()(absl::string_view lhs, absl::string_view rhs) const { + auto lhs_index = AtomSiteIndex(lhs); + auto rhs_index = AtomSiteIndex(rhs); + return std::tie(lhs_index, lhs) < std::tie(rhs_index, rhs); + } +}; + +class Column { + public: + Column(absl::string_view key, const std::vector* values) + : key_(key), values_(values) { + int max_value_length = 0; + for (size_t i = 0; i < values->size(); ++i) { + absl::string_view value = (*values)[i]; + if (absl::StrContains(value, '\n')) { + values_with_newlines_.insert(i); + } else { + absl::string_view quote = GetEscapeQuote(value); + if (!quote.empty()) { + values_with_quotes_[i] = quote; + } + max_value_length = + std::max(max_value_length, value.size() + quote.size() * 2); + } + } + max_value_length_ = max_value_length; + } + + absl::string_view key() const { return key_; } + + const std::vector* values() const { return values_; } + + int max_value_length() const { return max_value_length_; } + + bool has_newlines(size_t index) const { + return values_with_newlines_.contains(index); + } + + absl::string_view quote(size_t index) const { + if (auto it = values_with_quotes_.find(index); + it != values_with_quotes_.end()) { + return it->second; + } + return ""; + } + + private: + absl::string_view key_; + const std::vector* values_; + int max_value_length_; + // Values with newlines or quotes are very rare in a typical CIF file. + absl::flat_hash_set values_with_newlines_; + absl::flat_hash_map values_with_quotes_; +}; + +struct GroupedKeys { + std::vector grouped_columns; + int max_key_length; + int value_size; +}; + +} // namespace + +absl::StatusOr CifDict::FromString(absl::string_view cif_string) { + CifDict::Dict cif; + + bool loop_flag = false; + absl::string_view key; + + HeapStrings heap_strings; + auto tokens = TokenizeInternal(cif_string, &heap_strings); + if (!tokens.ok()) { + return tokens.status(); + } + + if (tokens->empty()) { + return absl::InvalidArgumentError("The CIF file must not be empty."); + } + + // The first token should be data_XXX. Split into key = data, value = XXX. + absl::string_view first_token = tokens->front(); + if (!absl::ConsumePrefix(&first_token, "data_")) { + return absl::InvalidArgumentError( + "The CIF file does not start with the data_ field."); + } + cif["data_"].emplace_back(first_token); + + // Counters for CIF loop_ regions. + int loop_token_index = 0; + int num_loop_keys = 0; + // Loops have usually O(10) columns but could have up to O(10^6) rows. It is + // therefore wasteful to look up the cif vector where to add a loop value + // since that means doing `columns * rows` map lookups. If we save pointers to + // these loop column fields instead, we need only 1 cif lookup per column. + std::vector*> loop_column_values; + + // Skip the first element since we already processed it above. + for (auto token_itr = tokens->begin() + 1; token_itr != tokens->end(); + ++token_itr) { + auto token = *token_itr; + if (absl::EqualsIgnoreCase(token, "loop_")) { + // A new loop started, get rid of old loop's data. + loop_flag = true; + loop_column_values.clear(); + loop_token_index = 0; + num_loop_keys = 0; + continue; + } else if (loop_flag) { + // The second condition checks we are in the first column. Some mmCIF + // files (e.g. 4q9r) have values in later columns starting with an + // underscore and we don't want to read these as keys. + int token_column_index = + num_loop_keys == 0 ? 0 : loop_token_index % num_loop_keys; + if (token_column_index == 0 && !token.empty() && token[0] == '_') { + if (loop_token_index > 0) { + // We are out of the loop. + loop_flag = false; + } else { + // We are in the keys (column names) section of the loop. + auto& columns = cif[token]; + columns.clear(); + + // Heuristic: _atom_site is typically the largest table in an mmCIF + // with ~16 columns. Make sure we reserve enough space for its values. + if (absl::StartsWith(token, "_atom_site.")) { + columns.reserve(tokens->size() / 20); + } + + // Save the pointer to the loop column values. + loop_column_values.push_back(&columns); + num_loop_keys += 1; + continue; + } + } else { + // We are in the values section of the loop. We have a pointer to the + // loops' values, add the new token in there. + if (token_column_index >= loop_column_values.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Too many columns at: '", token, + "' at column index: ", token_column_index, + " expected at most: ", loop_column_values.size())); + } + loop_column_values[token_column_index]->emplace_back(token); + loop_token_index++; + continue; + } + } + if (key.empty()) { + key = token; + } else { + cif[key].emplace_back(token); + key = ""; + } + } + return CifDict(std::move(cif)); +} + +absl::StatusOr CifDict::ToString() const { + std::string output; + + absl::string_view data_name; + // Check that the data_ field exists. + if (auto name_it = (*dict_).find("data_"); + name_it == (*dict_).end() || name_it->second.empty()) { + return absl::InvalidArgumentError( + "The CIF must contain a valid name for this data block in the special " + "data_ field."); + } else { + data_name = name_it->second.front(); + } + + if (absl::c_any_of(data_name, + [](char i) { return absl::ascii_isspace(i); })) { + return absl::InvalidArgumentError(absl::StrFormat( + "The CIF data block name must not contain any whitespace characters, " + "got '%s'.", + data_name)); + } + absl::StrAppend(&output, "data_", data_name, "\n#\n"); + + // Group keys by their prefix. Use btree_map to iterate in alphabetical order, + // but with some keys being placed at the end (e.g. _atom_site). + absl::btree_map grouped_keys; + for (const auto& [key, values] : *dict_) { + if (key == "data_") { + continue; // Skip the special data_ key, we are already done with it. + } + const std::pair key_parts = + absl::StrSplit(key, absl::MaxSplits('.', 1)); + const absl::string_view key_prefix = key_parts.first; + auto [it, inserted] = grouped_keys.emplace(key_prefix, GroupedKeys{}); + GroupedKeys& grouped_key = it->second; + grouped_key.grouped_columns.push_back(Column(key, &values)); + if (inserted) { + grouped_key.max_key_length = key.length(); + grouped_key.value_size = values.size(); + } else { + grouped_key.max_key_length = + std::max(key.length(), grouped_key.max_key_length); + if (grouped_key.value_size != values.size()) { + return absl::InvalidArgumentError( + absl::StrFormat("Values for key %s have different length (%d) than " + "the other values with the same key prefix (%d).", + key, values.size(), grouped_key.value_size)); + } + } + } + + for (auto& [key_prefix, group_info] : grouped_keys) { + if (key_prefix == "_atom_site") { + // Make sure we sort the _atom_site loop in the standard way. + absl::c_sort(group_info.grouped_columns, + [](const Column& lhs, const Column& rhs) { + return AtomSiteOrder{}(lhs.key(), rhs.key()); + }); + } else { + // Make the key ordering within a key group deterministic. + absl::c_sort(group_info.grouped_columns, + [](const Column& lhs, const Column& rhs) { + return lhs.key() < rhs.key(); + }); + } + + // Force `_atom_site` field to always be a loop. This resolves issues with + // third party mmCIF parsers such as OpenBabel which always expect a loop + // even when there is only a single atom present. + if (group_info.value_size == 1 && key_prefix != "_atom_site") { + // Plain key-value pairs, output them as they are. + for (const Column& grouped_column : group_info.grouped_columns) { + int width = group_info.max_key_length + 1; + size_t start_pos = output.size(); + output.append(width, ' '); + auto out_it = output.begin() + start_pos; + absl::c_copy(grouped_column.key(), out_it); + // Append the value, handle multi-line/quoting. + absl::string_view value = grouped_column.values()->front(); + if (grouped_column.has_newlines(0)) { + absl::StrAppend(&output, "\n;", value, "\n;\n"); // Multi-line value. + } else { + const absl::string_view quote_char = grouped_column.quote(0); + absl::StrAppend(&output, quote_char, value, quote_char, "\n"); + } + } + } else { + // CIF loop. Output the column names, then the rows with data. + absl::StrAppend(&output, "loop_\n"); + for (Column& grouped_column : group_info.grouped_columns) { + absl::StrAppend(&output, grouped_column.key(), "\n"); + } + // Write the loop values, line by line. This is the most expensive part + // since this path is taken to write the entire atom site table which has + // about 20 columns, but thousands of rows. + for (int i = 0; i < group_info.value_size; i++) { + for (int column_index = 0; + column_index < group_info.grouped_columns.size(); ++column_index) { + const Column& grouped_column = + group_info.grouped_columns[column_index]; + const absl::string_view value = (*grouped_column.values())[i]; + if (grouped_column.has_newlines(i)) { + // Multi-line. This is very rarely taken path. + if (column_index == 0) { + // No extra newline before leading ;, already inserted. + absl::StrAppend(&output, ";", value, "\n;\n"); + } else if (column_index == group_info.grouped_columns.size() - 1) { + // No extra newline after trailing ;, will be inserted. + absl::StrAppend(&output, "\n;", value, "\n;"); + } else { + absl::StrAppend(&output, "\n;", value, "\n;\n"); + } + } else { + size_t start_pos = output.size(); + output.append(grouped_column.max_value_length() + 1, ' '); + auto out_it = output.begin() + start_pos; + absl::string_view quote = grouped_column.quote(i); + if (!quote.empty()) { + out_it = absl::c_copy(quote, out_it); + out_it = absl::c_copy(value, out_it); + absl::c_copy(quote, out_it); + } else { + absl::c_copy(value, out_it); + } + } + } + absl::StrAppend(&output, "\n"); + } + } + absl::StrAppend(&output, "#\n"); // Comment token after every key group. + } + return output; +} + +absl::StatusOr< + std::vector>> +CifDict::ExtractLoopAsList(absl::string_view prefix) const { + std::vector column_names; + std::vector> column_data; + + for (const auto& element : *dict_) { + if (absl::StartsWith(element.first, prefix)) { + column_names.emplace_back(element.first); + auto& cells = column_data.emplace_back(); + cells.insert(cells.begin(), element.second.begin(), element.second.end()); + } + } + // Make sure all columns have the same number of rows. + const std::size_t num_rows = column_data.empty() ? 0 : column_data[0].size(); + for (const auto& column : column_data) { + if (column.size() != num_rows) { + return absl::InvalidArgumentError(absl::StrCat( + GetDataName(), + ": Columns do not have the same number of rows for prefix: '", prefix, + "'. One possible reason could be not including the trailing dot, " + "e.g. '_atom_site.'.")); + } + } + + std::vector> result; + result.reserve(num_rows); + CHECK_EQ(column_names.size(), column_data.size()); + for (std::size_t row_index = 0; row_index < num_rows; ++row_index) { + auto& row_dict = result.emplace_back(); + row_dict.reserve(column_names.size()); + for (int col_index = 0; col_index < column_names.size(); ++col_index) { + row_dict[column_names[col_index]] = column_data[col_index][row_index]; + } + } + return result; +} + +absl::StatusOr>> +CifDict::ExtractLoopAsDict(absl::string_view prefix, + absl::string_view index) const { + if (!absl::StartsWith(index, prefix)) { + return absl::InvalidArgumentError( + absl::StrCat(GetDataName(), ": The loop index '", index, + "' must start with the loop prefix '", prefix, "'.")); + } + absl::flat_hash_map> + result; + auto loop_as_list = ExtractLoopAsList(prefix); + if (!loop_as_list.ok()) { + return loop_as_list.status(); + } + result.reserve(loop_as_list->size()); + for (auto& entry : *loop_as_list) { + if (const auto it = entry.find(index); it != entry.end()) { + result[it->second] = entry; + } else { + return absl::InvalidArgumentError(absl::StrCat( + GetDataName(), ": The index column '", index, + "' could not be found in the loop with prefix '", prefix, "'.")); + } + } + return result; +} + +absl::StatusOr> Tokenize( + absl::string_view cif_string) { + HeapStrings heap_strings; + auto tokens = TokenizeInternal(cif_string, &heap_strings); + if (!tokens.ok()) { + return tokens.status(); + } + return std::vector(tokens->begin(), tokens->end()); +} + +absl::StatusOr> SplitLine( + absl::string_view line) { + std::vector tokens; + if (!SplitLineInline(line, &tokens)) { + return absl::InvalidArgumentError( + absl::StrCat("Line ended with quote open: ", line)); + } + return tokens; +} + +absl::StatusOr> ParseMultiDataCifDict( + absl::string_view cif_string) { + absl::flat_hash_map mapping; + constexpr absl::string_view delimiter = "data_"; + // Check cif_string starts with correct offset. + if (!cif_string.empty() && !absl::StartsWith(cif_string, delimiter)) { + return absl::InvalidArgumentError( + "Invalid format. MultiDataCifDict must start with 'data_'"); + } + for (absl::string_view data_block : + absl::StrSplit(cif_string, delimiter, absl::SkipEmpty())) { + absl::string_view block_with_delimitor( + data_block.data() - delimiter.size(), + data_block.size() + delimiter.size()); + absl::StatusOr parsed_block = + CifDict::FromString(block_with_delimitor); + if (!parsed_block.ok()) { + return parsed_block.status(); + } + absl::string_view data_name = parsed_block->GetDataName(); + mapping[data_name] = *std::move(parsed_block); + } + + return mapping; +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.h new file mode 100644 index 000000000..5c16eaa87 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.h @@ -0,0 +1,149 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +// A C++ implementation of a CIF parser. For the format specification see +// https://www.iucr.org/resources/cif/spec/version1.1/cifsyntax +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/node_hash_map.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" + +namespace alphafold3 { + +class CifDict { + public: + // Use absl::node_hash_map since it guarantees pointer stability. + using Dict = absl::node_hash_map>; + + CifDict() = default; + + explicit CifDict(Dict dict) + : dict_(std::make_shared(std::move(dict))) {} + + // Converts a CIF string into a dictionary mapping each CIF field to a list of + // values that field contains. + static absl::StatusOr FromString(absl::string_view cif_string); + + // Converts the CIF into into a string that is a valid CIF file. + absl::StatusOr ToString() const; + + // Extracts loop associated with a prefix from mmCIF data as a list. + // Reference for loop_ in mmCIF: + // http://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html + // Args: + // prefix: Prefix shared by each of the data items in the loop. + // e.g. '_entity_poly_seq.', where the data items are _entity_poly_seq.num, + // _entity_poly_seq.mon_id. Should include the trailing period. + // + // Returns a list of dicts; each dict represents 1 entry from an mmCIF loop. + // Lifetime of string_views tied to this. + absl::StatusOr< + std::vector>> + ExtractLoopAsList(absl::string_view prefix) const; + + // Extracts loop associated with a prefix from mmCIF data as a dictionary. + // Args: + // prefix: Prefix shared by each of the data items in the loop. + // e.g. '_entity_poly_seq.', where the data items are _entity_poly_seq.num, + // _entity_poly_seq.mon_id. Should include the trailing period. + // index: Which item of loop data should serve as the key. + // + // Returns a dict of dicts; each dict represents 1 entry from an mmCIF loop, + // indexed by the index column. + // Lifetime of string_views tied to this. + absl::StatusOr>> + ExtractLoopAsDict(absl::string_view prefix, absl::string_view index) const; + + // Returns value at key if present or an empty list. + absl::Span operator[](absl::string_view key) const { + auto it = dict_->find(key); + if (it != dict_->end()) { + return it->second; + } + return {}; + } + + // Returns boolean of whether dict contains key. + bool Contains(absl::string_view key) const { return dict_->contains(key); } + + // Returns number of values for the given key if present, 0 otherwise. + size_t ValueLength(absl::string_view key) const { + return (*this)[key].size(); + } + + // Returns the size of the underlying dictionary. + std::size_t Length() { return dict_->size(); } + + // Creates a copy of this CifDict object that will contain the original values + // but only if not updated by the given dictionary. + // E.g. if the CifDict = {a: [a1, a2], b: [b1]} and other = {a: [x], c: [z]}, + // you will get {a: [x], b: [b1], c: [z]}. + CifDict CopyAndUpdate(Dict other) const { + other.insert(dict_->begin(), dict_->end()); + return CifDict(std::move(other)); + } + + // Returns the value of the special CIF data_ field. + absl::string_view GetDataName() const { + // The data_ element has to be present by construction. + if (auto it = dict_->find("data_"); + it != dict_->end() && !it->second.empty()) { + return it->second.front(); + } else { + return ""; + } + } + + const std::shared_ptr& dict() const { return dict_; } + + private: + std::shared_ptr dict_; +}; + +// Tokenizes a CIF string into a list of string tokens. This is more involved +// than just a simple split on whitespace as CIF allows comments and quoting. +absl::StatusOr> Tokenize(absl::string_view cif_string); + +// Tokenizes a single line of a CIF string. +absl::StatusOr> SplitLine( + absl::string_view line); + +// Parses a CIF string with multiple data records and returns a mapping from +// record names to CifDict objects. For instance, the following CIF string: +// +// data_001 +// _foo bar +// +// data_002 +// _foo baz +// +// will be parsed as: +// {'001': CifDict({'_foo': ['bar']}), +// '002': CifDict({'_foo': ['baz']})} +absl::StatusOr> ParseMultiDataCifDict( + absl::string_view cif_string); + +} // namespace alphafold3 + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_LIB_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc new file mode 100644 index 000000000..130a8215a --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc @@ -0,0 +1,652 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "numpy/ndarrayobject.h" +#include "numpy/ndarraytypes.h" +#include "numpy/npy_common.h" +#include "absl/base/no_destructor.h" +#include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "pybind11/attr.h" +#include "pybind11/cast.h" +#include "pybind11/gil.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" + +namespace alphafold3 { +namespace { +namespace py = pybind11; + +template +bool GatherArray(size_t num_dims, npy_intp* shape_array, npy_intp* stride_array, + const char* data, absl::Span values, + ForEach&& for_each_cb) { + if (num_dims == 1) { + const npy_intp shape = shape_array[0]; + const npy_intp stride = stride_array[0]; + for (size_t i = 0; i < shape; ++i) { + Item index; + std::memcpy(&index, data + stride * i, sizeof(Item)); + if (index < 0 || index >= values.size()) { + PyErr_SetString(PyExc_IndexError, + absl::StrCat("index ", index, + " is out of bounds for column with size ", + values.size()) + .c_str()); + return false; + } + if (!for_each_cb(values[index])) { + return false; + } + } + } else if (num_dims == 0) { + Item index; + std::memcpy(&index, data, sizeof(Item)); + if (index < 0 || index >= values.size()) { + PyErr_SetString( + PyExc_IndexError, + absl::StrCat("index ", index, + " is out of bounds for column with size ", values.size()) + .c_str()); + return false; + } + if (!for_each_cb(values[index])) { + return false; + } + } else { + const npy_intp shape = shape_array[0]; + const npy_intp stride = stride_array[0]; + for (size_t i = 0; i < shape; ++i) { + if (!GatherArray(num_dims - 1, shape_array + 1, stride_array + 1, + data + stride * i, values, for_each_cb)) { + return false; + } + } + } + return true; +} + +template +bool Gather(PyObject* gather, absl::Span values, + Size&& size_cb, ForEach&& for_each_cb) { + if (gather == Py_None) { + npy_intp dim = static_cast(values.size()); + if (!size_cb(absl::MakeSpan(&dim, 1))) { + return false; + } + for (const std::string& v : values) { + if (!for_each_cb(v)) { + return false; + } + } + return true; + } + if (PySlice_Check(gather)) { + Py_ssize_t start, stop, step, slice_length; + if (PySlice_GetIndicesEx(gather, values.size(), &start, &stop, &step, + &slice_length) != 0) { + return false; + } + npy_intp dim = static_cast(slice_length); + if (!size_cb(absl::MakeSpan(&dim, 1))) { + return false; + } + for (size_t i = 0; i < slice_length; ++i) { + if (!for_each_cb(values[start + i * step])) { + return false; + } + } + return true; + } + if (PyArray_Check(gather)) { + PyArrayObject* gather_array = reinterpret_cast(gather); + auto shape = + absl::MakeSpan(PyArray_DIMS(gather_array), PyArray_NDIM(gather_array)); + switch (PyArray_TYPE(gather_array)) { + case NPY_INT16: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + case NPY_UINT16: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + case NPY_INT32: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + case NPY_UINT32: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + case NPY_INT64: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + case NPY_UINT64: + if (!size_cb(shape)) { + return false; + } + return GatherArray(shape.size(), shape.data(), + PyArray_STRIDES(gather_array), + PyArray_BYTES(gather_array), values, + std::forward(for_each_cb)); + default: + PyErr_SetString(PyExc_TypeError, "Unsupported NumPy array type."); + return false; + } + } + + PyErr_Format(PyExc_TypeError, "Invalid gather %R", gather); + return false; +} + +// Creates a NumPy array of objects of given strings. Reusing duplicates where +// possible. +PyObject* ConvertStrings(PyObject* gather, PyArray_Descr* type, + absl::Span values) { + absl::flat_hash_map existing; + + PyObject* ret = nullptr; + PyObject** dst; + if (Gather( + gather, values, + [&dst, &ret, type](absl::Span size) { + ret = PyArray_NewFromDescr( + /*subtype=*/&PyArray_Type, + /*type=*/type, + /*nd=*/size.size(), + /*dims=*/size.data(), + /*strides=*/nullptr, + /*data=*/nullptr, + /*flags=*/0, + /*obj=*/nullptr); + dst = static_cast( + PyArray_DATA(reinterpret_cast(ret))); + return true; + }, + [&dst, &existing](absl::string_view value) { + auto [it, inserted] = existing.emplace(value, nullptr); + if (inserted) { + it->second = + PyUnicode_FromStringAndSize(value.data(), value.size()); + PyUnicode_InternInPlace(&it->second); + } else { + Py_INCREF(it->second); + } + *dst++ = it->second; + return true; + })) { + return ret; + } else { + Py_XDECREF(ret); + return nullptr; + } +} + +// Creates NumPy array with given dtype given specified converter. +// `converter` shall have the following signature: +// bool converter(const std::string& value, T* result); +// It must return whether conversion is successful and store conversion in +// result. +template +inline PyObject* Convert(PyObject* gather, PyArray_Descr* type, + absl::Span values, C&& converter) { + py::object ret; + T* dst; + if (Gather( + gather, values, + [&dst, &ret, type](absl::Span size) { + // Construct uninitialised NumPy array of type T. + ret = py::reinterpret_steal(PyArray_NewFromDescr( + /*subtype=*/&PyArray_Type, + /*type=*/type, + /*nd=*/size.size(), + /*dims=*/size.data(), + /*strides=*/nullptr, + /*data=*/nullptr, + /*flags=*/0, + /*obj=*/nullptr)); + + dst = static_cast( + PyArray_DATA(reinterpret_cast(ret.ptr()))); + return true; + }, + [&dst, &converter](const std::string& value) { + if (!converter(value, dst++)) { + PyErr_SetString(PyExc_ValueError, value.c_str()); + return false; + } + return true; + })) { + return ret.release().ptr(); + } + return nullptr; +} + +PyObject* CifDictGetArray(const CifDict& self, absl::string_view key, + PyObject* dtype, PyObject* gather) { + import_array(); + PyArray_Descr* type = nullptr; + if (dtype == Py_None) { + type = PyArray_DescrFromType(NPY_OBJECT); + } else if (PyArray_DescrConverter(dtype, &type) == NPY_FAIL || !type) { + PyErr_Format(PyExc_TypeError, "Invalid dtype %R", dtype); + Py_XDECREF(type); + return nullptr; + } + auto entry = self.dict()->find(key); + if (entry == self.dict()->end()) { + Py_DECREF(type); + PyErr_SetObject(PyExc_KeyError, + PyUnicode_FromStringAndSize(key.data(), key.size())); + return nullptr; + } + + auto int_convert = [](absl::string_view str, auto* value) { + return absl::SimpleAtoi(str, value); + }; + + auto int_convert_bounded = [](absl::string_view str, auto* value) { + int64_t v; + if (absl::SimpleAtoi(str, &v)) { + using limits = + std::numeric_limits>; + if (limits::min() <= v && v <= limits::max()) { + *value = v; + return true; + } + } + return false; + }; + + absl::Span values = entry->second; + + switch (type->type_num) { + case NPY_DOUBLE: + return Convert( + gather, type, values, [](absl::string_view str, double* value) { + if (str == ".") { + *value = std::numeric_limits::quiet_NaN(); + return true; + } + return absl::SimpleAtod(str, value); + }); + case NPY_FLOAT: + return Convert( + gather, type, values, [](absl::string_view str, float* value) { + if (str == ".") { + *value = std::numeric_limits::quiet_NaN(); + return true; + } + return absl::SimpleAtof(str, value); + }); + case NPY_INT8: + return Convert(gather, type, values, int_convert_bounded); + case NPY_INT16: + return Convert(gather, type, values, int_convert_bounded); + case NPY_INT32: + return Convert(gather, type, values, int_convert); + case NPY_INT64: + return Convert(gather, type, values, int_convert); + case NPY_UINT8: + return Convert(gather, type, values, int_convert_bounded); + case NPY_UINT16: + return Convert(gather, type, values, int_convert_bounded); + case NPY_UINT32: + return Convert(gather, type, values, int_convert); + case NPY_UINT64: + return Convert(gather, type, values, int_convert); + case NPY_BOOL: + return Convert(gather, type, values, + [](absl::string_view str, bool* value) { + if (str == "n" || str == "no") { + *value = false; + return true; + } + if (str == "y" || str == "yes") { + *value = true; + return true; + } + return false; + }); + case NPY_OBJECT: + return ConvertStrings(gather, type, values); + default: { + PyErr_Format(PyExc_TypeError, "Unsupported dtype %R", dtype); + Py_XDECREF(type); + return nullptr; + } + } +} + +} // namespace + +void RegisterModuleCifDict(pybind11::module m) { + using Value = std::vector; + static absl::NoDestructor> empty_values; + + m.def( + "from_string", + [](absl::string_view s) { + absl::StatusOr dict = CifDict::FromString(s); + if (!dict.ok()) { + throw py::value_error(dict.status().ToString()); + } + return *dict; + }, + py::call_guard()); + + m.def( + "tokenize", + [](absl::string_view cif_string) { + absl::StatusOr> tokens = Tokenize(cif_string); + if (!tokens.ok()) { + throw py::value_error(tokens.status().ToString()); + } + return *std::move(tokens); + }, + py::arg("cif_string")); + + m.def("split_line", [](absl::string_view line) { + absl::StatusOr> tokens = SplitLine(line); + if (!tokens.ok()) { + throw py::value_error(tokens.status().ToString()); + } + return *std::move(tokens); + }); + + m.def( + "parse_multi_data_cif", + [](absl::string_view cif_string) { + auto result = ParseMultiDataCifDict(cif_string); + if (!result.ok()) { + throw py::value_error(result.status().ToString()); + } + py::dict dict; + for (auto& [key, value] : *result) { + dict[py::cast(key)] = py::cast(value); + } + return dict; + }, + py::arg("cif_string")); + + auto cif_dict = + py::class_(m, "CifDict") + .def(py::init<>([](py::dict dict) { + CifDict::Dict result; + for (const auto& [key, value] : dict) { + result.emplace(py::cast(key), + py::cast>(value)); + } + return CifDict(std::move(result)); + }), + "Initialise with a map") + .def("copy_and_update", + [](const CifDict& self, py::dict dict) { + CifDict::Dict result; + for (const auto& [key, value] : dict) { + result.emplace(py::cast(key), + py::cast>(value)); + } + { + py::gil_scoped_release gil_release; + return self.CopyAndUpdate(std::move(result)); + } + }) + .def( + "__str__", + [](const CifDict& self) { + absl::StatusOr result = self.ToString(); + if (!result.ok()) { + throw py::value_error(result.status().ToString()); + } + return *result; + }, + "Serialize to a string", py::call_guard()) + .def( + "to_string", + [](const CifDict& self) { + absl::StatusOr result = self.ToString(); + if (!result.ok()) { + throw py::value_error(result.status().ToString()); + } + return *result; + }, + "Serialize to a string", py::call_guard()) + .def("value_length", &CifDict::ValueLength, py::arg("key"), + "Num elements in value") + .def("__len__", + [](const CifDict& self) { return self.dict()->size(); }) + .def( + "__bool__", + [](const CifDict& self) { return !self.dict()->empty(); }, + "Check whether the map is nonempty") + .def( + "__contains__", + [](const CifDict& self, absl::string_view k) { + return self.dict()->find(k) != self.dict()->end(); + }, + py::arg("key"), py::call_guard()) + .def("get_data_name", &CifDict::GetDataName) + .def( + "get", + [](const CifDict& self, absl::string_view k, + py::object default_value) -> py::object { + auto it = self.dict()->find(k); + if (it == self.dict()->end()) return default_value; + py::list result(it->second.size()); + size_t index = 0; + for (const std::string& v : it->second) { + result[index++] = py::cast(v); + } + return result; + }, + py::arg("key"), py::arg("default_value") = py::none()) + .def( + "get_array", + [](const CifDict& self, absl::string_view key, py::handle dtype, + py::handle gather) -> py::object { + PyObject* obj = + CifDictGetArray(self, key, dtype.ptr(), gather.ptr()); + if (obj == nullptr) { + throw py::error_already_set(); + } + return py::reinterpret_steal(obj); + }, + py::arg("key"), py::arg("dtype") = py::none(), + py::arg("gather") = py::none()) + .def( + "__getitem__", + [](const CifDict& self, absl::string_view k) -> const Value& { + auto it = self.dict()->find(k); + if (it == self.dict()->end()) { + throw py::key_error(std::string(k).c_str()); + } + return it->second; + }, + py::arg("key"), py::call_guard()) + .def( + "extract_loop_as_dict", + [](const CifDict& self, absl::string_view prefix, + absl::string_view index) { + absl::StatusOr>> + dict; + { + py::gil_scoped_release gil_release; + dict = self.ExtractLoopAsDict(prefix, index); + if (!dict.ok()) { + throw py::value_error(dict.status().ToString()); + } + } + py::dict key_value_dict; + for (const auto& [key, value] : *dict) { + py::dict value_dict; + for (const auto& [key2, value2] : value) { + value_dict[py::cast(key2)] = py::cast(value2); + } + key_value_dict[py::cast(key)] = std::move(value_dict); + } + return key_value_dict; + }, + py::arg("prefix"), py::arg("index")) + .def( + "extract_loop_as_list", + [](const CifDict& self, absl::string_view prefix) { + absl::StatusOr>> + list_dict; + { + py::gil_scoped_release gil_release; + list_dict = self.ExtractLoopAsList(prefix); + if (!list_dict.ok()) { + throw py::value_error(list_dict.status().ToString()); + } + } + py::list list_obj(list_dict->size()); + size_t index = 0; + for (const auto& value : *list_dict) { + py::dict value_dict; + for (const auto& [key, value] : value) { + value_dict[py::cast(key)] = py::cast(value); + } + list_obj[index++] = std::move(value_dict); + } + return list_obj; + }, + py::arg("prefix")) + .def(py::pickle( + [](const CifDict& self) { // __getstate__. + py::tuple result_tuple(1); + py::dict result; + for (const auto& [key, value] : *self.dict()) { + result[py::cast(key)] = py::cast(value); + } + result_tuple[0] = std::move(result); + return result_tuple; + }, + [](py::tuple t) { // __setstate__. + py::dict dict = t[0].cast(); + CifDict::Dict result; + for (const auto& [key, value] : dict) { + result.emplace(py::cast(key), + py::cast>(value)); + } + return CifDict(std::move(result)); + })); + + // Item, value, and key views + struct KeyView { + CifDict map; + }; + + struct ValueView { + CifDict map; + }; + struct ItemView { + CifDict map; + }; + + py::class_(cif_dict, "ItemView") + .def("__len__", [](const ItemView& v) { return v.map.dict()->size(); }) + .def( + "__iter__", + [](const ItemView& v) { + return py::make_iterator(v.map.dict()->begin(), + v.map.dict()->end()); + }, + py::keep_alive<0, 1>()); + + py::class_(cif_dict, "KeyView") + .def("__contains__", + [](const KeyView& v, absl::string_view k) { + return v.map.dict()->find(k) != v.map.dict()->end(); + }) + .def("__contains__", [](const KeyView&, py::handle) { return false; }) + .def("__len__", [](const KeyView& v) { return v.map.dict()->size(); }) + .def( + "__iter__", + [](const KeyView& v) { + return py::make_key_iterator(v.map.dict()->begin(), + v.map.dict()->end()); + }, + py::keep_alive<0, 1>()); + + py::class_(cif_dict, "ValueView") + .def("__len__", [](const ValueView& v) { return v.map.dict()->size(); }) + .def( + "__iter__", + [](const ValueView& v) { + return py::make_value_iterator(v.map.dict()->begin(), + v.map.dict()->end()); + }, + py::keep_alive<0, 1>()); + + cif_dict + .def( + "__iter__", + [](CifDict& self) { + return py::make_key_iterator(self.dict()->begin(), + self.dict()->end()); + }, + py::keep_alive<0, 1>()) + .def( + "keys", [](CifDict& self) { return KeyView{self}; }, + "Returns an iterable view of the map's keys.") + .def( + "values", [](CifDict& self) { return ValueView{self}; }, + "Returns an iterable view of the map's values.") + .def( + "items", [](CifDict& self) { return ItemView{self}; }, + "Returns an iterable view of the map's items."); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.h new file mode 100644 index 000000000..ca4f94702 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleCifDict(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_CIF_DICT_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator.pyi new file mode 100644 index 000000000..d5da60ec8 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator.pyi @@ -0,0 +1,22 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +class FastaFileIterator: + def __init__(self, fasta_path: str) -> None: ... + def __iter__(self) -> FastaFileIterator: ... + def __next__(self) -> tuple[str,str]: ... + +class FastaStringIterator: + def __init__(self, fasta_string: str | bytes) -> None: ... + def __iter__(self) -> FastaStringIterator: ... + def __next__(self) -> tuple[str,str]: ... + +def parse_fasta(fasta_string: str | bytes) -> list[str]: ... +def parse_fasta_include_descriptions(fasta_string: str | bytes) -> tuple[list[str],list[str]]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.cc new file mode 100644 index 000000000..82cac9343 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.cc @@ -0,0 +1,121 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include "alphafold3/parsers/cpp/fasta_iterator_lib.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/ascii.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "absl/strings/string_view.h" +#include "absl/strings/strip.h" + +namespace alphafold3 { + +// Parse FASTA string and return list of strings with amino acid sequences. +// Returns a list of amino acid sequences only. +std::vector ParseFasta(absl::string_view fasta_string) { + std::vector sequences; + std::string* sequence = nullptr; + for (absl::string_view line_raw : absl::StrSplit(fasta_string, '\n')) { + absl::string_view line = absl::StripAsciiWhitespace(line_raw); + if (absl::ConsumePrefix(&line, ">")) { + sequence = &sequences.emplace_back(); + } else if (!line.empty() && sequence != nullptr) { + absl::StrAppend(sequence, line); + } + } + return sequences; +} + +// Parse FASTA string and return list of strings with amino acid sequences. +// Returns two lists: The first one with amino acid sequences, the second with +// the descriptions associated with each sequence. +std::pair, std::vector> +ParseFastaIncludeDescriptions(absl::string_view fasta_string) { + std::pair, std::vector> result; + auto& [sequences, descriptions] = result; + std::string* sequence = nullptr; + for (absl::string_view line_raw : absl::StrSplit(fasta_string, '\n')) { + absl::string_view line = absl::StripAsciiWhitespace(line_raw); + if (absl::ConsumePrefix(&line, ">")) { + descriptions.emplace_back(line); + sequence = &sequences.emplace_back(); + } else if (!line.empty() && sequence != nullptr) { + absl::StrAppend(sequence, line); + } + } + return result; +} + +absl::StatusOr> FastaFileIterator::Next() { + std::string line_str; + while (std::getline(reader_, line_str)) { + absl::string_view line = line_str; + line = absl::StripAsciiWhitespace(line); + if (absl::ConsumePrefix(&line, ">")) { + if (!description_.has_value()) { + description_ = line; + } else { + std::pair output(sequence_, *description_); + description_ = line; + sequence_ = ""; + return output; + } + } else if (description_.has_value()) { + absl::StrAppend(&sequence_, line); + } + } + has_next_ = false; + reader_.close(); + if (description_.has_value()) { + return std::pair(sequence_, *description_); + } else { + return absl::InvalidArgumentError( + absl::StrCat("Invalid FASTA file: ", filename_)); + } +} + +absl::StatusOr> +FastaStringIterator::Next() { + size_t consumed = 0; + for (absl::string_view line_raw : absl::StrSplit(fasta_string_, '\n')) { + consumed += line_raw.size() + 1; // +1 for the newline character. + absl::string_view line = absl::StripAsciiWhitespace(line_raw); + if (absl::ConsumePrefix(&line, ">")) { + if (!description_.has_value()) { + description_ = line; + } else { + std::pair output(sequence_, *description_); + description_ = line; + sequence_ = ""; + fasta_string_.remove_prefix(consumed); + return output; + } + } else if (description_.has_value()) { + absl::StrAppend(&sequence_, line); + } + } + has_next_ = false; + if (description_.has_value()) { + return std::pair(sequence_, *description_); + } else { + return absl::InvalidArgumentError("Invalid FASTA string"); + } +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.h new file mode 100644 index 000000000..486d05f20 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.h @@ -0,0 +1,94 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +// A C++ implementation of a FASTA parser. +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" + +namespace alphafold3 { + +// Parse FASTA string and return list of strings with amino acid sequences. +// Returns a list of amino acid sequences only. +std::vector ParseFasta(absl::string_view fasta_string); + +// Parse FASTA string and return list of strings with amino acid sequences. +// Returns two lists: The first one with amino acid sequences, the second with +// the descriptions associated with each sequence. +std::pair, std::vector> +ParseFastaIncludeDescriptions(absl::string_view fasta_string); + +// Lazy FASTA parser for memory efficient FASTA parsing from a path. +class FastaFileIterator { + public: + // Initialise FastaFileIterator with filename of fasta. If you initialize + // reader_ with an invalid path or empty file, it won't fail, only + // riegeli::ReadLine within the Next method will then return false. That will + // then trigger the "Invalid FASTA file" error. + explicit FastaFileIterator(absl::string_view fasta_path) + : filename_(fasta_path), + reader_(filename_, std::ios::in), + has_next_(true) {} + + // Returns whether there are more sequences. Returns true before first call to + // next even if the file is empty. + bool HasNext() const { return has_next_; } + + // Fetches the next (sequence, description) from the file. + absl::StatusOr> Next(); + + private: + // Use riegeli::FileReader instead of FileLineIterator for about 2x speedup. + std::string filename_; + std::fstream reader_; + std::optional description_; + std::string sequence_; + bool has_next_; +}; + +// Lazy FASTA parser for memory efficient FASTA parsing from a string. +class FastaStringIterator { + public: + // Initialise FastaStringIterator with a string_view of a FASTA. If you + // initialize it with an invalid FASTA string, it won't fail, the Next method + // will then return false. That will then trigger the "Invalid FASTA" error. + // WARNING: The object backing the fasta_string string_view must not be + // deleted while this Iterator is alive. + explicit FastaStringIterator(absl::string_view fasta_string) + : fasta_string_(fasta_string), has_next_(true) {} + + // Returns whether there are more sequences. Returns true before first call to + // next even if the string is empty. + bool HasNext() const { return has_next_; } + + // Fetches the next (sequence, description) from the string. + absl::StatusOr> Next(); + + private: + absl::string_view fasta_string_; + bool has_next_; + std::optional description_; + std::string sequence_; +}; + +} // namespace alphafold3 + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_LIB_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.cc new file mode 100644 index 000000000..0b47933d4 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.cc @@ -0,0 +1,127 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "alphafold3/parsers/cpp/fasta_iterator_lib.h" +#include "pybind11/attr.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" + +namespace alphafold3 { +namespace { + +namespace py = pybind11; + +template +T ValueOrThrowValueError(absl::StatusOr value) { + if (!value.ok()) throw py::value_error(value.status().ToString()); + return *std::move(value); +} + +constexpr char kFastaFileIteratorDoc[] = R"( +Lazy FASTA parser for memory efficient FASTA parsing from a path.)"; + +constexpr char kFastaStringIteratorDoc[] = R"( +Lazy FASTA parser for memory efficient FASTA parsing from a string. + +WARNING: The object backing the fasta_string string_view must not be +deleted while the FastaStringIterator is alive. E.g. this will break: + +``` +# Make sure the fasta_string is not interned. +fasta_string = '\n'.join(['>d\nS' for _ in range(10)]) +iterator = fasta_iterator.FastaStringIterator(fasta_string) +del fasta_string +iterator.next() # Heap use-after-free. +``` +)"; + +constexpr char kParseFastaDoc[] = R"( +Parses a FASTA string and returns a list of amino-acid sequences. + +Args: + fasta_string: The contents of a FASTA file. + +Returns: + List of sequences in the FASTA file. Descriptions are ignored. +)"; + +constexpr char kParseFastaIncludeDescriptionsDoc[] = R"( +Parses a FASTA string, returns amino-acid sequences with descriptions. + +Args: + fasta_string: The contents of a FASTA file. + +Returns: + A tuple with two lists (sequences, descriptions): + * A list of sequences. + * A list of sequence descriptions taken from the comment lines. In the + same order as the sequences. +)"; + +class PythonFastaStringIterator : public FastaStringIterator { + public: + explicit PythonFastaStringIterator(py::object fasta_string) + : FastaStringIterator(py::cast(fasta_string)), + fasta_string_(std::move(fasta_string)) {} + + private: + py::object fasta_string_; +}; + +} // namespace + +void RegisterModuleFastaIterator(pybind11::module m) { + py::class_(m, "FastaFileIterator", kFastaFileIteratorDoc) + .def(py::init(), py::arg("fasta_path")) + .def("__iter__", + [](FastaFileIterator& iterator) -> FastaFileIterator& { + return iterator; + }) + .def( + "__next__", + [](FastaFileIterator& iterator) { + if (iterator.HasNext()) { + return ValueOrThrowValueError(iterator.Next()); + } else { + throw py::stop_iteration(); + } + }, + py::call_guard()); + + py::class_(m, "FastaStringIterator", + kFastaStringIteratorDoc) + .def(py::init(), py::arg("fasta_string")) + .def("__iter__", + [](PythonFastaStringIterator& iterator) + -> PythonFastaStringIterator& { return iterator; }) + .def( + "__next__", + [](PythonFastaStringIterator& iterator) { + if (iterator.HasNext()) { + return ValueOrThrowValueError(iterator.Next()); + } else { + throw py::stop_iteration(); + } + }, + py::call_guard()); + + m.def("parse_fasta", &ParseFasta, py::arg("fasta_string"), + py::call_guard(), py::doc(kParseFastaDoc + 1)); + m.def("parse_fasta_include_descriptions", &ParseFastaIncludeDescriptions, + py::arg("fasta_string"), py::call_guard(), + py::doc(kParseFastaIncludeDescriptionsDoc + 1)); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.h new file mode 100644 index 000000000..091ea3fa2 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleFastaIterator(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_FASTA_ITERATOR_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion.pyi new file mode 100644 index 000000000..3602032b9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion.pyi @@ -0,0 +1,26 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Type annotations for Python bindings for `msa_conversion`. + +The type annotations in this file were modified from the automatically generated +stubgen output. +""" + +from collections.abc import Iterable + + +def align_sequence_to_gapless_query( + sequence: str | bytes, + query_sequence: str | bytes, +) -> str: ... + + +def convert_a3m_to_stockholm(a3m_sequences: Iterable[str]) -> list[str]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc new file mode 100644 index 000000000..c192052f0 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc @@ -0,0 +1,162 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include +#include +#include +#include +#include + +#include "absl/strings/ascii.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "pybind11/pybind11.h" +#include "pybind11/stl.h" + +namespace { + +namespace py = pybind11; + +std::vector ConvertA3MToStockholm( + std::vector a3m_sequences) { + std::vector stockholm_sequences(a3m_sequences.size()); + auto max_length_element = + std::max_element(a3m_sequences.begin(), a3m_sequences.end(), + [](absl::string_view lhs, absl::string_view rhs) { + return lhs.size() < rhs.size(); + }); + + for (auto& out : stockholm_sequences) { + out.reserve(max_length_element->size()); + } + + // While any sequence has remaining columns. + while (std::any_of(a3m_sequences.begin(), a3m_sequences.end(), + [](absl::string_view in) { return !in.empty(); })) { + if (std::any_of(a3m_sequences.begin(), a3m_sequences.end(), + [](absl::string_view in) { + return !in.empty() && absl::ascii_islower(in.front()); + })) { + // Insertion(s) found at column. + for (std::size_t i = 0; i < a3m_sequences.size(); ++i) { + absl::string_view& in = a3m_sequences[i]; + std::string& out = stockholm_sequences[i]; + if (!in.empty() && absl::ascii_islower(in.front())) { + // Consume insertion. + out.push_back(absl::ascii_toupper(in.front())); + in.remove_prefix(1); + } else { + // Row requires padding. + out.push_back('-'); + } + } + } else { + // No insertions found. + for (std::size_t i = 0; i < a3m_sequences.size(); ++i) { + absl::string_view& in = a3m_sequences[i]; + std::string& out = stockholm_sequences[i]; + if (!in.empty()) { + // Consume entire column. + out.push_back(in.front()); + in.remove_prefix(1); + } else { + // One alignment is shorter than the others. Should not happen with + // valid A3M input. + throw std::invalid_argument(absl::StrFormat( + "a3m rows have inconsistent lengths; row %d has no columns left " + "but not all rows are exhausted", + i)); + } + } + } + } + return stockholm_sequences; +} + +std::string AlignSequenceToGaplessQuery(absl::string_view sequence, + absl::string_view query_sequence) { + if (sequence.size() != query_sequence.size()) { + throw py::value_error( + absl::StrFormat("The sequence (%d) and the query sequence (%d) don't " + "have the same length.", + sequence.size(), query_sequence.size())); + } + std::string output; + for (std::size_t residue_index = 0, sequence_length = sequence.size(); + residue_index < sequence_length; ++residue_index) { + const char query_residue = query_sequence[residue_index]; + const char residue = sequence[residue_index]; + if (query_residue != '-') { + // No gap in the query, so the residue is aligned. + output += residue; + } else if (residue == '-') { + // Gap in both sequence and query, simply skip. + continue; + } else { + // Gap only in the query, so this must be an inserted residue. + output += absl::ascii_tolower(residue); + } + } + return output; +} + +constexpr char kConvertA3mToStockholm[] = R"( +Converts a list of sequences in a3m format to stockholm format sequences. + +As an example if the input is: +abCD +CgD +fCDa + +Then the output will be: +ABC-D- +--CGD- +F-C-DA + +Args: + a3m_sequences: A list of strings in a3m format. + +Returns + A list of strings converted to stockholm format. +)"; + +constexpr char kAlignSequenceToGaplessQuery[] = R"( +Aligns a sequence to a gapless query sequence. + +This is useful when converting Stockholm MSA to A3M MSA. Example: +Seq : AB--E +Query: A--DE +Output: Ab-E. + +Args: + sequence: A string containing to be aligned. + query_sequence: A string containing the reference sequence to align to. + +Returns + The input sequence with gaps dropped where both the `sequence` and + `query_sequence` have gaps, and sequence elements non-capitalized where the + `query_sequence` has a gap, but the `sequence` does not. +)"; + +} // namespace + +namespace alphafold3 { + +void RegisterModuleMsaConversion(pybind11::module m) { + m.def("convert_a3m_to_stockholm", &ConvertA3MToStockholm, + py::arg("a3m_sequences"), py::call_guard(), + py::doc(kConvertA3mToStockholm + 1)); + m.def("align_sequence_to_gapless_query", &AlignSequenceToGaplessQuery, + py::arg("sequence"), py::arg("query_sequence"), + py::call_guard(), + py::doc(kAlignSequenceToGaplessQuery + 1)); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.h new file mode 100644 index 000000000..65f5fe99e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMsaConversion(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_PARSERS_PYTHON_MSA_CONVERSION_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/__init__.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/__init__.py new file mode 100644 index 000000000..17f44cd06 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/__init__.py @@ -0,0 +1,46 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Structure module initialization.""" + +# pylint: disable=g-importing-member +from alphafold3.structure.bioassemblies import BioassemblyData +from alphafold3.structure.bonds import Bonds +from alphafold3.structure.chemical_components import ChemCompEntry +from alphafold3.structure.chemical_components import ChemicalComponentsData +from alphafold3.structure.chemical_components import get_data_for_ccd_components +from alphafold3.structure.chemical_components import populate_missing_ccd_data +from alphafold3.structure.mmcif import BondParsingError +from alphafold3.structure.parsing import BondAtomId +from alphafold3.structure.parsing import from_atom_arrays +from alphafold3.structure.parsing import from_mmcif +from alphafold3.structure.parsing import from_parsed_mmcif +from alphafold3.structure.parsing import from_res_arrays +from alphafold3.structure.parsing import from_sequences_and_bonds +from alphafold3.structure.parsing import ModelID +from alphafold3.structure.parsing import SequenceFormat +from alphafold3.structure.structure import ARRAY_FIELDS +from alphafold3.structure.structure import AuthorNamingScheme +from alphafold3.structure.structure import Bond +from alphafold3.structure.structure import CascadeDelete +from alphafold3.structure.structure import concat +from alphafold3.structure.structure import enumerate_residues +from alphafold3.structure.structure import fix_non_standard_polymer_residues +from alphafold3.structure.structure import GLOBAL_FIELDS +from alphafold3.structure.structure import make_empty_structure +from alphafold3.structure.structure import MissingAtomError +from alphafold3.structure.structure import MissingAuthorResidueIdError +from alphafold3.structure.structure import multichain_residue_index +from alphafold3.structure.structure import stack +from alphafold3.structure.structure import Structure +from alphafold3.structure.structure_tables import Atoms +from alphafold3.structure.structure_tables import Chains +from alphafold3.structure.structure_tables import Residues diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bioassemblies.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bioassemblies.py new file mode 100644 index 000000000..6c1d8e3cc --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bioassemblies.py @@ -0,0 +1,333 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Utilities for parsing and manipulating bioassembly data.""" + +from collections.abc import Mapping, Sequence +import copy +import dataclasses +from typing_extensions import Self + +from alphafold3.structure import mmcif +import numpy as np + + +@dataclasses.dataclass(frozen=True) +class Operation: + """A rigid transformation operation.""" + + trans: np.ndarray # shape: (3,) + rot: np.ndarray # shape: (3, 3) + + def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: + """Applies the rotation followed by the translation to `coords`.""" + return np.dot(coords, self.rot.T) + self.trans[np.newaxis, :] + + +@dataclasses.dataclass(frozen=True) +class Transform: + """A rigid transformation composed of a sequence of `Operation`s.""" + + # The sequence of operations that form the transform. These will be applied + # right-to-left (last-to-first). + operations: Sequence[Operation] + + # The chain IDs that this transform should be applied to. These are + # label_asym_ids in the mmCIF spec. + chain_ids: Sequence[str] + + # A mapping from chain IDs (of chains that participate in this transform) + # to their new values in the bioassembly. + chain_id_rename_map: Mapping[str, str] + + def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: + """Applies the `operations` in right-to-left order.""" + for operation in reversed(self.operations): + coords = operation.apply_to_coords(coords) + return coords + + +def _get_operation(oper_data: Mapping[str, str]) -> Operation: + """Parses an `Operation` from a mmCIF _pdbx_struct_oper_list row.""" + trans = np.zeros((3,), dtype=np.float32) + rot = np.zeros((3, 3), dtype=np.float32) + for i in range(3): + trans[i] = float(oper_data[f'_pdbx_struct_oper_list.vector[{i + 1}]']) + for i in range(3): + for j in range(3): + rot[i][j] = float( + oper_data[f'_pdbx_struct_oper_list.matrix[{i + 1}][{j + 1}]'] + ) + return Operation(trans=trans, rot=rot) + + +class MissingBioassemblyDataError(Exception): + """Raised when bioassembly data is missing from an mmCIF.""" + + +class BioassemblyData: + """Stores and processes bioassembly data from mmCIF tables.""" + + # Not all of these columns are required for internal operations, but all + # should be present whenever bioassemblies are defined in an mmCIF to stay + # consistent with external mmCIFs. + _REQUIRED_COLUMNS = ( + '_pdbx_struct_assembly.id', + '_pdbx_struct_assembly.details', + '_pdbx_struct_assembly.method_details', + '_pdbx_struct_assembly.oligomeric_details', + '_pdbx_struct_assembly.oligomeric_count', + '_pdbx_struct_assembly_gen.assembly_id', + '_pdbx_struct_assembly_gen.oper_expression', + '_pdbx_struct_assembly_gen.asym_id_list', + '_pdbx_struct_oper_list.id', + '_pdbx_struct_oper_list.type', + '_pdbx_struct_oper_list.name', + '_pdbx_struct_oper_list.symmetry_operation', + '_pdbx_struct_oper_list.matrix[1][1]', + '_pdbx_struct_oper_list.matrix[1][2]', + '_pdbx_struct_oper_list.matrix[1][3]', + '_pdbx_struct_oper_list.vector[1]', + '_pdbx_struct_oper_list.matrix[2][1]', + '_pdbx_struct_oper_list.matrix[2][2]', + '_pdbx_struct_oper_list.matrix[2][3]', + '_pdbx_struct_oper_list.vector[2]', + '_pdbx_struct_oper_list.matrix[3][1]', + '_pdbx_struct_oper_list.matrix[3][2]', + '_pdbx_struct_oper_list.matrix[3][3]', + '_pdbx_struct_oper_list.vector[3]', + ) + + def __init__( + self, + *, + pdbx_struct_assembly: Mapping[str, Mapping[str, str]], + pdbx_struct_assembly_gen: Mapping[str, Sequence[Mapping[str, str]]], + pdbx_struct_oper_list: Mapping[str, Mapping[str, str]], + assembly_ids: Sequence[str], + oper_ids: Sequence[str], + ): + for assembly_id in assembly_ids: + for table, table_name in ( + (pdbx_struct_assembly, '_pdbx_struct_assembly'), + (pdbx_struct_assembly_gen, '_pdbx_struct_assembly_gen'), + ): + if assembly_id not in table: + raise ValueError( + f'Assembly ID "{assembly_id}" missing from {table_name} ' + f'with keys: {table.keys()}' + ) + for oper_id in oper_ids: + if oper_id not in pdbx_struct_oper_list: + raise ValueError( + f'Oper ID "{oper_id}" missing from _pdbx_struct_oper_list ' + f'with keys: {pdbx_struct_oper_list.keys()}' + ) + + self._pdbx_struct_assembly = pdbx_struct_assembly + self._pdbx_struct_assembly_gen = pdbx_struct_assembly_gen + self._pdbx_struct_oper_list = pdbx_struct_oper_list + self._operations = { + oper_id: _get_operation(oper_data) + for oper_id, oper_data in self._pdbx_struct_oper_list.items() + } + self._assembly_ids = assembly_ids + self._oper_ids = oper_ids + + @classmethod + def from_mmcif(cls, cif: mmcif.Mmcif) -> Self: + """Constructs an instance of `BioassemblyData` from an `Mmcif` object.""" + for col in cls._REQUIRED_COLUMNS: + if col not in cif: + raise MissingBioassemblyDataError(col) + + pdbx_struct_assembly = cif.extract_loop_as_dict( + prefix='_pdbx_struct_assembly.', index='_pdbx_struct_assembly.id' + ) + pdbx_struct_oper_list = cif.extract_loop_as_dict( + prefix='_pdbx_struct_oper_list.', index='_pdbx_struct_oper_list.id' + ) + + # _pdbx_struct_assembly_gen is unlike the other two tables because it can + # have multiple rows share the same assembly ID. This can happen when an + # assembly is constructed by applying different sets of transforms to + # different sets of chain IDs. Each of these would have its own row. + # Here we group rows by their assembly_id. + pdbx_struct_assembly_gen = {} + for assembly_id, oper_expression, asym_id_list in zip( + cif['_pdbx_struct_assembly_gen.assembly_id'], + cif['_pdbx_struct_assembly_gen.oper_expression'], + cif['_pdbx_struct_assembly_gen.asym_id_list'], + ): + pdbx_struct_assembly_gen.setdefault(assembly_id, []).append({ + '_pdbx_struct_assembly_gen.assembly_id': assembly_id, + '_pdbx_struct_assembly_gen.oper_expression': oper_expression, + '_pdbx_struct_assembly_gen.asym_id_list': asym_id_list, + }) + + # We provide these separately to keep track of the original order that they + # appear in the mmCIF. + assembly_ids = cif['_pdbx_struct_assembly.id'] + oper_ids = cif['_pdbx_struct_oper_list.id'] + return cls( + pdbx_struct_assembly=pdbx_struct_assembly, + pdbx_struct_assembly_gen=pdbx_struct_assembly_gen, + pdbx_struct_oper_list=pdbx_struct_oper_list, + assembly_ids=assembly_ids, + oper_ids=oper_ids, + ) + + @property + def assembly_ids(self) -> Sequence[str]: + return self._assembly_ids + + def asym_id_by_assembly_chain_id(self, assembly_id: str) -> Mapping[str, str]: + asym_id_by_assembly_chain_id = {} + for transform in self.get_transforms(assembly_id): + for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): + asym_id_by_assembly_chain_id[assembly_chain_id] = asym_id + return asym_id_by_assembly_chain_id + + def assembly_chain_ids_by_asym_id( + self, assembly_id: str + ) -> Mapping[str, set[str]]: + assembly_chain_ids_by_asym_id = {} + for transform in self.get_transforms(assembly_id): + for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): + assembly_chain_ids_by_asym_id.setdefault(asym_id, set()).add( + assembly_chain_id + ) + return assembly_chain_ids_by_asym_id + + def get_default_assembly_id(self) -> str: + """Gets a default assembly ID.""" + # The first assembly is usually (though not always) the best choice. + # If we find a better heuristic for picking bioassemblies then this + # method should be updated. + return min(self._assembly_ids) + + def get_assembly_info(self, assembly_id: str) -> Mapping[str, str]: + return { + k.replace('_pdbx_struct_assembly.', ''): v + for k, v in self._pdbx_struct_assembly[assembly_id].items() + } + + def get_transforms(self, assembly_id: str) -> Sequence[Transform]: + """Returns the transforms required to generate the given assembly.""" + partial_transforms = [] + all_chain_ids = set() + for row in self._pdbx_struct_assembly_gen[assembly_id]: + oper_expression = row['_pdbx_struct_assembly_gen.oper_expression'] + parsed_oper_id_seqs = mmcif.parse_oper_expr(oper_expression) + label_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split( + ',') + all_chain_ids |= set(label_asym_ids) + for parsed_oper_id_seq in parsed_oper_id_seqs: + partial_transforms.append((parsed_oper_id_seq, label_asym_ids)) + + # We start assigning new chain IDs by finding the largest chain ID in + # the original structure that is involved in this bioassembly, and then + # starting from the next one. + max_int_chain_id = max(mmcif.str_id_to_int_id(c) + for c in all_chain_ids) + next_int_chain_id = max_int_chain_id + 1 + + transforms = [] + has_been_renamed = set() + for parsed_oper_id_seq, label_asym_ids in partial_transforms: + chain_id_rename_map = {} + for label_asym_id in label_asym_ids: + if label_asym_id not in has_been_renamed: + # The first time we see a label_asym_id we don't need to rename it. + # This isn't strictly necessary since we don't provide any + # guarantees about chain naming after bioassembly extraction but + # can make it a bit easier to inspect and compare structures + # pre and post bioassembly extraction. + chain_id_rename_map[label_asym_id] = label_asym_id + has_been_renamed.add(label_asym_id) + else: + chain_id_rename_map[label_asym_id] = mmcif.int_id_to_str_id( + next_int_chain_id + ) + next_int_chain_id += 1 + transforms.append( + Transform( + operations=[ + self._operations[oper_id] for oper_id in parsed_oper_id_seq + ], + chain_ids=label_asym_ids, + chain_id_rename_map=chain_id_rename_map, + ) + ) + return transforms + + def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]: + """Returns the bioassembly data as a dict suitable for `mmcif.Mmcif`.""" + mmcif_dict = {} + for assembly_id in self._assembly_ids: + for column, val in self._pdbx_struct_assembly[assembly_id].items(): + mmcif_dict.setdefault(column, []).append(val) + for row in self._pdbx_struct_assembly_gen[assembly_id]: + for column, val in row.items(): + mmcif_dict.setdefault(column, []).append(val) + for oper_id in self._oper_ids: + for column, val in self._pdbx_struct_oper_list[oper_id].items(): + mmcif_dict.setdefault(column, []).append(val) + return mmcif_dict + + def rename_label_asym_ids( + self, + mapping: Mapping[str, str], + present_chains: set[str], + ) -> Self: + """Returns a new BioassemblyData with renamed label_asym_ids. + + Args: + mapping: A mapping from original label_asym_ids to their new values. Any + label_asym_ids in this BioassemblyData that are not in this mapping will + remain unchanged. + present_chains: A set of label_asym_ids that are actually present in the + atom site list. All label_asym_ids that are in the BioassemblyData but + not in present_chains won't be included in the output BioassemblyData. + + Returns: + A new BioassemblyData with renamed label_asym_ids. + + Raises: + ValueError: If any two previously distinct chains do not have unique names + anymore after the rename. + """ + new_pdbx_struct_assembly_gen = copy.deepcopy( + self._pdbx_struct_assembly_gen) + for rows in new_pdbx_struct_assembly_gen.values(): + for row in rows: + old_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split( + ',') + new_asym_ids = [ + mapping.get(label_asym_id, label_asym_id) + for label_asym_id in old_asym_ids + if label_asym_id in present_chains + ] + if len(set(old_asym_ids) & present_chains) != len(set(new_asym_ids)): + raise ValueError( + 'Can not rename chains, the new names are not unique: ' + f'{sorted(new_asym_ids)}.' + ) + row['_pdbx_struct_assembly_gen.asym_id_list'] = ','.join( + new_asym_ids) # pytype: disable=unsupported-operands + + return BioassemblyData( + pdbx_struct_assembly=copy.deepcopy(self._pdbx_struct_assembly), + pdbx_struct_assembly_gen=new_pdbx_struct_assembly_gen, + pdbx_struct_oper_list=copy.deepcopy(self._pdbx_struct_oper_list), + assembly_ids=copy.deepcopy(self._assembly_ids), + oper_ids=copy.deepcopy(self._oper_ids), + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bonds.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bonds.py new file mode 100644 index 000000000..94689d797 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bonds.py @@ -0,0 +1,237 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Bond representation for structure module.""" + +import collections +from collections.abc import Mapping, Sequence +import dataclasses +import typing +from typing_extensions import Self + +from alphafold3.structure import table +import numpy as np + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Bonds(table.Table): + """Table of atomic bonds.""" + + # mmCIF column: _struct_conn.conn_type_id + # mmCIF desc: This data item is a pointer to _struct_conn_type.id in the + # STRUCT_CONN_TYPE category. + # E.g.: "covale", "disulf", "hydrog", "metalc". + type: np.ndarray + + # mmCIF column: _struct_conn.pdbx_role + # mmCIF desc: The chemical or structural role of the interaction. + # E.g.: "N-Glycosylation", "O-Glycosylation". + role: np.ndarray + + # mmCIF columns: _struct_conn.ptnr1_* + from_atom_key: np.ndarray + + # mmCIF columns: _struct_conn.ptnr2_* + dest_atom_key: np.ndarray + + @classmethod + def make_empty(cls) -> Self: + return cls( + key=np.empty((0,), dtype=np.int64), + from_atom_key=np.empty((0,), dtype=np.int64), + dest_atom_key=np.empty((0,), dtype=np.int64), + type=np.empty((0,), dtype=object), + role=np.empty((0,), dtype=object), + ) + + def get_atom_indices( + self, + atom_key: np.ndarray, + ) -> tuple[np.ndarray, np.ndarray]: + """Returns the indices of the from/dest atoms in the atom_key array.""" + from_atom_missing = ~np.isin(self.from_atom_key, atom_key) + dest_atom_missing = ~np.isin(self.dest_atom_key, atom_key) + if np.any(from_atom_missing): + raise ValueError( + f'No atoms for from_atom_key {self.from_atom_key[from_atom_missing]}' + ) + if np.any(dest_atom_missing): + raise ValueError( + f'No atoms for dest_atom_key {self.dest_atom_key[dest_atom_missing]}' + ) + sort_indices = np.argsort(atom_key) + from_indices_sorted = np.searchsorted( + atom_key, self.from_atom_key, sorter=sort_indices + ) + dest_indices_sorted = np.searchsorted( + atom_key, self.dest_atom_key, sorter=sort_indices + ) + from_indices = sort_indices[from_indices_sorted] + dest_indices = sort_indices[dest_indices_sorted] + return from_indices, dest_indices + + def restrict_to_atoms(self, atom_key: np.ndarray) -> Self: + if not self.size: # Early-out for empty table. + return self + from_atom_mask = np.isin(self.from_atom_key, atom_key) + dest_atom_mask = np.isin(self.dest_atom_key, atom_key) + mask = np.logical_and(from_atom_mask, dest_atom_mask) + return typing.cast(Bonds, self.filter(mask=mask)) + + def to_mmcif_dict_from_atom_arrays( + self, + atom_key: np.ndarray, + chain_id: np.ndarray, + res_id: np.ndarray, + res_name: np.ndarray, + atom_name: np.ndarray, + auth_asym_id: np.ndarray, + auth_seq_id: np.ndarray, + insertion_code: np.ndarray, + ) -> Mapping[str, Sequence[str] | np.ndarray]: + """Returns a dict suitable for building a CifDict, representing bonds. + + Args: + atom_key: A (num_atom,) integer array of atom_keys. + chain_id: A (num_atom,) array of label_asym_id strings. + res_id: A (num_atom,) array of label_seq_id strings. + res_name: A (num_atom,) array of label_comp_id strings. + atom_name: A (num_atom,) array of label_atom_id strings. + auth_asym_id: A (num_atom,) array of auth_asym_id strings. + auth_seq_id: A (num_atom,) array of auth_seq_id strings. + insertion_code: A (num_atom,) array of insertion code strings. + """ + mmcif_dict = collections.defaultdict(list) + ptnr1_indices, ptnr2_indices = self.get_atom_indices(atom_key) + + mmcif_dict['_struct_conn.ptnr1_label_asym_id'] = chain_id[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_label_asym_id'] = chain_id[ptnr2_indices] + mmcif_dict['_struct_conn.ptnr1_label_comp_id'] = res_name[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_label_comp_id'] = res_name[ptnr2_indices] + mmcif_dict['_struct_conn.ptnr1_label_seq_id'] = res_id[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_label_seq_id'] = res_id[ptnr2_indices] + mmcif_dict['_struct_conn.ptnr1_label_atom_id'] = atom_name[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_label_atom_id'] = atom_name[ptnr2_indices] + + mmcif_dict['_struct_conn.ptnr1_auth_asym_id'] = auth_asym_id[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_auth_asym_id'] = auth_asym_id[ptnr2_indices] + mmcif_dict['_struct_conn.ptnr1_auth_seq_id'] = auth_seq_id[ptnr1_indices] + mmcif_dict['_struct_conn.ptnr2_auth_seq_id'] = auth_seq_id[ptnr2_indices] + mmcif_dict['_struct_conn.pdbx_ptnr1_PDB_ins_code'] = insertion_code[ + ptnr1_indices + ] + mmcif_dict['_struct_conn.pdbx_ptnr2_PDB_ins_code'] = insertion_code[ + ptnr2_indices + ] + + label_alt_id = ['?'] * self.size + mmcif_dict['_struct_conn.pdbx_ptnr1_label_alt_id'] = label_alt_id + mmcif_dict['_struct_conn.pdbx_ptnr2_label_alt_id'] = label_alt_id + + # We need to set this to make visualisation work in NGL/PyMOL. + mmcif_dict['_struct_conn.pdbx_value_order'] = ['?'] * self.size + + # We use a symmetry of 1_555 which is the no-op transformation. Other + # values are used when bonds involve atoms that only exist after expanding + # the bioassembly, but we don't support this kind of bond at the moment. + symmetry = ['1_555'] * self.size + mmcif_dict['_struct_conn.ptnr1_symmetry'] = symmetry + mmcif_dict['_struct_conn.ptnr2_symmetry'] = symmetry + bond_type_counter = collections.Counter() + for bond_row in self.iterrows(): + bond_type = bond_row['type'] + bond_type_counter[bond_type] += 1 + mmcif_dict['_struct_conn.id'].append( + f'{bond_type}{bond_type_counter[bond_type]}' + ) + mmcif_dict['_struct_conn.pdbx_role'].append(bond_row['role']) + mmcif_dict['_struct_conn.conn_type_id'].append(bond_type) + + bond_types = np.unique(self.type) + mmcif_dict['_struct_conn_type.id'] = bond_types + unknown = ['?'] * len(bond_types) + mmcif_dict['_struct_conn_type.criteria'] = unknown + mmcif_dict['_struct_conn_type.reference'] = unknown + + return dict(mmcif_dict) + + +def concat_with_atom_keys( + bonds_tables: Sequence[Bonds | None], + atom_key_arrays: Sequence[np.ndarray], +) -> tuple[Bonds | None, np.ndarray]: + """Concatenates bonds tables and atom keys simultaneously. + + Args: + bonds_tables: A sequence of `Bonds` instances to concatenate. If any are + None then these are skipped. + atom_key_arrays: A sequence of integer `atom_key` arrays, where the n-th + bonds_table refers to the atoms in the n-th atom_key array. These must + all be non-None. + + Returns: + A pair of (bonds, atom_key) where atom_key is a unique atom_key array with + length equal to the sum of the input atom array sizes, and the bonds table + contains all the bonds from the individual bonds table inputs. + """ + if not bonds_tables or not atom_key_arrays: + if bonds_tables or atom_key_arrays: + raise ValueError( + 'bonds_tables and atom_keys must have same length but got' + f' {len(bonds_tables)=} and {len(atom_key_arrays)=}' + ) + return None, np.array([], dtype=np.int64) + max_key = -1 + atom_keys_to_concat = [] + types_to_concat = [] + roles_to_concat = [] + from_atom_keys_to_concat = [] + dest_atom_keys_to_concat = [] + for bonds, atom_key in zip(bonds_tables, atom_key_arrays, strict=True): + if not atom_key.size: + assert bonds is None or bonds.size == 0 + continue + # Should always be non-negative! + assert np.min(atom_key, initial=0) >= 0 + offset = max_key + 1 + offset_atom_key = atom_key + offset + atom_keys_to_concat.append(offset_atom_key) + max_key = np.max(offset_atom_key) + if bonds is not None: + types_to_concat.append(bonds.type) + roles_to_concat.append(bonds.role) + from_atom_keys_to_concat.append(bonds.from_atom_key + offset) + dest_atom_keys_to_concat.append(bonds.dest_atom_key + offset) + + if atom_keys_to_concat: + concatted_atom_keys = np.concatenate(atom_keys_to_concat, axis=0) + else: + concatted_atom_keys = np.array([], dtype=np.int64) + + if types_to_concat: + assert ( + len(types_to_concat) + == len(roles_to_concat) + == len(from_atom_keys_to_concat) + == len(dest_atom_keys_to_concat) + ) + num_bonds = sum(b.size for b in bonds_tables if b is not None) + concatted_bonds = Bonds( + key=np.arange(num_bonds, dtype=np.int64), + type=np.concatenate(types_to_concat, axis=0), + role=np.concatenate(roles_to_concat, axis=0), + from_atom_key=np.concatenate(from_atom_keys_to_concat, axis=0), + dest_atom_key=np.concatenate(dest_atom_keys_to_concat, axis=0), + ) + else: + concatted_bonds = None + + return concatted_bonds, concatted_atom_keys diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/chemical_components.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/chemical_components.py new file mode 100644 index 000000000..a25e91017 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/chemical_components.py @@ -0,0 +1,286 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Utilities for manipulating chemical components data.""" + +from collections.abc import Iterable, Mapping, Sequence +import dataclasses +import functools +from typing_extensions import Self + +from alphafold3.constants import chemical_components +from alphafold3.constants import residue_names +from alphafold3.structure import mmcif +import rdkit.Chem as rd_chem + + +@dataclasses.dataclass(frozen=True) +class ChemCompEntry: + """Items of _chem_comp category. + + For the full list of items and their semantics see + http://mmcif.rcsb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/chem_comp.html + """ + + type: str + name: str = '?' + pdbx_synonyms: str = '?' + formula: str = '?' + formula_weight: str = '?' + mon_nstd_flag: str = '?' + pdbx_smiles: str | None = None + + def __post_init__(self): + for field, value in vars(self).items(): + if not value and value is not None: + raise ValueError(f"{field} value can't be an empty string.") + + def extends(self, other: Self) -> bool: + """Checks whether this ChemCompEntry extends another one.""" + for field, value in vars(self).items(): + other_value = getattr(other, field) + if _value_is_missing(other_value): + continue + if value != other_value: + return False + return True + + @property + def rdkit_mol(self) -> rd_chem.Mol: + """Returns an RDKit Mol, created via RDKit from entry SMILES string.""" + if not self.pdbx_smiles: + raise ValueError( + 'Cannot construct RDKit Mol with empty pdbx_smiles') + return rd_chem.MolFromSmiles(self.pdbx_smiles) + + +_REQUIRED_MMCIF_COLUMNS = ('_chem_comp.id', '_chem_comp.type') + + +class MissingChemicalComponentsDataError(Exception): + """Raised when chemical components data is missing from an mmCIF.""" + + +@dataclasses.dataclass(frozen=True) +class ChemicalComponentsData: + """Extra information for chemical components occurring in mmCIF. + + Fields: + chem_comp: A mapping from _chem_comp.id to associated items in the + chem_comp category. + """ + + chem_comp: Mapping[str, ChemCompEntry] + + @classmethod + def from_mmcif( + cls, cif: mmcif.Mmcif, fix_mse: bool, fix_unknown_dna: bool + ) -> Self: + """Constructs an instance of ChemicalComponentsData from an Mmcif object.""" + for col in _REQUIRED_MMCIF_COLUMNS: + if col not in cif: + raise MissingChemicalComponentsDataError(col) + + id_ = cif['_chem_comp.id'] # Guaranteed to be present. + type_ = cif['_chem_comp.type'] # Guaranteed to be present. + name = cif.get('_chem_comp.name', ['?'] * len(id_)) + synonyms = cif.get('_chem_comp.pdbx_synonyms', ['?'] * len(id_)) + formula = cif.get('_chem_comp.formula', ['?'] * len(id_)) + weight = cif.get('_chem_comp.formula_weight', ['?'] * len(id_)) + mon_nstd_flag = cif.get('_chem_comp.mon_nstd_flag', ['?'] * len(id_)) + smiles = cif.get('_chem_comp.pdbx_smiles', ['?'] * len(id_)) + smiles = [None if s == '?' else s for s in smiles] + + chem_comp = { + component_name: ChemCompEntry(*entry) + for component_name, *entry in zip( + id_, type_, name, synonyms, formula, weight, mon_nstd_flag, smiles + ) + } + + if fix_mse and 'MSE' in chem_comp: + if 'MET' not in chem_comp: + chem_comp['MET'] = ChemCompEntry( + type='L-PEPTIDE LINKING', + name='METHIONINE', + pdbx_synonyms='?', + formula='C5 H11 N O2 S', + formula_weight='149.211', + mon_nstd_flag='y', + pdbx_smiles=None, + ) + + if fix_unknown_dna and 'N' in chem_comp: + # Do not delete 'N' as it may be needed for RNA in the system. + if 'DN' not in chem_comp: + chem_comp['DN'] = ChemCompEntry( + type='DNA LINKING', + name="UNKNOWN 2'-DEOXYNUCLEOTIDE", + pdbx_synonyms='?', + formula='C5 H11 O6 P', + formula_weight='198.111', + mon_nstd_flag='y', + pdbx_smiles=None, + ) + + return ChemicalComponentsData(chem_comp) + + def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]: + """Returns chemical components data as a dict suitable for `mmcif.Mmcif`.""" + mmcif_dict = {} + + mmcif_fields = set() + for entry in self.chem_comp.values(): + for field, value in vars(entry).items(): + if value: + mmcif_fields.add(field) + chem_comp_ids = [] + for component_id in sorted(self.chem_comp): + entry = self.chem_comp[component_id] + chem_comp_ids.append(component_id) + for field in mmcif_fields: + mmcif_dict.setdefault(f'_chem_comp.{field}', []).append( + getattr(entry, field) or '?' + ) + if chem_comp_ids: + mmcif_dict['_chem_comp.id'] = chem_comp_ids + return mmcif_dict + + +def _value_is_missing(value: str) -> bool: + return not value or value in ('.', '?') + + +def get_data_for_ccd_components( + ccd: chemical_components.Ccd, + chemical_component_ids: Iterable[str], + populate_pdbx_smiles: bool = False, +) -> ChemicalComponentsData: + """Returns `ChemicalComponentsData` for chemical components known by PDB.""" + chem_comp = {} + for chemical_component_id in chemical_component_ids: + chem_data = chemical_components.component_name_to_info( + ccd=ccd, res_name=chemical_component_id + ) + if not chem_data: + continue + chem_comp[chemical_component_id] = ChemCompEntry( + type=chem_data.type, + name=chem_data.name, + pdbx_synonyms=chem_data.pdbx_synonyms, + formula=chem_data.formula, + formula_weight=chem_data.formula_weight, + mon_nstd_flag=chem_data.mon_nstd_flag, + pdbx_smiles=( + chem_data.pdbx_smiles or None if populate_pdbx_smiles else None + ), + ) + return ChemicalComponentsData(chem_comp=chem_comp) + + +def populate_missing_ccd_data( + ccd: chemical_components.Ccd, + chemical_components_data: ChemicalComponentsData, + chemical_component_ids: Iterable[str] | None = None, + populate_pdbx_smiles: bool = False, +) -> ChemicalComponentsData: + """Populates missing data for the chemical components from CCD. + + Args: + ccd: The chemical components database. + chemical_components_data: ChemicalComponentsData to populate missing values + for. This function doesn't modify the object, extended version is provided + as a return value. + chemical_component_ids: chemical components to populate missing values for. + If not specified, the function will consider all chemical components which + are already present in `chemical_components_data`. + populate_pdbx_smiles: whether to populate `pdbx_smiles` field using SMILES + descriptors from _pdbx_chem_comp_descriptor CCD table. If CCD provides + multiple SMILES strings, any of them could be used. + + Returns: + New instance of ChemicalComponentsData without missing values for CCD + entries. + """ + if chemical_component_ids is None: + chemical_component_ids = chemical_components_data.chem_comp.keys() + + ccd_data = get_data_for_ccd_components( + ccd, chemical_component_ids, populate_pdbx_smiles + ) + chem_comp = dict(chemical_components_data.chem_comp) + for component_id, ccd_entry in ccd_data.chem_comp.items(): + if component_id not in chem_comp: + chem_comp[component_id] = ccd_entry + else: + already_specified_fields = { + field: value + for field, value in vars(chem_comp[component_id]).items() + if not _value_is_missing(value) + } + chem_comp[component_id] = ChemCompEntry( + **{**vars(ccd_entry), **already_specified_fields} + ) + return ChemicalComponentsData(chem_comp=chem_comp) + + +def get_all_atoms_in_entry( + ccd: chemical_components.Ccd, res_name: str +) -> Mapping[str, Sequence[str]]: + """Get all possible atoms and bonds for this residue in a standard order. + + Args: + ccd: The chemical components dictionary. + res_name: Full CCD name. + + Returns: + A dictionary table of the atoms and bonds for this residue in this residue + type. + """ + # The CCD version of 'UNK' is weird. It has a CB and a CG atom. We just want + # the minimal amino-acid here which is GLY. + if res_name == 'UNK': + res_name = 'GLY' + ccd_data = ccd.get(res_name) + if not ccd_data: + raise ValueError(f'Unknown residue type {res_name}') + + keys = ( + '_chem_comp_atom.atom_id', + '_chem_comp_atom.type_symbol', + '_chem_comp_bond.atom_id_1', + '_chem_comp_bond.atom_id_2', + ) + + # Add terminal hydrogens for protonation of the N-terminal + if res_name == 'PRO': + res_atoms = {key: [*ccd_data.get(key, [])] for key in keys} + res_atoms['_chem_comp_atom.atom_id'].extend(['H2', 'H3']) + res_atoms['_chem_comp_atom.type_symbol'].extend(['H', 'H']) + res_atoms['_chem_comp_bond.atom_id_1'].extend(['N', 'N']) + res_atoms['_chem_comp_bond.atom_id_2'].extend(['H2', 'H3']) + elif res_name in residue_names.PROTEIN_TYPES_WITH_UNKNOWN: + res_atoms = {key: [*ccd_data.get(key, [])] for key in keys} + res_atoms['_chem_comp_atom.atom_id'].append('H3') + res_atoms['_chem_comp_atom.type_symbol'].append('H') + res_atoms['_chem_comp_bond.atom_id_1'].append('N') + res_atoms['_chem_comp_bond.atom_id_2'].append('H3') + else: + res_atoms = {key: ccd_data.get(key, []) for key in keys} + + return res_atoms + + +@functools.lru_cache(maxsize=128) +def get_res_atom_names(ccd: chemical_components.Ccd, res_name: str) -> set[str]: + """Gets the names of the atoms in a given CCD residue.""" + atoms = get_all_atoms_in_entry(ccd, res_name)['_chem_comp_atom.atom_id'] + return set(atoms) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation.pyi new file mode 100644 index 000000000..8f4a8b375 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation.pyi @@ -0,0 +1,13 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from collections.abc import Sequence + +def indices_grouped_by_value(values: Sequence[int]) -> dict[int, list[int]]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.cc new file mode 100644 index 000000000..5ac46d62c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.cc @@ -0,0 +1,54 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/types/span.h" +#include "pybind11/cast.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" +#include "pybind11_abseil/absl_casters.h" + +namespace { + +namespace py = pybind11; + +absl::flat_hash_map> IndicesGroupedByValue( + absl::Span values) { + absl::flat_hash_map> group_indices; + for (int64_t i = 0, e = values.size(); i < e; ++i) { + group_indices[values[i]].push_back(i); + } + return group_indices; +} + +constexpr char kIndicesGroupedByValue[] = R"( +Returns a map from value to a list of indices this value occupies. + +E.g. indices_grouped_by_value([1, 1, 2, 3, 3, 1, 1]) returns: +{1: [0, 1, 5, 6], 2: [2], 3: [3, 4]} + +Args: + values: a list of values to group. +)"; + +} // namespace + +namespace alphafold3 { + +void RegisterModuleAggregation(py::module m) { + m.def("indices_grouped_by_value", &IndicesGroupedByValue, py::arg("values"), + py::doc(kIndicesGroupedByValue + 1), + py::call_guard()); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.h new file mode 100644 index 000000000..9547b9448 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleAggregation(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_AGGREGATION_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership.pyi new file mode 100644 index 000000000..305f36600 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership.pyi @@ -0,0 +1,18 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +import numpy + + +def isin( + array: numpy.ndarray[numpy.int64], + test_elements: set[int], + invert: bool = ..., +) -> numpy.ndarray[bool]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.cc new file mode 100644 index 000000000..2b3faf8a2 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.cc @@ -0,0 +1,82 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "pybind11/cast.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" +#include "pybind11_abseil/absl_casters.h" + +namespace { + +namespace py = pybind11; + +py::array_t IsIn(const py::array_t& array, + const absl::flat_hash_set& test_elements, + bool invert) { + const size_t num_elements = array.size(); + + py::array_t output(num_elements); + std::fill(output.mutable_data(), output.mutable_data() + output.size(), + invert); + + // Shortcut: The output will be trivially always false if test_elements empty. + if (test_elements.empty()) { + return output; + } + + for (size_t i = 0; i < num_elements; ++i) { + if (test_elements.contains(array.data()[i])) { + output.mutable_data()[i] = !invert; + } + } + if (array.ndim() > 1) { + auto shape = + std::vector(array.shape(), array.shape() + array.ndim()); + return output.reshape(shape); + } + return output; +} + +constexpr char kIsInDoc[] = R"( +Computes whether each element is in test_elements. + +Same use as np.isin, but much faster. If len(array) = n, len(test_elements) = m: +* This function has complexity O(n). +* np.isin with kind='sort' has complexity O(m*log(m) + n * log(m)). + +Args: + array: Input NumPy array with dtype=np.int64. + test_elements: The values against which to test each value of array. + invert: If True, the values in the returned array are inverted, as if + calculating `element not in test_elements`. Default is False. + `isin(a, b, invert=True)` is equivalent to but faster than `~isin(a, b)`. + +Returns + A boolean array of the same shape as the input array. Each value `val` is: + * `val in test_elements` if `invert=False`, + * `val not in test_elements` if `invert=True`. +)"; + +} // namespace + +namespace alphafold3 { + +void RegisterModuleMembership(pybind11::module m) { + m.def("isin", &IsIn, py::arg("array"), py::arg("test_elements"), + py::kw_only(), py::arg("invert") = false, py::doc(kIsInDoc + 1)); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.h new file mode 100644 index 000000000..d224fb1f6 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMembership(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MEMBERSHIP_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc new file mode 100644 index 000000000..cea9a1b1c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc @@ -0,0 +1,249 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include "alphafold3/structure/cpp/mmcif_altlocs.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/log/log.h" +#include "absl/strings/numbers.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/structure/cpp/mmcif_layout.h" + +namespace alphafold3 { +namespace { + +float OccupancyToFloat(absl::string_view occupancy) { + float result = 0.0f; + LOG_IF(ERROR, !absl::SimpleAtof(occupancy, &result)) + << "Invalid Occupancy: " << occupancy; + return result; +} + +// Deuterium is the same atom as Hydrogen so keep equivalent for grouping. +bool AtomEquiv(absl::string_view lhs, absl::string_view rhs) { + if (lhs == rhs) return true; + if (lhs.empty() != rhs.empty()) return false; + // Both lhs and rhs are guaranteed to be non-empty after this. + char first_lhs = lhs.front(); + char second_rhs = rhs.front(); + if ((first_lhs == 'H' && second_rhs == 'D') || + (first_lhs == 'D' && second_rhs == 'H')) { + lhs.remove_prefix(1); + rhs.remove_prefix(1); + return lhs == rhs; + } + return false; +} + +// Calls group_callback with that start index and count for each group of +// equivalent values in `values`, starting at `start` and ending at `count`. +// Example: +// GroupBy({"B", "B", "B", "C", "C"}, 0, 5, [](size_t start, size_t count) { +// absl::Printf("start=%d, count=%d\n", start, count); +// }); +// Would print: +// start=0, count=3 +// start=3, count=2 +template > +void GroupBy(absl::Span values, std::size_t start, + std::size_t count, GroupCallback&& group_callback, + IsEqual&& is_equal = std::equal_to{}) { + std::size_t span_start = start; + if (count > 0) { + for (std::size_t i = start + 1; i < start + count; ++i) { + if (!is_equal(values[i], values[span_start])) { + group_callback(span_start, i - span_start); + span_start = i; + } + } + group_callback(span_start, start + count - span_start); + } +} + +void ProcessAltLocGroupsWhole(std::size_t alt_loc_start, + std::size_t alt_loc_count, + absl::Span comp_ids, + absl::Span atom_ids, + absl::Span alt_ids, + absl::Span occupancies, + std::vector& in_out_keep_indices) { + std::pair best_split = {alt_loc_start, + alt_loc_count}; + std::vector alt_loc_groups; + float best_occupancy = -std::numeric_limits::infinity(); + char best_group = alt_ids[alt_loc_start].front(); + std::vector> occupancy_stats; + + // Group by residue type. + GroupBy(comp_ids, alt_loc_start, alt_loc_count, + [&](std::size_t start, std::size_t count) { + // This callback selects the best residue group and the best + // Alt-loc char within that group. + alt_loc_groups.clear(); + occupancy_stats.clear(); + // Calculate total occupancy for residue type. + for (std::size_t i = 0; i < count; ++i) { + char alt_loc_id = alt_ids[start + i].front(); + float occupancy = OccupancyToFloat(occupancies[start + i]); + if (auto loc = absl::c_find(alt_loc_groups, alt_loc_id); + loc == alt_loc_groups.end()) { + occupancy_stats.emplace_back(1, occupancy); + alt_loc_groups.push_back(alt_loc_id); + } else { + auto& stat = + occupancy_stats[std::distance(alt_loc_groups.begin(), loc)]; + ++stat.first; + stat.second += occupancy; + } + } + float total_occupancy = 0.0; + for (auto& stat : occupancy_stats) { + total_occupancy += stat.second / stat.first; + } + char group = *absl::c_min_element(alt_loc_groups); + // Compares occupancy of residue to best seen so far. + // Tie breaks alphabetic. + if (total_occupancy > best_occupancy || + (total_occupancy == best_occupancy && group < best_group)) { + // Selects the best sub group. + best_group = alt_loc_groups.front(); + float best_amount = occupancy_stats.front().second / + occupancy_stats.front().first; + for (std::size_t i = 1; i < occupancy_stats.size(); ++i) { + float amount = + occupancy_stats[i].second / occupancy_stats[i].first; + char group = alt_loc_groups[i]; + if (amount > best_amount || + (amount == best_amount && group < best_group)) { + best_amount = amount; + best_group = group; + } + } + best_occupancy = total_occupancy; + best_split = {start, count}; + } + }); + + // Now that the best residue type has been selected and the best alt-loc + // within that has been selected add indices of indices to keep to the keep + // list. + auto [split_start, split_count] = best_split; + GroupBy( + atom_ids, split_start, split_count, + [&in_out_keep_indices, &alt_ids, best_group](std::size_t start, + std::size_t count) { + // This makes sure we select an atom for each atom id even if it does + // not have our selected alt-loc char. + std::size_t best_index = start; + for (std::size_t i = 1; i < count; ++i) { + if (alt_ids[start + i].front() == best_group) { + best_index = start + i; + break; + } + } + in_out_keep_indices.push_back(best_index); + }, + AtomEquiv); +} + +// Finds the alt-loc group with the highest score and pushes the indices on to +// the back of in_out_keep_indices. +void ProcessAltLocGroupPartial( + std::size_t alt_loc_start, std::size_t alt_loc_count, + absl::Span atom_ids, + absl::Span alt_ids, + absl::Span occupancies, + std::vector& in_out_keep_indices) { + GroupBy( + atom_ids, alt_loc_start, alt_loc_count, + [&](std::size_t start, std::size_t count) { + if (count == 1) { + in_out_keep_indices.push_back(start); + } else { + float best_occ = OccupancyToFloat(occupancies[start]); + std::size_t best_index = start; + char best_group = alt_ids[start].front(); + for (std::size_t i = 0; i < count; ++i) { + float occ = OccupancyToFloat(occupancies[start + i]); + char group = alt_ids[start + i].front(); + if (occ > best_occ || (occ == best_occ && group < best_group)) { + best_group = group; + best_index = start + i; + best_occ = occ; + } + } + in_out_keep_indices.push_back(best_index); + } + }, + AtomEquiv); +} + +} // namespace + +// Resolves alt-locs returning the atom indices that will be left. +std::vector ResolveMmcifAltLocs( + const MmcifLayout& layout, absl::Span comp_ids, + absl::Span atom_ids, + absl::Span alt_ids, + absl::Span occupancies, + absl::Span chain_indices) { + std::vector keep_indices; + keep_indices.reserve(layout.num_atoms()); + std::size_t alt_loc_start = 0; + for (std::size_t chain_index : chain_indices) { + auto [residues_start, residues_end] = layout.residue_range(chain_index); + for (std::size_t residue = residues_start; residue < residues_end; + ++residue) { + std::size_t alt_loc_count = 0; + auto [atom_start, atom_end] = layout.atom_range(residue); + for (std::size_t i = atom_start; i < atom_end; ++i) { + char alt_loc_id = alt_ids[i].front(); + if (alt_loc_id == '.' || alt_loc_id == '?') { + if (alt_loc_count > 0) { + ProcessAltLocGroupPartial(alt_loc_start, alt_loc_count, atom_ids, + alt_ids, occupancies, keep_indices); + alt_loc_count = 0; + } + keep_indices.push_back(i); + } else { + if (alt_loc_count == 0) { + alt_loc_start = i; + } + ++alt_loc_count; + } + } + if (alt_loc_count > 0) { + if (atom_end - atom_start == alt_loc_count) { + ProcessAltLocGroupsWhole(alt_loc_start, alt_loc_count, comp_ids, + atom_ids, alt_ids, occupancies, + keep_indices); + } else { + ProcessAltLocGroupPartial(alt_loc_start, alt_loc_count, atom_ids, + alt_ids, occupancies, keep_indices); + } + } + } + } + + return keep_indices; +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.h new file mode 100644 index 000000000..fab57817c --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.h @@ -0,0 +1,51 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ + +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "alphafold3/structure/cpp/mmcif_layout.h" + +namespace alphafold3 { + +// Returns the list of indices that should be kept after resolving alt-locs. +// 1) Partial Residue. Each cycle of alt-locs are resolved separately with the +// highest occupancy alt-loc. Tie-breaks are resolved alphabetically. See +// tests for examples. +// 2) Whole Residue. These are resolved in two passes. +// a) The residue with the highest occupancy is chosen. +// b) The locations for a given residue are resolved. +// All tie-breaks are resolved alphabetically. See tests for examples. +// +// Preconditions: layout and comp_ids, alt_ids, occupancies are all from same +// mmCIF file and chain_indices are monotonically increasing and less than +// layout.num_chains(). +// +// comp_ids from '_atom_site.label_comp_id'. +// alt_ids from '_atom_site.label_alt_id'. +// occupancies from '_atom_site.occupancy'. +std::vector ResolveMmcifAltLocs( + const MmcifLayout& layout, absl::Span comp_ids, + absl::Span atom_ids, + absl::Span alt_ids, + absl::Span occupancies, + absl::Span chain_indices); + +} // namespace alphafold3 + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ALTLOCS_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site.pyi new file mode 100644 index 000000000..5f0ba34b0 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site.pyi @@ -0,0 +1,23 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from collections.abc import Callable +from alphafold3.cpp import cif_dict + + +def get_internal_to_author_chain_id_map( + mmcif: cif_dict.CifDict +) -> dict[str,str]: ... + + +def get_or_infer_type_symbol( + mmcif: cif_dict.CifDict, + atom_id_to_type_symbol: Callable[[str, str], str], +) -> list[str]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc new file mode 100644 index 000000000..6037fe08b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc @@ -0,0 +1,83 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "pybind11/gil.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" +#include "pybind11_abseil/absl_casters.h" + +namespace alphafold3 { +namespace { +namespace py = pybind11; + +// If present, returns the _atom_site.type_symbol. If not, infers it using +// _atom_site.label_comp_id (residue name), _atom_site.label_atom_id (atom name) +// and the CCD. +py::list GetOrInferTypeSymbol(const CifDict& mmcif, + const py::object& atom_id_to_type_symbol) { + const auto& type_symbol = mmcif["_atom_site.type_symbol"]; + const int num_atom = mmcif["_atom_site.id"].size(); + py::list patched_type_symbol(num_atom); + if (type_symbol.empty()) { + const auto& label_comp_id = mmcif["_atom_site.label_comp_id"]; + const auto& label_atom_id = mmcif["_atom_site.label_atom_id"]; + CHECK_EQ(label_comp_id.size(), num_atom); + CHECK_EQ(label_atom_id.size(), num_atom); + for (int i = 0; i < num_atom; i++) { + patched_type_symbol[i] = + atom_id_to_type_symbol(label_comp_id[i], label_atom_id[i]); + } + } else { + for (int i = 0; i < num_atom; i++) { + patched_type_symbol[i] = type_symbol[i]; + } + } + return patched_type_symbol; +} + +absl::flat_hash_map +GetInternalToAuthorChainIdMap(const CifDict& mmcif) { + const auto& label_asym_ids = mmcif["_atom_site.label_asym_id"]; + const auto& auth_asym_ids = mmcif["_atom_site.auth_asym_id"]; + CHECK_EQ(label_asym_ids.size(), auth_asym_ids.size()); + + absl::flat_hash_map mapping; + for (size_t i = 0, num_rows = label_asym_ids.size(); i < num_rows; ++i) { + // Use only the first internal_chain_id occurrence to generate the mapping. + // It should not matter as there should not be a case where a single + // internal chain ID would map to more than one author chain IDs (i.e. the + // mapping should be injective). Since we need this method to be fast, we + // choose not to check it. + mapping.emplace(label_asym_ids[i], auth_asym_ids[i]); + } + return mapping; +} + +} // namespace + +namespace py = pybind11; + +void RegisterModuleMmcifAtomSite(pybind11::module m) { + m.def("get_or_infer_type_symbol", &GetOrInferTypeSymbol, py::arg("mmcif"), + py::arg("atom_id_to_type_symbol")); + + m.def("get_internal_to_author_chain_id_map", &GetInternalToAuthorChainIdMap, + py::arg("mmcif"), py::call_guard()); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.h new file mode 100644 index 000000000..1f2104ecf --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMmcifAtomSite(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_ATOM_SITE_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.h new file mode 100644 index 000000000..51c67c528 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.h @@ -0,0 +1,146 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" + +namespace alphafold3 { + +// Holds the layout of a parsed mmCIF file. +class MmcifLayout { + public: + MmcifLayout(std::vector chain_ends, + std::vector residues, std::size_t model_offset, + std::size_t num_models) + : chain_ends_(std::move(chain_ends)), + residue_ends_(std::move(residues)), + model_offset_(model_offset), + num_models_(num_models) {} + + // Reads a layout from a valid parsed mmCIF. If a valid model_id is provided + // the offsets will select that model from the mmCIF. + // If no model_id is specified, we calculate the layout of the first model + // only. Therefore it is a requirement that each model has identical atom + // layouts. An error is returned if the atom counts do not between models. + static absl::StatusOr Create(const CifDict& mmcif, + absl::string_view model_id = ""); + + std::string ToDebugString() const; + + // Returns the start index and one past the last residue index of a given + // chain. A chain_index of n refers to the n-th chain in the mmCIF. The + // returned residue indices are 0-based enumerations of residues in the + // _atom_site records, and therefore do not include missing residues. + std::pair residue_range( + std::size_t chain_index) const { + if (chain_index > 0) { + return {chain_ends_[chain_index - 1], chain_ends_[chain_index]}; + } else { + return {0, chain_ends_[0]}; + } + } + + // Returns the start index and one past the last index of a given residue. + // A residue_index of n refers to the n-th residue in the mmCIF, not + // including residues that are unresolved (i.e. only using _atom_site). + std::pair atom_range( + std::size_t residue_index) const { + if (residue_index > 0) { + return {residue_ends_[residue_index - 1], residue_ends_[residue_index]}; + } else { + return {model_offset_, residue_ends_[residue_index]}; + } + } + + // If model_id was provided during construction then this is 1, otherwise + // it is the number of models present in the mmCIF. + std::size_t num_models() const { return num_models_; } + // The number of atoms in the chosen model. + std::size_t num_atoms() const { + return residue_ends_.empty() ? 0 : residue_ends_.back() - model_offset_; + } + // The number of chains in the chosen model. + std::size_t num_chains() const { return chain_ends_.size(); } + // The number of residues in the chosen model, not counting unresolved + // residues. + std::size_t num_residues() const { return residue_ends_.size(); } + + // Returns the first atom index that is part of the specified chain. + // The chain is specified using chain_index, which is a 0-based + // enumeration of the chains in the _atom_site table. + std::size_t atom_site_from_chain_index(std::size_t chain_index) const { + if (chain_index == 0) { + return model_offset_; + } + return atom_site_from_residue_index(chain_ends_[chain_index - 1]); + } + + // Returns the first atom index that is part of the specified residue. + // The residue is specified using residue_index, which is a 0-based + // enumeration of the residues in the _atom_site table. + std::size_t atom_site_from_residue_index(std::size_t residues_index) const { + if (residues_index == 0) { + return model_offset_; + } + return residue_ends_[residues_index - 1]; + } + + // One past last residue index of each chain. The residue index does not + // include unresolved residues and is a simple 0-based enumeration of the + // residues in _atom_site table. + const std::vector& chains() const { return chain_ends_; } + + // Indices of the first atom of each chain. Note that this returns atom + // indices (like residue_starts()), not residue indices (like chains()). + std::vector chain_starts() const; + + // One past last atom index of each residue. + const std::vector& residues() const { return residue_ends_; } + + // Indices of the first atom of each residue. + std::vector residue_starts() const { + std::vector residue_starts; + if (!residue_ends_.empty()) { + residue_starts.reserve(residue_ends_.size()); + residue_starts.push_back(model_offset_); + residue_starts.insert(residue_starts.end(), residue_ends_.begin(), + residue_ends_.end() - 1); + } + return residue_starts; + } + + // The first atom index that is part of the specified model. + std::size_t model_offset() const { return model_offset_; } + + void Filter(absl::Span keep_indices); + + private: + std::vector chain_ends_; + std::vector residue_ends_; + std::size_t model_offset_; + std::size_t num_models_; +}; + +} // namespace alphafold3 + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.pyi new file mode 100644 index 000000000..add1b05ea --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.pyi @@ -0,0 +1,26 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from alphafold3.cpp import cif_dict + +class MmcifLayout: + def atom_range(self, residue_index: int) -> tuple[int, int]: ... + def chain_starts(self) -> list[int]: ... + def chains(self) -> list[int]: ... + def model_offset(self) -> int: ... + def num_atoms(self) -> int: ... + def num_chains(self) -> int: ... + def num_models(self) -> int: ... + def num_residues(self) -> int: ... + def residue_range(self, chain_index: int) -> tuple[int, int]: ... + def residue_starts(self) -> list[int]: ... + def residues(self) -> list[int]: ... + +def from_mmcif(mmcif: cif_dict.CifDict, model_id: str = ...) -> MmcifLayout: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc new file mode 100644 index 000000000..91ad70c0b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc @@ -0,0 +1,213 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "alphafold3/structure/cpp/mmcif_layout.h" + +namespace alphafold3 { + +std::string MmcifLayout::ToDebugString() const { + return absl::StrFormat( + "MmcifLayout(models=%d, chains=%d, num_residues=%d, atoms=%d)", + num_models(), num_chains(), num_residues(), num_atoms()); +} + +// Changes layout to match keep_indices removing empty chains/residues. +void MmcifLayout::Filter(absl::Span keep_indices) { + if (num_chains() == 0) { + return; + } + // Update residue indices. + auto keep_it = absl::c_lower_bound(keep_indices, residue_ends_.front()); + for (auto& residue : residue_ends_) { + while (keep_it != keep_indices.end() && *keep_it < residue) { + ++keep_it; + } + residue = std::distance(keep_indices.begin(), keep_it); + } + // Unique residue_ends_ with updating chains. + auto first = residue_ends_.begin(); + auto tail = first; + std::size_t num_skipped = 0; + std::size_t current = 0; + for (std::size_t& chain_end : chain_ends_) { + for (auto e = residue_ends_.begin() + chain_end; first != e; ++first) { + std::size_t next = *first; + *tail = next; + if (current != next) { + current = next; + ++tail; + } else { + ++num_skipped; + } + } + chain_end -= num_skipped; + } + residue_ends_.erase(tail, residue_ends_.end()); + + current = 0; + chain_ends_.erase(std::remove_if(chain_ends_.begin(), chain_ends_.end(), + [¤t](std::size_t next) { + bool result = current == next; + current = next; + return result; + }), + chain_ends_.end()); + model_offset_ = 0; +} + +absl::StatusOr MmcifLayout::Create(const CifDict& mmcif, + absl::string_view model_id) { + auto model_ids = mmcif["_atom_site.pdbx_PDB_model_num"]; + auto chain_ids = mmcif["_atom_site.label_asym_id"]; // chain ID. + auto label_seq_ids = mmcif["_atom_site.label_seq_id"]; // residue ID. + auto auth_seq_ids = mmcif["_atom_site.auth_seq_id"]; // author residue ID. + auto insertion_codes = mmcif["_atom_site.pdbx_PDB_ins_code"]; + + if (model_ids.size() != chain_ids.size() || + model_ids.size() != label_seq_ids.size() || + (model_ids.size() != auth_seq_ids.size() && !auth_seq_ids.empty()) || + (model_ids.size() != insertion_codes.size() && + !insertion_codes.empty())) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid _atom_site table.", // + " len(_atom_site.pdbx_PDB_model_num): ", model_ids.size(), + " len(_atom_site.label_asym_id): ", chain_ids.size(), + " len(_atom_site.label_seq_id): ", label_seq_ids.size(), + " len(_atom_site.auth_seq_id): ", auth_seq_ids.size(), + " len(_atom_site.pdbx_PDB_ins_code): ", insertion_codes.size())); + } + std::size_t num_atoms = model_ids.size(); + if (num_atoms == 0) { + return MmcifLayout({}, {}, 0, 0); + } + std::size_t model_offset = 0; + std::size_t num_models; + std::size_t num_atoms_per_model; + if (model_id.empty()) { + absl::string_view first_model_id = model_ids.front(); + + // Binary search for where the first model ends. + num_atoms_per_model = std::distance( + model_ids.begin(), + absl::c_upper_bound(model_ids, first_model_id, std::not_equal_to<>{})); + if (num_atoms % num_atoms_per_model != 0) { + return absl::InvalidArgumentError(absl::StrCat( + "Each model must have the same number of atoms: (", num_atoms, " % ", + num_atoms_per_model, " == ", num_atoms % num_atoms_per_model, ").")); + } + num_models = num_atoms / num_atoms_per_model; + // Test boundary conditions for each model hold. + for (std::size_t i = 1; i < num_models; ++i) { + if ((model_ids[i * num_atoms_per_model] != + model_ids[(i + 1) * num_atoms_per_model - 1]) || + (model_ids[i * num_atoms_per_model - 1] == + model_ids[i * num_atoms_per_model])) { + return absl::InvalidArgumentError( + absl::StrCat("Each model must have the same number of atoms: (", + num_atoms, " % ", num_atoms_per_model, + " == ", num_atoms % num_atoms_per_model, ").")); + } + } + } else { + num_models = 1; + model_offset = + std::distance(model_ids.begin(), absl::c_find(model_ids, model_id)); + if (model_offset == model_ids.size()) { + return absl::InvalidArgumentError( + absl::StrCat("Unknown model_id: ", model_id)); + } + model_ids.remove_prefix(model_offset); + chain_ids.remove_prefix(model_offset); + label_seq_ids.remove_prefix(model_offset); + if (!auth_seq_ids.empty()) auth_seq_ids.remove_prefix(model_offset); + if (!insertion_codes.empty()) insertion_codes.remove_prefix(model_offset); + + num_atoms_per_model = std::distance( + model_ids.begin(), std::upper_bound(model_ids.begin(), model_ids.end(), + model_id, std::not_equal_to<>{})); + num_atoms = num_atoms_per_model; + } + std::vector residues; + std::vector chains; + absl::string_view chain_id = chain_ids.front(); + if (!auth_seq_ids.empty() && !insertion_codes.empty()) { + // If author residue IDs are present then these are preferred to + // label residue IDs because they work for multi-residue ligands (which + // are given constant "." label residue IDs). + // NB: Author residue IDs require both the auth_seq_id and the insertion + // code to be unique. + absl::string_view auth_seq_id = auth_seq_ids.front(); + absl::string_view insertion_code = insertion_codes.front(); + for (std::size_t i = 1; i < num_atoms_per_model; ++i) { + if (absl::string_view current_chain_id = chain_ids[i]; + current_chain_id != chain_id) { + residues.push_back(i + model_offset); + chains.push_back(residues.size()); + chain_id = current_chain_id; + auth_seq_id = auth_seq_ids[i]; + insertion_code = insertion_codes[i]; + } else if (absl::string_view current_seq_id = auth_seq_ids[i], + current_insertion_code = insertion_codes[i]; + insertion_code != current_insertion_code || + auth_seq_id != current_seq_id) { + residues.push_back(i + model_offset); + auth_seq_id = current_seq_id; + insertion_code = current_insertion_code; + } + } + } else { + absl::string_view label_seq_id = label_seq_ids.front(); + for (std::size_t i = 1; i < num_atoms_per_model; ++i) { + if (absl::string_view current_chain_id = chain_ids[i]; + current_chain_id != chain_id) { + residues.push_back(i + model_offset); + chains.push_back(residues.size()); + chain_id = current_chain_id; + label_seq_id = label_seq_ids[i]; + } else if (absl::string_view current_seq_id = label_seq_ids[i]; + label_seq_id != current_seq_id) { + residues.push_back(i + model_offset); + label_seq_id = current_seq_id; + } + } + } + residues.push_back(num_atoms_per_model + model_offset); + chains.push_back(residues.size()); + return MmcifLayout(std::move(chains), std::move(residues), model_offset, + num_models); +} + +std::vector MmcifLayout::chain_starts() const { + std::vector chain_starts; + chain_starts.reserve(chain_ends_.size()); + for (std::size_t index = 0; index < chain_ends_.size(); ++index) { + chain_starts.push_back(atom_site_from_chain_index(index)); + } + return chain_starts; +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.cc new file mode 100644 index 000000000..8eb69befc --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.cc @@ -0,0 +1,49 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include "alphafold3/structure/cpp/mmcif_layout.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" + +namespace alphafold3 { + +namespace py = pybind11; + +void RegisterModuleMmcifLayout(pybind11::module m) { + py::class_(m, "MmcifLayout") + .def("__str__", &MmcifLayout::ToDebugString) + .def("num_models", &MmcifLayout::num_models) + .def("num_chains", &MmcifLayout::num_chains) + .def("num_residues", &MmcifLayout::num_residues) + .def("num_atoms", &MmcifLayout::num_atoms) + .def("residue_range", &MmcifLayout::residue_range, py::arg("chain_index")) + .def("atom_range", &MmcifLayout::atom_range, py::arg("residue_index")) + .def("chains", &MmcifLayout::chains, + py::doc("Returns a list of indices one past the last residue of " + "each chain.")) + .def( + "chain_starts", &MmcifLayout::chain_starts, + py::doc("Returns a list of indices of the first atom of each chain.")) + .def("residues", &MmcifLayout::residues, + py::doc("Returns a list of indices one past the last atom of each " + "residue.")) + .def("residue_starts", &MmcifLayout::residue_starts, + py::doc( + "Returns a list of indices of the first atom of each residue.")) + .def("model_offset", &MmcifLayout::model_offset, + py::doc("Returns the first atom index that is part of the specified " + "model.")); + + m.def("from_mmcif", &MmcifLayout::Create, py::arg("mmcif"), + py::arg("model_id") = ""); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.h new file mode 100644 index 000000000..c79b2dd50 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMmcifLayout(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_LAYOUT_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.h new file mode 100644 index 000000000..821be658d --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.h @@ -0,0 +1,34 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ + +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" + +namespace alphafold3 { + +// Returns a pair of atom indices for each row in the bonds table (aka +// _struct_conn). The indices are simple 0-based indexes into the columns of +// the _atom_site table in the input mmCIF, and do not necessarily correspond +// to the values in _atom_site.id, or any other column. +absl::StatusOr, std::vector>> +GetBondAtomIndices(const CifDict& mmcif, absl::string_view model_id); + +} // namespace alphafold3 + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.pyi new file mode 100644 index 000000000..d293e666a --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.pyi @@ -0,0 +1,13 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from alphafold3.cpp import cif_dict + +def get_bond_atom_indices(mmcif_dict: cif_dict.CifDict, model_id: str) -> tuple[list[int],list[int]]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc new file mode 100644 index 000000000..afb930fab --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc @@ -0,0 +1,380 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "alphafold3/structure/cpp/mmcif_struct_conn.h" + +namespace alphafold3 { + +namespace { + +struct AtomId { + absl::string_view chain_id; + absl::string_view res_id_1; + absl::string_view res_id_2; + absl::string_view atom_name; + absl::string_view alt_id; + + friend bool operator==(const AtomId&, const AtomId&) = default; + template + friend H AbslHashValue(H h, const AtomId& m) { + return H::combine(std::move(h), m.chain_id, m.res_id_1, m.res_id_2, + m.atom_name, m.alt_id); + } +}; + +using StringArrayRef = absl::Span; +using BondIndexByAtom = absl::flat_hash_map>; +using BondAtomIndices = std::vector; + +// Returns whether each container is the same size. +template +bool AreSameSize(const C& c, const Cs&... cs) { + return ((c.size() == cs.size()) && ...); +} + +struct ColumnSpec { + absl::string_view chain_id_col; + absl::string_view res_id_1_col; + absl::string_view res_id_2_col; + absl::string_view atom_name_col; + std::optional alt_id_col; // Not used by OpenMM. +}; + +class AtomColumns { + public: + static absl::StatusOr Create(const CifDict& mmcif, + const ColumnSpec& column_spec) { + StringArrayRef chain_id = mmcif[column_spec.chain_id_col]; + StringArrayRef res_id_1 = mmcif[column_spec.res_id_1_col]; + StringArrayRef res_id_2 = mmcif[column_spec.res_id_2_col]; + StringArrayRef atom_name = mmcif[column_spec.atom_name_col]; + if (!AreSameSize(chain_id, res_id_1, res_id_2, atom_name)) { + return absl::InvalidArgumentError(absl::StrCat( + "Atom columns are not the same size. ", // + "len(", column_spec.chain_id_col, ")=", chain_id.size(), // + ", len(", column_spec.res_id_1_col, ")=", res_id_1.size(), // + ", len(", column_spec.res_id_2_col, ")=", res_id_2.size(), // + ", len(", column_spec.atom_name_col, ")=", atom_name.size(), // + ".")); + } + if (column_spec.alt_id_col.has_value()) { + StringArrayRef alt_id = mmcif[*column_spec.alt_id_col]; + if (!AreSameSize(alt_id, chain_id)) { + return absl::InvalidArgumentError(absl::StrCat( + "Atom columns are not the same size. ", // + "len(", column_spec.chain_id_col, ")=", chain_id.size(), // + ", len(", *column_spec.alt_id_col, ")=", alt_id.size(), // + ".")); + } + return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, alt_id, + column_spec); + } else { + return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, std::nullopt, + column_spec); + } + } + + inline std::size_t size() const { return size_; } + + absl::string_view GetNormalizedAltId(const std::size_t index) const { + constexpr absl::string_view kFullStop = "."; + if (alt_id_.has_value()) { + absl::string_view alt_id = (*alt_id_)[index]; + return alt_id == "?" ? kFullStop : alt_id; + } else { + return kFullStop; + } + } + + AtomId GetAtom(const std::size_t index) const { + return {.chain_id = chain_id_[index], + .res_id_1 = res_id_1_[index], + .res_id_2 = res_id_2_[index], + .atom_name = atom_name_[index], + .alt_id = GetNormalizedAltId(index)}; + } + + std::string GetAtomString(const std::size_t index) const { + std::string alt_id_col; + if (column_spec_.alt_id_col.has_value()) { + alt_id_col = *column_spec_.alt_id_col; + } else { + alt_id_col = "default label_alt_id"; + } + return absl::StrCat( + column_spec_.chain_id_col, "=", chain_id_[index], ", ", // + column_spec_.res_id_1_col, "=", res_id_1_[index], ", ", // + column_spec_.res_id_2_col, "=", res_id_2_[index], ", ", // + column_spec_.atom_name_col, "=", atom_name_[index], ", ", // + alt_id_col, "=", GetNormalizedAltId(index)); // + } + + private: + AtomColumns(StringArrayRef chain_id, StringArrayRef res_id_1, + StringArrayRef res_id_2, StringArrayRef atom_name, + std::optional alt_id, + const ColumnSpec& column_spec) + : chain_id_(chain_id), + res_id_1_(res_id_1), + res_id_2_(res_id_2), + atom_name_(atom_name), + alt_id_(alt_id), + column_spec_(column_spec), + size_(chain_id.size()) {} + StringArrayRef chain_id_; + StringArrayRef res_id_1_; + StringArrayRef res_id_2_; + StringArrayRef atom_name_; + std::optional alt_id_; + ColumnSpec column_spec_; + std::size_t size_; +}; + +// Adds the atom index to any rows in the bond table involving that atom. +absl::Status FillInBondsForAtom(const BondIndexByAtom& bond_index_by_atom, + const AtomId& atom, + const std::size_t atom_index, + BondAtomIndices& bond_atom_indices) { + if (auto bond_index_it = bond_index_by_atom.find(atom); + bond_index_it != bond_index_by_atom.end()) { + for (std::size_t bond_index : bond_index_it->second) { + if (bond_index < 0 || bond_index >= bond_atom_indices.size()) { + return absl::OutOfRangeError( + absl::StrCat("Bond index out of range: ", bond_index)); + } + bond_atom_indices[bond_index] = atom_index; + } + } + return absl::OkStatus(); +} + +// Checks that the CifDict has all of the columns in the column spec. +bool HasAllColumns(const CifDict& mmcif, const ColumnSpec& columns) { + return mmcif.Contains(columns.chain_id_col) && + mmcif.Contains(columns.res_id_1_col) && + mmcif.Contains(columns.res_id_2_col) && + mmcif.Contains(columns.atom_name_col) && + (!columns.alt_id_col.has_value() || + mmcif.Contains(*columns.alt_id_col)); +} + +// Fully specified ptnr1 atom. +constexpr ColumnSpec kStructConnPtnr1ColumnsFull{ + .chain_id_col = "_struct_conn.ptnr1_label_asym_id", + .res_id_1_col = "_struct_conn.ptnr1_auth_seq_id", + .res_id_2_col = "_struct_conn.pdbx_ptnr1_PDB_ins_code", + .atom_name_col = "_struct_conn.ptnr1_label_atom_id", + .alt_id_col = "_struct_conn.pdbx_ptnr1_label_alt_id", +}; + +// Fully specified ptnr2 atom. +constexpr ColumnSpec kStructConnPtnr2ColumnsFull{ + .chain_id_col = "_struct_conn.ptnr2_label_asym_id", + .res_id_1_col = "_struct_conn.ptnr2_auth_seq_id", + .res_id_2_col = "_struct_conn.pdbx_ptnr2_PDB_ins_code", + .atom_name_col = "_struct_conn.ptnr2_label_atom_id", + .alt_id_col = "_struct_conn.pdbx_ptnr2_label_alt_id", +}; + +// Columns used by OpenMM for ptnr1 atoms. +constexpr ColumnSpec kStructConnPtnr1OpenMM{ + .chain_id_col = "_struct_conn.ptnr1_label_asym_id", + .res_id_1_col = "_struct_conn.ptnr1_label_seq_id", + .res_id_2_col = "_struct_conn.ptnr1_label_comp_id", + .atom_name_col = "_struct_conn.ptnr1_label_atom_id", + .alt_id_col = std::nullopt, +}; + +// Columns used by OpenMM for ptnr2 atoms. +constexpr ColumnSpec kStructConnPtnr2OpenMM{ + .chain_id_col = "_struct_conn.ptnr2_label_asym_id", + .res_id_1_col = "_struct_conn.ptnr2_label_seq_id", + .res_id_2_col = "_struct_conn.ptnr2_label_comp_id", + .atom_name_col = "_struct_conn.ptnr2_label_atom_id", + .alt_id_col = std::nullopt, +}; + +// Fully specified atom sites. +constexpr ColumnSpec kAtomSiteColumnsFull{ + .chain_id_col = "_atom_site.label_asym_id", + .res_id_1_col = "_atom_site.auth_seq_id", + .res_id_2_col = "_atom_site.pdbx_PDB_ins_code", + .atom_name_col = "_atom_site.label_atom_id", + .alt_id_col = "_atom_site.label_alt_id", +}; + +// Atom site columns used to match OpenMM _struct_conn tables. +constexpr ColumnSpec kAtomSiteColumnsOpenMM{ + .chain_id_col = "_atom_site.label_asym_id", + .res_id_1_col = "_atom_site.label_seq_id", + .res_id_2_col = "_atom_site.label_comp_id", + .atom_name_col = "_atom_site.label_atom_id", + .alt_id_col = "_atom_site.label_alt_id", +}; + +} // namespace + +absl::StatusOr> GetBondAtomIndices( + const CifDict& mmcif, absl::string_view model_id) { + ColumnSpec ptnr1_columns, ptnr2_columns, atom_site_columns; + + if (HasAllColumns(mmcif, kStructConnPtnr1ColumnsFull) && + HasAllColumns(mmcif, kStructConnPtnr2ColumnsFull)) { + ptnr1_columns = kStructConnPtnr1ColumnsFull; + ptnr2_columns = kStructConnPtnr2ColumnsFull; + atom_site_columns = kAtomSiteColumnsFull; + } else { + ptnr1_columns = kStructConnPtnr1OpenMM; + ptnr2_columns = kStructConnPtnr2OpenMM; + atom_site_columns = kAtomSiteColumnsOpenMM; + } + + absl::StatusOr ptnr1_atoms = + AtomColumns::Create(mmcif, ptnr1_columns); + if (!ptnr1_atoms.ok()) { + return ptnr1_atoms.status(); + } + absl::StatusOr ptnr2_atoms = + AtomColumns::Create(mmcif, ptnr2_columns); + if (!ptnr2_atoms.ok()) { + return ptnr2_atoms.status(); + } + StringArrayRef struct_conn_id = mmcif["_struct_conn.id"]; + if (!AreSameSize(struct_conn_id, *ptnr1_atoms, *ptnr2_atoms)) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid '_struct_conn.' loop. ", // + "len(id) = ", struct_conn_id.size(), ", ", // + "len(ptnr1_atoms) = ", ptnr1_atoms->size(), ", ", // + "len(ptnr2_atoms) = ", ptnr2_atoms->size(), "." // + )); + } + + absl::StatusOr atoms = + AtomColumns::Create(mmcif, atom_site_columns); + if (!atoms.ok()) { + return atoms.status(); + } + StringArrayRef atom_site_id = mmcif["_atom_site.id"]; + StringArrayRef atom_site_model_id = mmcif["_atom_site.pdbx_PDB_model_num"]; + if (!AreSameSize(atom_site_id, atom_site_model_id, *atoms)) { + return absl::InvalidArgumentError(absl::StrCat( + "Invalid '_atom_site.' loop. ", // + "len(id)= ", atom_site_id.size(), ", ", // + "len(pdbx_PDB_model_num)= ", atom_site_model_id.size(), ", ", // + "len(atoms)= ", atoms->size(), ".")); // + } + + // Build maps from atom ID tuples to the rows in _struct_conn where that + // atom appears (NB could be multiple). + const std::size_t struct_conn_size = struct_conn_id.size(); + BondIndexByAtom ptnr1_rows_by_atom(struct_conn_size); + BondIndexByAtom ptnr2_rows_by_atom(struct_conn_size); + for (std::size_t i = 0; i < struct_conn_size; ++i) { + ptnr1_rows_by_atom[ptnr1_atoms->GetAtom(i)].push_back(i); + ptnr2_rows_by_atom[ptnr2_atoms->GetAtom(i)].push_back(i); + } + + // Allocate two output arrays with one element per row in struct_conn, where + // each element will be the index of that atom in the atom_site table. + // Fill the arrays with atom_site_size, which is an invalid value, so that + // we can check at the end that each atom has been found. + const std::size_t atom_site_size = atom_site_id.size(); + BondAtomIndices ptnr1_atom_indices(struct_conn_size, atom_site_size); + BondAtomIndices ptnr2_atom_indices(struct_conn_size, atom_site_size); + + bool model_id_ecountered = false; + absl::flat_hash_set seen_alt_ids; + for (std::size_t atom_i = 0; atom_i < atom_site_size; ++atom_i) { + if (atom_site_model_id[atom_i] != model_id) { + if (!model_id_ecountered) { + continue; + } else { + // Models are contiguous so once we see a different model ID after + // encountering our model ID then we can exit early. + break; + } + } else { + model_id_ecountered = true; + } + AtomId atom = atoms->GetAtom(atom_i); + seen_alt_ids.insert(atom.alt_id); + + if (auto fill_in_bonds_status1 = FillInBondsForAtom( + ptnr1_rows_by_atom, atom, atom_i, ptnr1_atom_indices); + !fill_in_bonds_status1.ok()) { + return fill_in_bonds_status1; + } + if (auto fill_in_bonds_status2 = FillInBondsForAtom( + ptnr2_rows_by_atom, atom, atom_i, ptnr2_atom_indices); + !fill_in_bonds_status2.ok()) { + return fill_in_bonds_status2; + } + } + // The seen_alt_ids check is a workaround for a known PDB issue: some mmCIFs + // (2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024) + // have multiple models and they set different whole-chain altloc in each + // model. The bond table however doesn't distinguish between models, so there + // are bonds that are valid only for some models. E.g. 2grz has model 1 with + // chain A with altloc A, and model 2 with chain A with altloc B. The bonds + // table lists a bond for each of these. + + // Check that a ptnr1 atom was found for every bond. + if (auto row_it = absl::c_find(ptnr1_atom_indices, atom_site_size); + row_it != ptnr1_atom_indices.end()) { + if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") || + seen_alt_ids.contains("?")) { + std::size_t i = std::distance(ptnr1_atom_indices.begin(), row_it); + return absl::InvalidArgumentError( + absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ", + "Cannot find atom for bond ID ", struct_conn_id[i], ": ", + ptnr1_atoms->GetAtomString(i))); + } + } + + // Check that a ptnr2 atom was found for every bond. + if (auto row_it = absl::c_find(ptnr2_atom_indices, atom_site_size); + row_it != ptnr2_atom_indices.end()) { + if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") || + seen_alt_ids.contains("?")) { + std::size_t i = std::distance(ptnr2_atom_indices.begin(), row_it); + return absl::InvalidArgumentError( + absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ", + "Cannot find atom for bond ID ", struct_conn_id[i], ": ", + ptnr2_atoms->GetAtomString(i))); + } + } + + if (!model_id_ecountered) { + return absl::InvalidArgumentError(absl::StrCat( + "Error parsing \"", mmcif.GetDataName(), "\". model_id \"", model_id, + "\" not found in _atom_site.pdbx_PDB_model_num.")); + } + + return std::make_pair(std::move(ptnr1_atom_indices), + std::move(ptnr2_atom_indices)); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc new file mode 100644 index 000000000..111715ab5 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc @@ -0,0 +1,68 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include "absl/strings/string_view.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "alphafold3/structure/cpp/mmcif_struct_conn.h" +#include "pybind11/gil.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" + +namespace alphafold3 { + +namespace py = pybind11; + +constexpr char kGetBondAtomIndices[] = R"( +Extracts the indices of the atoms that participate in bonds. + +This function has a workaround for a known PDB issue: some mmCIFs have +(2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024) +multiple models and they set different whole-chain altloc in each model. +The bond table however doesn't distinguish between models, so there are +bonds that are valid only for some models. E.g. 2grz has model 1 with +chain A with altloc A, and model 2 with chain A with altloc B. The bonds +table lists a bond for each of these. This case is rather rare (10 cases +in PDB as of 2024). For the offending bonds, the returned atom index is +set to the size of the atom_site table, i.e. it is an invalid index. + +Args: + mmcif: The mmCIF object to process. + model_id: The ID of the model that the returned atoms will belong to. This + should be a value in the mmCIF's _atom_site.pdbx_PDB_model_num column. + +Returns: + Two lists of atom indices, `from_atoms` and `to_atoms`, each one having + length num_bonds (as defined by _struct_conn, the bonds table). The bond + i, defined by the i'th row in _struct_conn, is a bond from atom at index + from_atoms[i], to the atom at index to_atoms[i]. The indices are simple + 0-based indexes into the columns of the _atom_site table in the input + mmCIF, and do not necessarily correspond to the values in _atom_site.id, + or any other column. +)"; + +void RegisterModuleMmcifStructConn(pybind11::module m) { + m.def( + "get_bond_atom_indices", + [](const CifDict& mmcif, absl::string_view model_id) { + auto result = GetBondAtomIndices(mmcif, model_id); + if (result.ok()) { + return *result; + } + throw py::value_error(std::string(result.status().message())); + }, + py::arg("mmcif_dict"), py::arg("model_id"), + py::doc(kGetBondAtomIndices + 1), + py::call_guard()); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h new file mode 100644 index 000000000..acdbf7b77 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMmcifStructConn(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_STRUCT_CONN_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils.pyi new file mode 100644 index 000000000..aa2dc23e9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils.pyi @@ -0,0 +1,71 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from collections.abc import Sequence + +import numpy as np + +from alphafold3.cpp import cif_dict +from alphafold3.structure.python import mmcif_layout + + +def filter( + mmcif: cif_dict.CifDict, + include_nucleotides: bool, + include_ligands: bool = ..., + include_water: bool = ..., + include_other: bool = ..., + model_id: str = ..., +) -> tuple[np.ndarray[int], mmcif_layout.MmcifLayout]: ... + + +def fix_residues( + layout: mmcif_layout.MmcifLayout, + comp_id: Sequence[str], + atom_id: Sequence[str], + atom_x: Sequence[float], + atom_y: Sequence[float], + atom_z: Sequence[float], + fix_arg: bool = ..., +) -> None: ... + + +def read_layout( + mmcif: cif_dict.CifDict, model_id: str = ... +) -> mmcif_layout.MmcifLayout: ... + + +def selected_ligand_residue_mask( + layout: mmcif_layout.MmcifLayout, + atom_site_label_asym_ids: list[str], + atom_site_label_seq_ids: list[str], + atom_site_auth_seq_ids: list[str], + atom_site_label_comp_ids: list[str], + atom_site_pdbx_pdb_ins_codes: list[str], + nonpoly_asym_ids: list[str], + nonpoly_auth_seq_ids: list[str], + nonpoly_pdb_ins_codes: list[str], + nonpoly_mon_ids: list[str], + branch_asym_ids: list[str], + branch_auth_seq_ids: list[str], + branch_pdb_ins_codes: list[str], + branch_mon_ids: list[str], +) -> tuple[list[bool], list[bool]]: ... + + +def selected_polymer_residue_mask( + layout: mmcif_layout.MmcifLayout, + atom_site_label_asym_ids: list[str], + atom_site_label_seq_ids: list[str], + atom_site_label_comp_ids: list[str], + poly_seq_asym_ids: list[str], + poly_seq_seq_ids: list[str], + poly_seq_mon_ids: list[str], +) -> list[bool]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc new file mode 100644 index 000000000..52bd039b2 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc @@ -0,0 +1,787 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "numpy/ndarrayobject.h" +#include "numpy/ndarraytypes.h" +#include "numpy/npy_common.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/memory/memory.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "alphafold3/parsers/cpp/cif_dict_lib.h" +#include "alphafold3/structure/cpp/mmcif_altlocs.h" +#include "alphafold3/structure/cpp/mmcif_layout.h" +#include "pybind11/cast.h" +#include "pybind11/gil.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11/stl.h" +#include "pybind11_abseil/absl_casters.h" + +namespace alphafold3 { +namespace { +namespace py = pybind11; + +struct PyObjectDeleter { + inline void operator()(PyObject* obj) const { Py_CLEAR(obj); } +}; + +using ScopedPyObject = std::unique_ptr; + +using StringArrayRef = absl::Span; +using Indexer = absl::flat_hash_map; + +// Returns the reverse look-up map of name to index. +Indexer MakeIndex(StringArrayRef col) { + Indexer index; + index.reserve(col.size()); + for (std::size_t i = 0; i < col.size(); ++i) { + index[col[i]] = i; + } + return index; +} + +// Returns whether each container is the same size. +template +bool AreSameSize(C c, const Cs&... cs) { + return ((c.size() == cs.size()) && ...); +} + +// Stores references to columns in `_atom_site` ensuring they all exist and +// are the same size. +struct AtomSiteLoop { + explicit AtomSiteLoop(const CifDict& cif_dict) + : id(cif_dict["_atom_site.id"]), + model_id(cif_dict["_atom_site.pdbx_PDB_model_num"]), + chain_id(cif_dict["_atom_site.label_asym_id"]), + seq_id(cif_dict["_atom_site.label_seq_id"]), + + comp_id(cif_dict["_atom_site.label_comp_id"]), + atom_id(cif_dict["_atom_site.label_atom_id"]), + + alt_id(cif_dict["_atom_site.label_alt_id"]), + occupancy(cif_dict["_atom_site.occupancy"]) + + { + if (!AreSameSize(id, model_id, chain_id, seq_id, comp_id, atom_id, alt_id, + occupancy)) { + throw py::value_error( + absl::StrCat("Invalid '_atom_site.' loop. ", // + "len(id)=", id.size(), ", ", // + "len(pdbx_PDB_model_num)=", model_id.size(), ", ", // + "len(label_asym_id)=", chain_id.size(), ", ", // + "len(label_seq_id)=", seq_id.size(), ", ", // + "len(label_comp_id)=", comp_id.size(), ", ", // + "len(atom_id)=", atom_id.size(), ", ", // + "len(label_alt_id)=", alt_id.size(), ", ", // + "len(occupancy)=", occupancy.size())); + } + } + StringArrayRef id; + StringArrayRef model_id; + StringArrayRef chain_id; + StringArrayRef seq_id; + StringArrayRef comp_id; + StringArrayRef atom_id; + StringArrayRef alt_id; + StringArrayRef occupancy; +}; + +// Stores references to columns in `_entity` ensuring they all exist and are the +// same size. +struct EntityLoop { + explicit EntityLoop(const CifDict& cif_dict) + : id(cif_dict["_entity.id"]), type(cif_dict["_entity.type"]) { + if (!AreSameSize(id, type)) { + throw py::value_error(absl::StrCat("Invalid '_entity.' loop. ", // + "len(id)=", id.size(), ", ", // + "len(type)=", type.size())); + } + } + StringArrayRef id; + StringArrayRef type; +}; + +// Stores references to columns in `_entity_poly` ensuring they all exist and +// are the same size. +struct EntityPolyLoop { + explicit EntityPolyLoop(const CifDict& cif_dict) + : entity_id(cif_dict["_entity_poly.entity_id"]), + type(cif_dict["_entity_poly.type"]) { + if (!AreSameSize(entity_id, type)) { + throw py::value_error(absl::StrCat("Invalid '_entity_poly.' loop. ", // + "len(entity_id)=", entity_id.size(), + ", ", // + "len(type)=", type.size())); + } + } + StringArrayRef entity_id; + StringArrayRef type; +}; + +// Returns a set of entity names removing ones not included by the flags +// specified. +absl::flat_hash_set SelectChains(const CifDict& mmcif, + bool include_nucleotides, + bool include_ligands, + bool include_water, + bool include_other) { + EntityLoop entity_loop(mmcif); + EntityPolyLoop entity_poly(mmcif); + absl::flat_hash_set permitted_polymers{"polypeptide(L)"}; + absl::flat_hash_set forbidden_polymers; + for (absl::string_view type : + {"polydeoxyribonucleotide", "polyribonucleotide", + "polydeoxyribonucleotide/polyribonucleotide hybrid"}) { + if (include_nucleotides) { + permitted_polymers.emplace(type); + } else { + forbidden_polymers.emplace(type); + } + } + + absl::flat_hash_set permitted_nonpoly_entity_types; + absl::flat_hash_set forbidden_nonpoly_entity_types; + for (absl::string_view type : {"non-polymer", "branched"}) { + if (include_ligands) { + permitted_nonpoly_entity_types.emplace(type); + } else { + forbidden_nonpoly_entity_types.emplace(type); + } + } + absl::string_view water_type = "water"; + if (include_water) { + permitted_nonpoly_entity_types.emplace(water_type); + } else { + forbidden_nonpoly_entity_types.emplace(water_type); + } + + StringArrayRef chain_ids = mmcif["_struct_asym.id"]; + StringArrayRef entity_ids = mmcif["_struct_asym.entity_id"]; + Indexer chain_index = MakeIndex(chain_ids); + Indexer entity_poly_index = MakeIndex(entity_poly.entity_id); + Indexer entity_id_to_index = MakeIndex(entity_loop.id); + + absl::flat_hash_set keep_chain_id; + for (std::size_t i = 0; i < chain_ids.size(); ++i) { + absl::string_view chain_id = chain_ids[i]; + absl::string_view entity_id = entity_ids[i]; + if (entity_id_to_index.empty() || + entity_loop.type[entity_id_to_index[entity_id]] == "polymer") { + if (auto it = entity_poly_index.find(entity_id); + it != entity_poly_index.end()) { + absl::string_view poly_type = entity_poly.type[it->second]; + if (include_other) { + if (!forbidden_polymers.contains(poly_type)) { + keep_chain_id.insert(chain_id); + } + } else { + if (permitted_polymers.contains(poly_type)) { + keep_chain_id.insert(chain_id); + } + } + } + } else { + absl::string_view entity_type = + entity_loop.type[entity_id_to_index[entity_id]]; + if (include_other) { + if (!forbidden_nonpoly_entity_types.contains(entity_type)) { + keep_chain_id.insert(chain_id); + continue; + } + } else { + if (permitted_nonpoly_entity_types.contains(entity_type)) { + keep_chain_id.insert(chain_id); + continue; + } + } + } + } + return keep_chain_id; +} + +class ProcessResidue { + public: + explicit ProcessResidue(const char* residue) + : residue_(PyUnicode_InternFromString(residue)) {} + bool IsResidue(PyObject* residue) { + return ArePyObjectsEqual(residue_.get(), residue); + } + + static bool ArePyObjectsEqual(PyObject* lhs, PyObject* rhs) { + switch (PyObject_RichCompareBool(lhs, rhs, Py_EQ)) { + case -1: + PyErr_Clear(); + return false; + case 0: + return false; + default: + return true; + } + } + + private: + ScopedPyObject residue_; +}; + +struct Position3 { + float x; + float y; + float z; +}; + +float DistanceSquared(Position3 v1, Position3 v2) { + float dx = v1.x - v2.x; + float dy = v1.y - v2.y; + float dz = v1.z - v2.z; + return dx * dx + dy * dy + dz * dz; +} + +class FixArginine : public ProcessResidue { + public: + FixArginine() + : ProcessResidue("ARG"), + cd_(PyUnicode_InternFromString("CD")), + nh1_(PyUnicode_InternFromString("NH1")), + nh2_(PyUnicode_InternFromString("NH2")), + hh11_(PyUnicode_InternFromString("HH11")), + hh21_(PyUnicode_InternFromString("HH21")), + hh12_(PyUnicode_InternFromString("HH12")), + hh22_(PyUnicode_InternFromString("HH22")) {} + void Fix(absl::Span atom_ids, absl::Span atom_x, + absl::Span atom_y, absl::Span atom_z) { + std::ptrdiff_t cd_index = -1; + std::ptrdiff_t nh1_index = -1; + std::ptrdiff_t nh2_index = -1; + std::ptrdiff_t hh11_index = -1; + std::ptrdiff_t hh21_index = -1; + std::ptrdiff_t hh12_index = -1; + std::ptrdiff_t hh22_index = -1; + for (std::ptrdiff_t index = 0; index < atom_ids.size(); ++index) { + PyObject* atom_id = atom_ids[index]; + if (cd_index == -1 && ArePyObjectsEqual(atom_id, cd_.get())) { + cd_index = index; + } else if (nh1_index == -1 && ArePyObjectsEqual(atom_id, nh1_.get())) { + nh1_index = index; + } else if (nh2_index == -1 && ArePyObjectsEqual(atom_id, nh2_.get())) { + nh2_index = index; + } else if (hh11_index == -1 && ArePyObjectsEqual(atom_id, hh11_.get())) { + hh11_index = index; + } else if (hh21_index == -1 && ArePyObjectsEqual(atom_id, hh21_.get())) { + hh21_index = index; + } else if (hh12_index == -1 && ArePyObjectsEqual(atom_id, hh12_.get())) { + hh12_index = index; + } else if (hh22_index == -1 && ArePyObjectsEqual(atom_id, hh22_.get())) { + hh22_index = index; + } + } + if (cd_index < 0 || nh1_index < 0 || nh2_index < 0) { + return; + } + Position3 cd_pos(atom_x[cd_index], atom_y[cd_index], atom_z[cd_index]); + Position3 nh1_pos(atom_x[nh1_index], atom_y[nh1_index], atom_z[nh1_index]); + Position3 nh2_pos(atom_x[nh2_index], atom_y[nh2_index], atom_z[nh2_index]); + if (DistanceSquared(nh1_pos, cd_pos) <= DistanceSquared(nh2_pos, cd_pos)) { + return; + } + std::swap(atom_ids[nh1_index], atom_ids[nh2_index]); + if (hh11_index >= 0 && hh21_index >= 0) { + std::swap(atom_ids[hh11_index], atom_ids[hh21_index]); + } else if (hh11_index >= 0) { + Py_DECREF(atom_ids[hh11_index]); + Py_INCREF(hh21_.get()); + atom_ids[hh11_index] = hh21_.get(); + } else if (hh21_index >= 0) { + Py_DECREF(atom_ids[hh21_index]); + Py_INCREF(hh11_.get()); + atom_ids[hh21_index] = hh11_.get(); + } + if (hh12_index >= 0 && hh22_index >= 0) { + std::swap(atom_ids[hh12_index], atom_ids[hh22_index]); + } else if (hh12_index >= 0) { + Py_DECREF(atom_ids[hh12_index]); + Py_INCREF(hh22_.get()); + atom_ids[hh12_index] = hh22_.get(); + } else if (hh22_index >= 0) { + Py_DECREF(atom_ids[hh22_index]); + Py_INCREF(hh21_.get()); + atom_ids[hh22_index] = hh21_.get(); + } + } + + private: + ScopedPyObject cd_; + ScopedPyObject nh1_; + ScopedPyObject nh2_; + ScopedPyObject hh11_; + ScopedPyObject hh21_; + ScopedPyObject hh12_; + ScopedPyObject hh22_; +}; + +// Returns the layout of the mmCIF `_atom_site` table. +inline MmcifLayout ReadMmcifLayout(const CifDict& mmcif, + absl::string_view model_id = "") { + py::gil_scoped_release release; + auto mmcif_layout = MmcifLayout::Create(mmcif, model_id); + if (mmcif_layout.ok()) { + return *mmcif_layout; + } + + throw py::value_error(std::string(mmcif_layout.status().message())); +} + +std::pair MmcifFilter( // + const CifDict& mmcif, // + bool include_nucleotides, // + bool include_ligands, // + bool include_water, // + bool include_other, // + absl::string_view model_id) { + if (_import_array() < 0) { + throw py::import_error("Failed to import NumPy."); + } + auto layout = ReadMmcifLayout(mmcif, model_id); + std::unique_ptr> keep_indices; + size_t new_num_atoms; + + { + py::gil_scoped_release release; + + AtomSiteLoop atom_site(mmcif); + + auto keep_chain_ids = + SelectChains(mmcif, include_nucleotides, include_ligands, include_water, + include_other); + + std::vector chain_indices; + chain_indices.reserve(keep_chain_ids.size()); + for (std::size_t i = 0; i < layout.num_chains(); ++i) { + if (keep_chain_ids.contains( + atom_site.chain_id[layout.atom_site_from_chain_index(i)])) { + chain_indices.push_back(i); + } + } + + keep_indices = + absl::WrapUnique(new std::vector(ResolveMmcifAltLocs( + layout, atom_site.comp_id, atom_site.atom_id, atom_site.alt_id, + atom_site.occupancy, chain_indices))); + new_num_atoms = keep_indices->size(); + + if (layout.num_models() > 1) { + keep_indices->reserve(layout.num_models() * new_num_atoms); + std::uint64_t* start = &(*keep_indices->begin()); + std::size_t num_atom = keep_indices->size(); + // Copy first model indices into all model indices offsetting each copy. + for (std::size_t i = 1; i < layout.num_models(); ++i) { + std::size_t offset = i * layout.num_atoms(); + std::transform(start, start + num_atom, + std::back_inserter(*keep_indices), + [offset](std::size_t v) { return v + offset; }); + } + } + } + + layout.Filter(*keep_indices); + + npy_intp shape[] = {static_cast(layout.num_models()), + static_cast(new_num_atoms)}; + PyObject* arr = + PyArray_SimpleNewFromData(2, shape, NPY_INT64, keep_indices->data()); + // Create a capsule to hold the memory of the buffer so NumPy knows how to + // delete it when done with it. + PyObject* capsule = PyCapsule_New( + keep_indices.release(), nullptr, +[](PyObject* capsule_cleanup) { + void* memory = PyCapsule_GetPointer(capsule_cleanup, nullptr); + delete static_cast*>(memory); + }); + PyArray_SetBaseObject(reinterpret_cast(arr), capsule); + + return std::make_pair(py::reinterpret_steal(arr), + std::move(layout)); +} + +void MmcifFixResidues( // + const MmcifLayout& layout, // + absl::Span comp_id, // + absl::Span atom_id, // + absl::Span atom_x, // + absl::Span atom_y, // + absl::Span atom_z, // + bool fix_arginine // +) { + std::optional arginine; + std::size_t num_atoms = layout.num_atoms(); + if (comp_id.size() != num_atoms || atom_id.size() != num_atoms || + atom_x.size() != num_atoms || atom_y.size() != num_atoms || + atom_z.size() != num_atoms) { + throw py::value_error( + absl::StrCat("Sizes must match. ", // + "num_atoms=", num_atoms, ", ", // + "len(comp_id)=", comp_id.size(), ", ", // + "len(atom_id)=", atom_id.size(), ", ", // + "len(atom_x)=", atom_x.size(), ", ", // + "len(atom_y)=", atom_y.size(), ", ", // + "len(atom_z)=", atom_z.size())); + } + + if (fix_arginine) { + arginine.emplace(); + } + if (!arginine.has_value()) { + return; + } + + for (std::size_t res_index = 0; res_index < layout.num_residues(); + ++res_index) { + auto [atom_start, atom_end] = layout.atom_range(res_index); + std::size_t atom_count = atom_end - atom_start; + PyObject* resname = comp_id[atom_start]; + if (arginine.has_value() && arginine->IsResidue(resname)) { + arginine->Fix(atom_id.subspan(atom_start, atom_count), + atom_x.subspan(atom_start, atom_count), + atom_y.subspan(atom_start, atom_count), + atom_z.subspan(atom_start, atom_count)); + } + } +} + +std::vector SelectedPolymerResidueMask( + const MmcifLayout& layout, + const std::vector& atom_site_label_asym_ids, // + const std::vector& atom_site_label_seq_ids, // + const std::vector& atom_site_label_comp_ids, // + const std::vector& poly_seq_asym_ids, // + const std::vector& poly_seq_seq_ids, // + const std::vector& poly_seq_mon_ids // +) { + absl::flat_hash_map, + absl::string_view> + selected; + selected.reserve(layout.num_residues()); + // layout.residues() is O(1) while layout.residue_starts() is O(num_res). + const std::vector& residue_starts = layout.residue_starts(); + for (int i = 0; i < layout.residues().size(); ++i) { + std::size_t res_start = residue_starts[i]; + std::size_t res_end = layout.residues()[i]; + if (res_start == res_end) { + continue; // Skip empty residues (containing no atoms). + } + + absl::string_view label_seq_id = atom_site_label_seq_ids[i]; + if (label_seq_id == ".") { + continue; // Skip non-polymers. + } + + absl::string_view label_asym_id = atom_site_label_asym_ids[i]; + absl::string_view label_comp_id = atom_site_label_comp_ids[i]; + selected[std::make_pair(label_asym_id, label_seq_id)] = label_comp_id; + } + + std::vector mask; + mask.reserve(poly_seq_mon_ids.size()); + for (int i = 0; i < poly_seq_mon_ids.size(); ++i) { + absl::string_view poly_seq_asym_id = poly_seq_asym_ids[i]; + absl::string_view poly_seq_seq_id = poly_seq_seq_ids[i]; + absl::string_view poly_seq_mon_id = poly_seq_mon_ids[i]; + + auto it = selected.find(std::make_pair(poly_seq_asym_id, poly_seq_seq_id)); + if (it != selected.end()) { + mask.push_back(it->second == poly_seq_mon_id); + } else { + mask.push_back(true); // Missing residues are not heterogeneous. + } + } + return mask; +} + +std::pair, std::vector> SelectedLigandResidueMask( + const MmcifLayout& layout, // + const std::vector& atom_site_label_asym_ids, // + const std::vector& atom_site_label_seq_ids, // + const std::vector& atom_site_auth_seq_ids, // + const std::vector& atom_site_label_comp_ids, // + const std::vector& atom_site_pdbx_pdb_ins_codes, // + const std::vector& nonpoly_asym_ids, // + const std::vector& nonpoly_auth_seq_ids, // + const std::vector& nonpoly_pdb_ins_codes, // + const std::vector& nonpoly_mon_ids, // + const std::vector& branch_asym_ids, // + const std::vector& branch_auth_seq_ids, // + const std::vector& branch_pdb_ins_codes, // + const std::vector& branch_mon_ids) { + absl::flat_hash_map< + std::tuple, + absl::string_view> + selected; + selected.reserve(layout.num_residues()); + // layout.residues() is O(1) while layout.residue_starts() is O(num_res). + const std::vector& residue_starts = layout.residue_starts(); + for (int i = 0; i < layout.residues().size(); ++i) { + std::size_t res_start = residue_starts[i]; + std::size_t res_end = layout.residues()[i]; + if (res_start == res_end) { + continue; // Skip empty residues (containing no atoms). + } + + absl::string_view label_seq_id = atom_site_label_seq_ids[i]; + if (label_seq_id != ".") { + continue; // Skip polymers. + } + + absl::string_view label_asym_id = atom_site_label_asym_ids[i]; + absl::string_view auth_seq_id = atom_site_auth_seq_ids[i]; + absl::string_view ins_code = atom_site_pdbx_pdb_ins_codes[i]; + ins_code = ins_code == "?" ? "." : ins_code; // Remap unknown to unset. + absl::string_view label_comp_id = atom_site_label_comp_ids[i]; + selected[std::make_tuple(label_asym_id, auth_seq_id, ins_code)] = + label_comp_id; + } + + std::vector nonpoly_mask; + nonpoly_mask.reserve(nonpoly_asym_ids.size()); + for (int i = 0; i < nonpoly_asym_ids.size(); ++i) { + absl::string_view nonpoly_asym_id = nonpoly_asym_ids[i]; + absl::string_view nonpoly_auth_seq_id = nonpoly_auth_seq_ids[i]; + absl::string_view nonpoly_ins_code = nonpoly_pdb_ins_codes[i]; + // Remap unknown to unset. + nonpoly_ins_code = nonpoly_ins_code == "?" ? "." : nonpoly_ins_code; + absl::string_view nonpoly_mon_id = nonpoly_mon_ids[i]; + + auto it = selected.find(std::make_tuple( + nonpoly_asym_id, nonpoly_auth_seq_id, nonpoly_ins_code)); + if (it != selected.end()) { + nonpoly_mask.push_back(it->second == nonpoly_mon_id); + } else { + nonpoly_mask.push_back(true); // Missing residues are not heterogeneous. + } + } + + std::vector branch_mask; + branch_mask.reserve(branch_asym_ids.size()); + for (int i = 0; i < branch_asym_ids.size(); ++i) { + absl::string_view branch_asym_id = branch_asym_ids[i]; + absl::string_view branch_auth_seq_id = branch_auth_seq_ids[i]; + + // Insertion codes in _pdbx_branch_scheme are not required and can be + // missing. Default to unset ('.') in such case. + absl::string_view branch_ins_code; + if (i < branch_pdb_ins_codes.size()) { + branch_ins_code = branch_pdb_ins_codes[i]; + // Remap unknown to unset. + branch_ins_code = branch_ins_code == "?" ? "." : branch_ins_code; + } else { + branch_ins_code = "."; + } + + absl::string_view branch_mon_id = branch_mon_ids[i]; + + auto it = selected.find( + std::make_tuple(branch_asym_id, branch_auth_seq_id, branch_ins_code)); + if (it != selected.end()) { + branch_mask.push_back(it->second == branch_mon_id); + } else { + branch_mask.push_back(true); // Missing residues are not heterogeneous. + } + } + + return std::make_pair(nonpoly_mask, branch_mask); +} + +constexpr char kReadMmcifLayout[] = R"( +Returns the layout of the cif_dict. + +Args: + mmcif: mmCIF to calculate the layout for. + model_id: If non-empty the layout of the given model is returned + otherwise the layout of all models are returned. +Raises: + ValueError: if the mmCIF is malformed or the number of atoms in each + model are inconsistent. +)"; + +constexpr char kMmcifFilter[] = R"( +Returns NumpyArray of selected rows in `_atom_site` and new layout. + +Args: + mmcif: mmCIF to filter. + include_nucleotides: Whether to include polymer entities of type: + "polypeptide(L)\", "polydeoxyribonucleotide", "polyribonucleotide". + Otherwise only "polypeptide(L)\". ("polypeptide(D)\" is never included.) + include_ligands: Whether to include non-polymer entities of type: + "non-polymer", "branched". + include_water: Whether to include entities of type water. + include_other: Whether to include other (non-standard) entity types + that are not covered by any of the above parameters. + model_id: If non-empty the model with given name is selected otherwise + all models are selected. + +Returns: + A tuple containing a numpy array with a shape (num_models, num_atoms) + with the atom_site indices selected and the new layout. + +Raises: + ValueError error if mmCIF dict does not have all required fields. +)"; + +constexpr char kMmcifFixResidues[] = R"( +Fixes residue columns in-place. + +Args: + layout: layout from filter command. + comp_id: '_atom_site.label_comp_id' of first model. + group: '_atom_site.group_PDB' of first model. + atom_id: '_atom_site.label_atom_id' of first model. + type_symbol: '_atom_site.type_symbol' of first model. + atom_x: '_atom_site.Cartn_x' of first model. + atom_y: '_atom_site.Cartn_y' of first model. + atom_z: '_atom_site.Cartn_z' of first model. + fix_mse: Whether to convert MSE residues into MET residues. + fix_arg: Whether to ensure the atoms in ARG are in the correct order. + fix_unknown_dna: Whether to convert DNA residues from N to DN. + dna_mask: Which atoms are from DNA chains. + +Raises: + ValueError: If shapes are invalid. +)"; + +constexpr char kSelectedPolymerResidueMask[] = R"( +Returns a _pdbx_poly_seq_scheme mask for selected hetero residues. + +Should be called after filtering the layout using mmcif_utils.filter. + +Args: + layout: Layout defining the _atom_site residue selection. + atom_site_label_asym_ids: Internal (label) chain ID, per selected residue. + atom_site_label_seq_ids: Internal (label) residue ID, per selected residue. + atom_site_label_comp_ids: Residue name, per selected residue. + poly_seq_asym_ids: Internal (label) chain ID, per residue. + poly_seq_seq_ids: Internal (label) residue ID, per residue. + poly_seq_mon_ids: Residue name, per residue. + +Returns: + A mask for the _pdbx_poly_seq_scheme table. If residues are selected + using this mask, they will have consistent heterogeneous residue + selection with the _atom_site table. +)"; + +constexpr char kSelectedLigandResidueMask[] = R"( +Returns masks for selected ligand hetero residues. + +Should be called after filtering the layout using mmcif_utils.filter. + +Args: + layout: Layout defining the _atom_site residue selection. + atom_site_label_asym_ids: Internal (label) chain ID, per selected residue. + atom_site_label_seq_ids: Internal (author) residue ID, per selected residue. + atom_site_auth_seq_ids: External (author) residue ID, per selected residue. + atom_site_label_comp_ids: Residue name, per selected residue. + atom_site_pdbx_pdb_ins_codes: Insertion code, per selected residue. + nonpoly_asym_ids: Internal (label) chain ID, per residue from + _pdbx_nonpoly_scheme. + nonpoly_auth_seq_ids: External (author) residue ID, per residue from + _pdbx_nonpoly_scheme. + nonpoly_pdb_ins_codes: Residue name, per residue from + _pdbx_nonpoly_scheme. + nonpoly_mon_ids: Insertion code, per residue from _pdbx_nonpoly_scheme. + branch_asym_ids: Internal (label) chain ID, per residue from + _pdbx_branch_scheme. + branch_auth_seq_ids: External (author) residue ID, per residue from + _pdbx_branch_scheme. + branch_pdb_ins_codes: Residue name, per residue from _pdbx_branch_scheme. + branch_mon_ids: Insertion code, per residue from _pdbx_branch_scheme. + +Returns: + A tuple with masks for _pdbx_nonpoly_scheme and _pdbx_branch_scheme. If + residues are selected using these masks, they will have consistent + heterogeneous residue selection with the _atom_site table. +)"; + +} // namespace + +void RegisterModuleMmcifUtils(pybind11::module m) { + m.def("read_layout", ReadMmcifLayout, + py::arg("mmcif"), // + py::arg("model_id") = "", // + py::doc(kReadMmcifLayout + 1) // + ); + + m.def("filter", MmcifFilter, // + py::arg("mmcif"), // + py::arg("include_nucleotides"), // + py::arg("include_ligands") = false, // + py::arg("include_water") = false, // + py::arg("include_other") = false, // + py::arg("model_id") = "", // + py::doc(kMmcifFilter + 1) // + ); + + m.def("fix_residues", MmcifFixResidues, + py::arg("layout"), // + py::arg("comp_id"), // + py::arg("atom_id"), // + py::arg("atom_x"), // + py::arg("atom_y"), // + py::arg("atom_z"), // + py::arg("fix_arg") = false, // + py::doc(kMmcifFixResidues + 1) // + ); + + m.def("selected_polymer_residue_mask", SelectedPolymerResidueMask, + py::arg("layout"), // + py::arg("atom_site_label_asym_ids"), // + py::arg("atom_site_label_seq_ids"), // + py::arg("atom_site_label_comp_ids"), // + py::arg("poly_seq_asym_ids"), // + py::arg("poly_seq_seq_ids"), // + py::arg("poly_seq_mon_ids"), // + py::call_guard(), // + py::doc(kSelectedPolymerResidueMask + 1) // + ); + + m.def("selected_ligand_residue_mask", SelectedLigandResidueMask, + py::arg("layout"), // + py::arg("atom_site_label_asym_ids"), // + py::arg("atom_site_label_seq_ids"), // + py::arg("atom_site_auth_seq_ids"), // + py::arg("atom_site_label_comp_ids"), // + py::arg("atom_site_pdbx_pdb_ins_codes"), // + py::arg("nonpoly_asym_ids"), // + py::arg("nonpoly_auth_seq_ids"), // + py::arg("nonpoly_pdb_ins_codes"), // + py::arg("nonpoly_mon_ids"), // + py::arg("branch_asym_ids"), // + py::arg("branch_auth_seq_ids"), // + py::arg("branch_pdb_ins_codes"), // + py::arg("branch_mon_ids"), // + py::call_guard(), // + py::doc(kSelectedLigandResidueMask + 1) // + ); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.h new file mode 100644 index 000000000..7ba19420b --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleMmcifUtils(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_MMCIF_UTILS_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array.pyi b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array.pyi new file mode 100644 index 000000000..b4b76c27f --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array.pyi @@ -0,0 +1,50 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from collections.abc import Sequence +from typing import Any, overload + +import numpy as np + + +def format_float_array( + values: Sequence[float], num_decimal_places: int +) -> list[str]: ... + + +def isin( + array: np.ndarray[object], + test_elements: set[str | bytes], + *, + invert: bool = ..., +) -> np.ndarray[bool]: ... + + +@overload +def remap( + array: np.ndarray[object], + mapping: dict[str, str], + default_value: str, + inplace: bool = ..., +) -> np.ndarray[object]: ... + + +@overload +def remap( + array: np.ndarray[object], + mapping: dict[str, str], + inplace: bool = ..., +) -> np.ndarray[object]: ... + + +def remap_multiple( + arrays: Sequence[np.ndarray[object]], + mapping: dict[tuple[Any], int], +) -> np.ndarray[int]: ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc new file mode 100644 index 000000000..29fac727a --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc @@ -0,0 +1,329 @@ +// Copyright 2024 DeepMind Technologies Limited +// +// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +// +// To request access to the AlphaFold 3 model parameters, follow the process set +// out at https://github.com/google-deepmind/alphafold3. You may only use these +// if received directly from Google. Use is subject to terms of use available at +// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "numpy/arrayobject.h" +#include "numpy/ndarrayobject.h" +#include "numpy/ndarraytypes.h" +#include "numpy/npy_common.h" +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "pybind11/cast.h" +#include "pybind11/numpy.h" +#include "pybind11/pybind11.h" +#include "pybind11/pytypes.h" +#include "pybind11_abseil/absl_casters.h" + +namespace { + +namespace py = pybind11; + +PyObject* RemapNumpyArrayObjects(PyObject* array, PyObject* mapping, + bool inplace, PyObject* default_value) { + import_array(); + if (!PyArray_Check(array)) { + PyErr_SetString(PyExc_TypeError, "'array' must be a np.ndarray."); + return nullptr; + } + if (!PyDict_Check(mapping)) { + PyErr_SetString(PyExc_TypeError, "'mapping' must be a Python dict."); + return nullptr; + } + + PyArrayObject* array_obj = reinterpret_cast(array); + if (PyArray_TYPE(array_obj) != NPY_OBJECT) { + PyErr_SetString(PyExc_TypeError, "`array` must be an array of objects."); + return nullptr; + } + + if (inplace) { + // We are returning original array so we need to increase the ref count. + Py_INCREF(array); + } else { + // We are returning a fresh copy. + array = PyArray_NewCopy(array_obj, NPY_CORDER); + if (array == nullptr) { + PyErr_SetString(PyExc_MemoryError, "Out of memory!"); + return nullptr; + } + array_obj = reinterpret_cast(array); + } + + if (PyArray_SIZE(array_obj) == 0) { + return array; + } + + if (default_value == nullptr && PyDict_Size(mapping) == 0) { + return array; + } + + NpyIter* iter = NpyIter_New( + array_obj, NPY_ITER_READWRITE | NPY_ITER_EXTERNAL_LOOP | NPY_ITER_REFS_OK, + NPY_KEEPORDER, NPY_NO_CASTING, nullptr); + if (iter == nullptr) { + PyErr_SetString(PyExc_MemoryError, "Out of memory!"); + Py_XDECREF(array); + return nullptr; + } + + NpyIter_IterNextFunc* iter_next = NpyIter_GetIterNext(iter, nullptr); + if (iter_next == nullptr) { + NpyIter_Deallocate(iter); + Py_XDECREF(array); + PyErr_SetString(PyExc_MemoryError, "Out of memory!"); + return nullptr; + } + + // Iterating arrays taken from: + // https://numpy.org/doc/stable/reference/c-api/iterator.html + char** data_pointer = NpyIter_GetDataPtrArray(iter); + npy_intp* stride_pointer = NpyIter_GetInnerStrideArray(iter); + npy_intp* inner_size_pointer = NpyIter_GetInnerLoopSizePtr(iter); + do { + char* data = *data_pointer; + npy_intp stride = *stride_pointer; + npy_intp count = *inner_size_pointer; + for (size_t i = 0; i < count; ++i) { + PyObject* entry; + std::memcpy(&entry, data, sizeof(PyObject*)); + PyObject* result = PyDict_GetItem(mapping, entry); + if (result != nullptr) { + // Replace entry. + Py_INCREF(result); + Py_XDECREF(entry); + std::memcpy(data, &result, sizeof(PyObject*)); + } else if (default_value != nullptr) { + // Replace entry with a default value. + Py_INCREF(default_value); + Py_XDECREF(entry); + std::memcpy(data, &default_value, sizeof(PyObject*)); + } + data += stride; + } + } while (iter_next(iter)); + + NpyIter_Deallocate(iter); + return array; +} + +// Convert 1D Numpy float array to a list of strings where each string has fixed +// number of decimal points. This is faster than Python list comprehension. +std::vector FormatFloatArray(absl::Span values, + int num_decimal_places) { + std::vector output; + output.reserve(values.size()); + + absl::c_transform(values, std::back_inserter(output), + [num_decimal_places](float value) { + return absl::StrFormat("%.*f", num_decimal_places, value); + }); + return output; +} + +py::array_t IsIn( + const py::array_t& array, + const absl::flat_hash_set& test_elements, bool invert) { + const size_t num_elements = array.size(); + py::array_t output(num_elements); + std::fill(output.mutable_data(), output.mutable_data() + output.size(), + invert); + + // Shortcut: The output will be trivially always false if test_elements empty. + if (test_elements.empty()) { + return output; + } + + for (size_t i = 0; i < num_elements; ++i) { + // Compare the string values instead of comparing just object pointers. + py::handle handle = array.data()[i]; + if (!PyUnicode_Check(handle.ptr()) && !PyBytes_Check(handle.ptr())) { + continue; + } + if (test_elements.contains(py::cast(handle))) { + output.mutable_data()[i] = !invert; + } + } + if (array.ndim() > 1) { + auto shape = + std::vector(array.shape(), array.shape() + array.ndim()); + return output.reshape(shape); + } + return output; +} + +py::array RemapMultipleArrays( + const std::vector>& arrays, + const py::dict& mapping) { + size_t array_size = arrays[0].size(); + for (const auto& array : arrays) { + if (array.size() != array_size) { + throw py::value_error("All arrays must have the same length."); + } + } + + // Create a result buffer. + auto result = py::array_t(array_size); + absl::Span result_buffer(result.mutable_data(), array_size); + PyObject* entry = PyTuple_New(arrays.size()); + if (entry == nullptr) { + throw py::error_already_set(); + } + std::vector> array_spans; + array_spans.reserve(arrays.size()); + for (const auto& array : arrays) { + array_spans.emplace_back(array.data(), array.size()); + } + + // Iterate over arrays and look up elements in the `py_dict`. + bool fail = false; + for (size_t i = 0; i < array_size; ++i) { + for (size_t j = 0; j < array_spans.size(); ++j) { + PyTuple_SET_ITEM(entry, j, array_spans[j][i]); + } + PyObject* result = PyDict_GetItem(mapping.ptr(), entry); + if (result != nullptr) { + int64_t result_value = PyLong_AsLongLong(result); + if (result_value == -1 && PyErr_Occurred()) { + fail = true; + break; + } + if (result_value > std::numeric_limits::max() || + result_value < std::numeric_limits::lowest()) { + PyErr_SetString(PyExc_OverflowError, "Result value too large."); + fail = true; + break; + } + result_buffer[i] = result_value; + } else { + PyErr_Format(PyExc_KeyError, "%R", entry); + fail = true; + break; + } + } + + for (size_t j = 0; j < array_spans.size(); ++j) { + PyTuple_SET_ITEM(entry, j, nullptr); + } + Py_XDECREF(entry); + if (fail) { + throw py::error_already_set(); + } + return result; +} + +constexpr char kRemapNumpyArrayObjects[] = R"( +Replace objects in NumPy array of objects using mapping. + +Args: + array: NumPy array with dtype=object. + mapping: Dict mapping old values to new values. + inplace: Bool (default False) whether to replace values inplace or to + create a new array. + default_value: If given, what value to map to if the mapping is missing + for that particular item. If not given, such items are left unchanged. + +Returns + NumPy array of dtype object with values replaced according to mapping. + If inplace is True the original array is modified inplace otherwise a + new array is returned. +)"; + +constexpr char kFormatFloatArrayDoc[] = R"( +Converts float -> string array with given number of decimal places. +)"; + +constexpr char kIsInDoc[] = R"( +Computes whether each element is in test_elements. + +Same use as np.isin, but much faster. If len(array) = n, len(test_elements) = m: +* This function has complexity O(n). +* np.isin with arrays of objects has complexity O(m*log(m) + n * log(m)). + +Args: + array: Input NumPy array with dtype=object. + test_elements: The values against which to test each value of array. + invert: If True, the values in the returned array are inverted, as if + calculating `element not in test_elements`. Default is False. + `isin(a, b, invert=True)` is equivalent to but faster than `~isin(a, b)`. + +Returns + A boolean array of the same shape as the input array. Each value `val` is: + * `val in test_elements` if `invert=False`, + * `val not in test_elements` if `invert=True`. +)"; + +constexpr char kRemapMultipleDoc[] = R"( +Maps keys from multiple aligned arrays to a single array. + +Args: + arrays: Numpy arrays of the same length. The tuple of aligned entries is used + as key for the mapping. + mapping: Dict mapping from tuples to integer values. + +Returns + NumPy array of dtype `int` with values looked up in mapping according to the + tuple of aligned array entries as keys. +)"; + +} // namespace + +namespace alphafold3 { + +void RegisterModuleStringArray(pybind11::module m) { + m.def( + "remap", + [](py::object array, py::object mapping, bool inplace, + py::object default_value) -> py::object { + PyObject* result = RemapNumpyArrayObjects(array.ptr(), mapping.ptr(), + inplace, default_value.ptr()); + if (result == nullptr) { + throw py::error_already_set(); + } + return py::reinterpret_steal(result); + }, + py::return_value_policy::take_ownership, py::arg("array"), + py::arg("mapping"), py::arg("inplace") = false, py::arg("default_value"), + py::doc(kRemapNumpyArrayObjects + 1)); + m.def( + "remap", + [](py::object array, py::object mapping, bool inplace) -> py::object { + PyObject* result = RemapNumpyArrayObjects(array.ptr(), mapping.ptr(), + inplace, nullptr); + if (result == nullptr) { + throw py::error_already_set(); + } + return py::reinterpret_steal(result); + }, + py::return_value_policy::take_ownership, py::arg("array"), + py::arg("mapping"), py::arg("inplace") = false, + py::doc(kRemapNumpyArrayObjects + 1)); + m.def("format_float_array", &FormatFloatArray, py::arg("values"), + py::arg("num_decimal_places"), py::doc(kFormatFloatArrayDoc + 1), + py::call_guard()); + m.def("isin", &IsIn, py::arg("array"), py::arg("test_elements"), + py::kw_only(), py::arg("invert") = false, py::doc(kIsInDoc + 1)); + m.def("remap_multiple", &RemapMultipleArrays, py::arg("arrays"), + py::arg("mapping"), py::doc(kRemapMultipleDoc + 1)); +} + +} // namespace alphafold3 diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.h b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.h new file mode 100644 index 000000000..85790ddd8 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.h @@ -0,0 +1,24 @@ +/* + * Copyright 2024 DeepMind Technologies Limited + * + * AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of + * this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ + * + * To request access to the AlphaFold 3 model parameters, follow the process set + * out at https://github.com/google-deepmind/alphafold3. You may only use these + * if received directly from Google. Use is subject to terms of use available at + * https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + */ + +#ifndef ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ +#define ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ + +#include "pybind11/pybind11.h" + +namespace alphafold3 { + +void RegisterModuleStringArray(pybind11::module m); + +} + +#endif // ALPHAFOLD3_SRC_ALPHAFOLD3_STRUCTURE_PYTHON_STRING_ARRAY_PYBIND_H_ diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py new file mode 100644 index 000000000..d1b71c028 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py @@ -0,0 +1,333 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Low level mmCIF parsing operations and wrappers for nicer C++/Py errors. + +Note that the cif_dict.CifDict class has many useful methods to help with data +extraction which are not shown in this file. You can find them in cif_dict.clif +together with docstrings. The cif_dict.CifDict class behaves like an immutable +Python dictionary (some methods are not implemented though). +""" +from collections.abc import Callable, Mapping, Sequence +import functools +import itertools +import re +from typing import ParamSpec, TypeAlias, TypeVar + +from alphafold3.constants import chemical_components +from alphafold3.cpp import cif_dict +from alphafold3.cpp import mmcif_atom_site +from alphafold3.cpp import mmcif_struct_conn +from alphafold3.cpp import string_array +import numpy as np + +Mmcif = cif_dict.CifDict + + +_P = ParamSpec('_P') +_T = TypeVar('_T') +_WappedFn: TypeAlias = Callable[_P, _T] + + +@functools.lru_cache(maxsize=256) +def int_id_to_str_id(num: int) -> str: + """Encodes a number as a string, using reverse spreadsheet style naming. + + Args: + num: A positive integer. + + Returns: + A string that encodes the positive integer using reverse spreadsheet style, + naming e.g. 1 = A, 2 = B, ..., 27 = AA, 28 = BA, 29 = CA, ... This is the + usual way to encode chain IDs in mmCIF files. + """ + if num <= 0: + raise ValueError(f'Only positive integers allowed, got {num}.') + + num = num - 1 # 1-based indexing. + output = [] + while num >= 0: + output.append(chr(num % 26 + ord('A'))) + num = num // 26 - 1 + return ''.join(output) + + +@functools.lru_cache(maxsize=256) +def str_id_to_int_id(str_id: str) -> int: + """Encodes an mmCIF-style string chain ID as an integer. + + The integer IDs are one based so this function is the inverse of + int_id_to_str_id. + + Args: + str_id: A string chain ID consisting only of upper case letters A-Z. + + Returns: + An integer that can be used to order mmCIF chain IDs in the standard + (reverse spreadsheet style) ordering. + """ + if not re.match('^[A-Z]+$', str_id): + raise ValueError( + f'String ID must be upper case letters, got {str_id}.') + + offset = ord('A') - 1 + output = 0 + for i, c in enumerate(str_id): + output += (ord(c) - offset) * int(26**i) + return output + + +def from_string(mmcif_string: str | bytes) -> Mmcif: + return cif_dict.from_string(mmcif_string) + + +def parse_multi_data_cif(cif_string: str) -> dict[str, Mmcif]: + """Parses a CIF string with multiple data records. + + For instance, the CIF string: + + ``` + data_001 + _foo bar + # + data_002 + _foo baz + ``` + + is parsed as: + + ``` + {'001': Mmcif({'_foo': ['bar']}), '002': Mmcif({'_foo': ['baz']})} + ``` + + Args: + cif_string: The multi-data CIF string to be parsed. + + Returns: + A dictionary mapping record names to Mmcif objects with data. + """ + return cif_dict.parse_multi_data_cif(cif_string) + + +def tokenize(mmcif_string: str) -> list[str]: + return cif_dict.tokenize(mmcif_string) + + +def split_line(line: str) -> list[str]: + return cif_dict.split_line(line) + + +class BondParsingError(Exception): + """Exception raised by errors when getting bond atom indices.""" + + +def get_bond_atom_indices( + mmcif: Mmcif, + model_id: str = '1', +) -> tuple[Sequence[int], Sequence[int]]: + """Extracts the indices of the atoms that participate in bonds. + + Args: + mmcif: The mmCIF object to process. + model_id: The ID of the model that the returned atoms will belong to. This + should be a value in the mmCIF's _atom_site.pdbx_PDB_model_num column. + + Returns: + Two lists of atom indices, `from_atoms` and `to_atoms`, each one having + length num_bonds (as defined by _struct_conn, the bonds table). The bond + i, defined by the i'th row in _struct_conn, is a bond from atom at index + from_atoms[i], to the atom at index to_atoms[i]. The indices are simple + 0-based indexes into the columns of the _atom_site table in the input + mmCIF, and do not necessarily correspond to the values in _atom_site.id, + or any other column. + + Raises: + BondParsingError: If any of the required tables or columns are not present + in + the mmCIF, or if the _struct_conn table refers to atoms that cannot + be found in the _atom_site table. + """ + try: + return mmcif_struct_conn.get_bond_atom_indices(mmcif, model_id) + except ValueError as e: + raise BondParsingError(str(e)) from e + + +def get_or_infer_type_symbol( + mmcif: Mmcif, ccd: chemical_components.Ccd | None = None +) -> Sequence[str]: + """Returns the type symbol (element) for all of the atoms. + + Args: + mmcif: A parsed mmCIF file in the Mmcif format. + ccd: The chemical component dictionary. If not provided, defaults to the + cached CCD. + + If present, returns the _atom_site.type_symbol. If not, infers it using + _atom_site.label_comp_id (residue name), _atom_site.label_atom_id (atom name) + and the CCD. + """ + ccd = ccd or chemical_components.cached_ccd() + + def type_symbol_fn(res_name, atom_name): return chemical_components.type_symbol( + ccd, res_name, atom_name + ) + return mmcif_atom_site.get_or_infer_type_symbol(mmcif, type_symbol_fn) + + +def get_chain_type_by_entity_id(mmcif: Mmcif) -> Mapping[str, str]: + """Returns mapping from entity ID to its type or polymer type if available. + + If the entity is in the _entity_poly table, returns its polymer chain type. + If not, returns the type as specified in the _entity table. + + Args: + mmcif: CifDict holding the mmCIF. + """ + poly_entity_id = mmcif.get('_entity_poly.entity_id', []) + poly_type = mmcif.get('_entity_poly.type', []) + poly_type_by_entity_id = dict(zip(poly_entity_id, poly_type, strict=True)) + + chain_type_by_entity_id = {} + for entity_id, entity_type in zip( + mmcif.get('_entity.id', []), mmcif.get('_entity.type', []), strict=True + ): + chain_type = poly_type_by_entity_id.get(entity_id) or entity_type + chain_type_by_entity_id[entity_id] = chain_type + + return chain_type_by_entity_id + + +def get_internal_to_author_chain_id_map(mmcif: Mmcif) -> Mapping[str, str]: + """Returns a mapping from internal chain ID to the author chain ID. + + Note that this is not a bijection. One author chain ID can map to multiple + internal chain IDs. For example, a protein chain and a ligand bound to it will + share the same author chain ID, but they will each have a unique internal + chain ID). + + Args: + mmcif: CifDict holding the mmCIF. + """ + return mmcif_atom_site.get_internal_to_author_chain_id_map(mmcif) + + +def get_experimental_method(mmcif: Mmcif) -> str | None: + field = '_exptl.method' + return ','.join(mmcif[field]).lower() if field in mmcif else None + + +def get_release_date(mmcif: Mmcif) -> str | None: + """Returns the oldest revision date.""" + if '_pdbx_audit_revision_history.revision_date' not in mmcif: + return None + + # Release dates are ISO-8601, hence sort well. + return min(mmcif['_pdbx_audit_revision_history.revision_date']) + + +def get_resolution(mmcif: Mmcif) -> float | None: + """Returns the resolution of the structure. + + More than one resolution can be reported in an mmCIF. This function returns + the first one (in the order _refine.ls_d_res_high, + _em_3d_reconstruction.resolution, _reflns.d_resolution_high) that appears + in the mmCIF as is parseable as a float. + + Args: + mmcif: An `Mmcif` object. + + Returns: + The resolution as reported in the mmCIF. + """ + for res_key in ('_refine.ls_d_res_high', + '_em_3d_reconstruction.resolution', + '_reflns.d_resolution_high'): + if res_key in mmcif: + try: + raw_resolution = mmcif[res_key][0] + return float(raw_resolution) + except ValueError: + continue + return None + + +def parse_oper_expr(oper_expression: str) -> list[tuple[str, ...]]: + """Determines which transforms to apply based on an MMCIF oper_expression str. + + Args: + oper_expression: the field oper_expression from MMCIF format data. + Transform ids may be either numbers or single letters. Hyphens are used to + denote a numeric range of transforms to apply, and commas are used to + delimit a sequence of transforms. Where two sets of parentheses are + adjacent without a comma, the two sets of transforms should be combined as + a cartesian product, i.e. all possible pairs. + example 1,2,3 -> generate 3 copies of each chain by applying 1, 2 or 3. + example (1-3) -> generate 3 copies of each chain by applying 1, 2 or 3. + example (1-3)(4-6) -> generate 9 copies of each chain by applying one of + [(1,4), (1,5), (1,6), + (2,4), (2,5), (2,6), + (3,4), (3,5), (3,6)] + example (P) -> apply transform with id P. + + Raises: + ValueError: Failure to parse oper_expression. + + Returns: + A list with one element for each chain copy that should be generated. + Each element is a list of transform ids to apply. + """ + # Expand ranges, e.g. 1-4 -> 1,2,3,4. + def range_expander(match): + return ','.join( + [str(i) for i in range(int(match.group(1)), + int(match.group(2)) + 1)]) + + ranges_expanded = re.sub(r'\b(\d+)-(\d+)', range_expander, oper_expression) + + if re.fullmatch(r'(\w+,)*\w+', ranges_expanded): + # No brackets, just a single range, e.g. "1,2,3". + return [(t,) for t in ranges_expanded.split(',')] + elif re.fullmatch(r'\((\w+,)*\w+\)', ranges_expanded): + # Single range in brackets, e.g. "(1,2,3)". + return [(t,) for t in ranges_expanded[1:-1].split(',')] + elif re.fullmatch(r'\((\w+,)*\w+\)\((\w+,)*\w+\)', ranges_expanded): + # Cartesian product of two ranges, e.g. "(1,2,3)(4,5)". + part1, part2 = ranges_expanded[1:-1].split(')(') + return list(itertools.product(part1.split(','), part2.split(','))) + else: + raise ValueError( + f'Unsupported oper_expression format: {oper_expression}') + + +def format_float_array( + values: np.ndarray, num_decimal_places: int) -> Sequence[str]: + """Converts 1D array to a list of strings with the given number of decimals. + + This function is faster than converting via Python list comprehension, e.g.: + atoms_x = ['%.3f' % x for x in atoms_x] + + Args: + values: A numpy array with values to convert. This array is casted to + float32 before doing the conversion. + num_decimal_places: The number of decimal points to keep, including trailing + zeros. E.g. for 1.07 and num_decimal_places=1: 1.1, + num_decimal_places=2: 1.07, num_decimal_places=3: 1.070. + + Returns: + A list of formatted strings. + """ + if values.ndim != 1: + raise ValueError(f'The given array must be 1D, got {values.ndim}D') + + return string_array.format_float_array( + values=values.astype(np.float32), num_decimal_places=num_decimal_places + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py new file mode 100644 index 000000000..e449cbcf6 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py @@ -0,0 +1,1805 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Module for parsing various data sources and producing Structures.""" + +from collections.abc import Collection, Mapping, MutableMapping, Sequence +import dataclasses +import datetime +import enum +import functools +import itertools +from typing import TypeAlias +import numpy as np +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.cpp import mmcif_utils +from alphafold3.cpp import string_array +from alphafold3.structure import bioassemblies +from alphafold3.structure import bonds +from alphafold3.structure import chemical_components as struc_chem_comps +from alphafold3.structure import mmcif +from alphafold3.structure import structure +from alphafold3.structure import structure_tables + + +ChainIndex: TypeAlias = int +ResIndex: TypeAlias = int +AtomName: TypeAlias = str +BondAtomId: TypeAlias = tuple[ChainIndex, ResIndex, AtomName] + +_INSERTION_CODE_REMAP: Mapping[str, str] = {'.': '?'} + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class BondIndices: + from_indices: list[int] + dest_indices: list[int] + + +@enum.unique +class ModelID(enum.Enum): + """Values for specifying model IDs when parsing.""" + + FIRST = 1 # The first model in the file. + ALL = 2 # All models in the file. + + +@enum.unique +class SequenceFormat(enum.Enum): + """The possible formats for an input sequence.""" + + FASTA = 'fasta' # One-letter code used in FASTA. + # Multiple-letter chemical components dictionary ids. + CCD_CODES = 'ccd_codes' + LIGAND_SMILES = 'ligand_smiles' # SMILES string defining a molecule. + + +def _create_bond_lookup( + bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]], +) -> Mapping[tuple[ChainIndex, ResIndex], Mapping[AtomName, BondIndices]]: + """Creates maps to help find bonds during a loop over residues.""" + bond_lookup = {} + for bond_i, (from_atom_id, dest_atom_id) in enumerate(bonded_atom_pairs): + from_chain_i, from_res_i, from_atom_name = from_atom_id + dest_chain_i, dest_res_i, dest_atom_name = dest_atom_id + bonds_by_from_atom_name = bond_lookup.setdefault( + (from_chain_i, from_res_i), {} + ) + bonds_by_dest_atom_name = bond_lookup.setdefault( + (dest_chain_i, dest_res_i), {} + ) + bonds_by_from_atom_name.setdefault( + from_atom_name, BondIndices(from_indices=[], dest_indices=[]) + ).from_indices.append(bond_i) + bonds_by_dest_atom_name.setdefault( + dest_atom_name, BondIndices(from_indices=[], dest_indices=[]) + ).dest_indices.append(bond_i) + return bond_lookup + + +def _get_atom_element( + ccd: chemical_components.Ccd, res_name: str, atom_name: str +) -> str: + return ( + chemical_components.type_symbol( + ccd=ccd, res_name=res_name, atom_name=atom_name + ) + or '?' + ) + + +def _get_representative_atom( + ccd: chemical_components.Ccd, + res_name: str, + chain_type: str, + sequence_format: SequenceFormat, +) -> tuple[str, str]: + match sequence_format: + case SequenceFormat.CCD_CODES: + atom_name = _get_first_non_leaving_atom(ccd=ccd, res_name=res_name) + atom_element = _get_atom_element( + ccd=ccd, res_name=res_name, atom_name=atom_name + ) + return atom_name, atom_element + case SequenceFormat.LIGAND_SMILES: + return '', '?' + case SequenceFormat.FASTA: + if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: + return 'CA', 'C' + if chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: + return "C1'", 'C' + else: + raise ValueError(chain_type) + case _: + raise ValueError(sequence_format) + + +@functools.lru_cache(maxsize=128) +def _get_first_non_leaving_atom( + ccd: chemical_components.Ccd, res_name: str +) -> str: + """Returns first definitely non-leaving atom if exists, as a stand-in.""" + all_atoms = struc_chem_comps.get_all_atoms_in_entry(ccd, res_name=res_name)[ + '_chem_comp_atom.atom_id' + ] + representative_atom = all_atoms[0] + if representative_atom == 'O1' and len(all_atoms) > 1: + representative_atom = all_atoms[1] + return representative_atom + + +def _add_ligand_to_chem_comp( + chem_comp: MutableMapping[str, struc_chem_comps.ChemCompEntry], + ligand_id: str, + ligand_smiles: str, +): + """Adds a ligand to chemical components. Raises ValueError on mismatch.""" + new_entry = struc_chem_comps.ChemCompEntry( + type='non-polymer', pdbx_smiles=ligand_smiles + ) + + existing_entry = chem_comp.get(ligand_id) + if existing_entry is None: + chem_comp[ligand_id] = new_entry + elif existing_entry != new_entry: + raise ValueError( + f'Mismatching data for ligand {ligand_id}: ' + f'{new_entry} != {existing_entry}' + ) + + +def _get_first_model_id(cif: mmcif.Mmcif) -> str: + """Returns cheaply the first model ID from the mmCIF.""" + return cif.get_array( + '_atom_site.pdbx_PDB_model_num', dtype=object, gather=slice(1) + )[0] + + +def _get_str_model_id( + cif: mmcif.Mmcif, + model_id: ModelID | int, +) -> str: + """Converts a user-specified model_id argument into a string.""" + match model_id: + case int(): + str_model_id = str(model_id) + case enum.Enum(): + # We compare the enum's value attribute since regular enum comparison + # breaks when adhoc importing. + match model_id.value: + case ModelID.FIRST.value: + str_model_id = _get_first_model_id(cif) + case ModelID.ALL.value: + str_model_id = '' + case _: + raise ValueError( + f'Model ID {model_id} with value {model_id.value} not recognized.' + ) + case _: + raise ValueError( + f'Model ID {model_id} with type {type(model_id)} not recognized.' + ) + return str_model_id + + +def _parse_bonds( + cif: mmcif.Mmcif, + atom_key: np.ndarray, + model_id: str, +) -> bonds.Bonds: + """Returns the bonds table extracted from the mmCIF. + + Args: + cif: The raw mmCIF to extract the bond information from. + atom_key: A numpy array defining atom key for each atom in _atom_site. Note + that the atom key must be computed before resolving alt-locs since this + function operates on the raw mmCIF! + model_id: The ID of the model to get bonds for. + """ + if '_struct_conn.id' not in cif: + # This is the category key item for the _struct_conn table, therefore + # we use it to determine whether to parse bond info. + return bonds.Bonds.make_empty() + from_atom, dest_atom = mmcif.get_bond_atom_indices(cif, model_id) + from_atom = np.array(from_atom, dtype=np.int64) + dest_atom = np.array(dest_atom, dtype=np.int64) + num_bonds = from_atom.shape[0] + bond_key = np.arange(num_bonds, dtype=np.int64) + bond_type = cif.get_array('_struct_conn.conn_type_id', dtype=object) + if '_struct_conn.pdbx_role' in cif: # This column isn't always present. + bond_role = cif.get_array('_struct_conn.pdbx_role', dtype=object) + else: + bond_role = np.full((num_bonds,), '?', dtype=object) + + bonds_mask = np.ones((num_bonds,), dtype=bool) + # Symmetries other than 1_555 imply the atom is not part of the asymmetric + # unit, and therefore this is a bond that only exists in the expanded + # bioassembly. + # We do not currently support parsing these types of bonds. + if '_struct_conn.ptnr1_symmetry' in cif: + ptnr1_symmetry = cif.get_array( + '_struct_conn.ptnr1_symmetry', dtype=object) + np.logical_and(bonds_mask, ptnr1_symmetry == '1_555', out=bonds_mask) + if '_struct_conn.ptnr2_symmetry' in cif: + ptnr2_symmetry = cif.get_array( + '_struct_conn.ptnr2_symmetry', dtype=object) + np.logical_and(bonds_mask, ptnr2_symmetry == '1_555', out=bonds_mask) + # Remove bonds that involve atoms that are not part of the structure, + # e.g. waters if include_water=False. In a rare case this also removes invalid + # bonds that are indicated by a key that is set to _atom_site size. + np.logical_and(bonds_mask, np.isin(from_atom, atom_key), out=bonds_mask) + np.logical_and(bonds_mask, np.isin(dest_atom, atom_key), out=bonds_mask) + return bonds.Bonds( + key=bond_key[bonds_mask], + type=bond_type[bonds_mask], + role=bond_role[bonds_mask], + from_atom_key=from_atom[bonds_mask], + dest_atom_key=dest_atom[bonds_mask], + ) + + +@dataclasses.dataclass(frozen=True, slots=True) +class _MmcifHeader: + name: str + resolution: float | None + release_date: datetime.date | None + structure_method: str | None + bioassembly_data: bioassemblies.BioassemblyData | None + chemical_components_data: struc_chem_comps.ChemicalComponentsData | None + + +def _get_mmcif_header( + cif: mmcif.Mmcif, + fix_mse: bool, + fix_unknown_dna: bool, +) -> _MmcifHeader: + """Extract header fields from an mmCIF object.""" + name = cif.get_data_name() + resolution = mmcif.get_resolution(cif) + + release_date = mmcif.get_release_date(cif) + if release_date is not None: + release_date = datetime.date.fromisoformat(release_date) + + experiments = cif.get('_exptl.method') + structure_method = ','.join(experiments) if experiments else None + + try: + bioassembly_data = bioassemblies.BioassemblyData.from_mmcif(cif) + except bioassemblies.MissingBioassemblyDataError: + bioassembly_data = None + + try: + chemical_components_data = ( + struc_chem_comps.ChemicalComponentsData.from_mmcif( + cif, fix_mse=fix_mse, fix_unknown_dna=fix_unknown_dna + ) + ) + except struc_chem_comps.MissingChemicalComponentsDataError: + chemical_components_data = None + + return _MmcifHeader( + name=name, + resolution=resolution, + release_date=release_date, + structure_method=structure_method, + bioassembly_data=bioassembly_data, + chemical_components_data=chemical_components_data, + ) + + +def from_parsed_mmcif( + mmcif_object: mmcif.Mmcif, + *, + name: str | None = None, + fix_mse_residues: bool = False, + fix_arginines: bool = False, + fix_unknown_dna: bool = False, + include_water: bool = False, + include_other: bool = False, + include_bonds: bool = False, + model_id: int | ModelID = ModelID.FIRST, +) -> structure.Structure: + """Construct a Structure from a parsed mmCIF object. + + This function is called by `from_mmcif` but can be useful when an mmCIF has + already been parsed e.g. to extract extra information from the header before + then converting to Structure for further manipulation. + + Args: + mmcif_object: A parsed mmcif.Mmcif object. + name: Optional name for the structure. If not provided, the name will be + taken from the mmCIF data_ field. + fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine + (MSE) residues will be changed to sulphur atom sites (SD). This is because + methionine (MET) residues are often replaced with MSE to aid X-Ray + crystallography. If False, the SE MSE atom sites won't be modified. + fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so + that NH1 is always closer to CD than NH2. If False, no atom sites in + arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too. + fix_unknown_dna: If True, residues with name N in DNA chains will have their + res_name replaced with DN. Atoms are not changed. + include_water: If True, water (HOH) molecules will be parsed. Water + molecules may be grouped into chains, where number of residues > 1. Water + molecules are usually grouped into chains but do not necessarily all share + the same chain ID. + include_other: If True, all other atoms that are not included by any of the + above parameters will be included. This covers e.g. "polypeptide(D)" and + "macrolide" entities, as well as all other non-standard types. + include_bonds: If True, bond information will be parsed from the mmCIF and + stored in the Structure. + model_id: Either the integer model ID to parse, or one of ModelID.FIRST to + parse the first model, or ModelID.ALL to parse all models. + + Returns: + A Structure representation of the mmCIF object. + """ + str_model_id = _get_str_model_id(cif=mmcif_object, model_id=model_id) + header = _get_mmcif_header( + mmcif_object, fix_mse=fix_mse_residues, fix_unknown_dna=fix_unknown_dna + ) + + chains, residues, atoms = get_tables( + cif=mmcif_object, + fix_mse_residues=fix_mse_residues, + fix_arginines=fix_arginines, + fix_unknown_dna=fix_unknown_dna, + include_water=include_water, + include_other=include_other, + model_id=str_model_id, + ) + + if include_bonds: + # NB: parsing the atom table before the bonds table allows for a more + # informative error message when dealing with bad multi-model mmCIFs. + # We also ensure that we always use a specific model ID, even when parsing + # all models. + if str_model_id == '': # pylint: disable=g-explicit-bool-comparison + bonds_model_id = _get_first_model_id(mmcif_object) + else: + bonds_model_id = str_model_id + + bonds_table = _parse_bonds( + mmcif_object, + atom_key=atoms.key, + model_id=bonds_model_id, + ) + else: + bonds_table = bonds.Bonds.make_empty() + + return structure.Structure( + name=name if name is not None else header.name, + resolution=header.resolution, + release_date=header.release_date, + structure_method=header.structure_method, + bioassembly_data=header.bioassembly_data, + chemical_components_data=header.chemical_components_data, + bonds=bonds_table, + chains=chains, + residues=residues, + atoms=atoms, + ) + + +def from_mmcif( + mmcif_string: str | bytes, + *, + name: str | None = None, + fix_mse_residues: bool = False, + fix_arginines: bool = False, + fix_unknown_dna: bool = False, + include_water: bool = False, + include_other: bool = False, + include_bonds: bool = False, + model_id: int | ModelID = ModelID.FIRST, +) -> structure.Structure: + """Construct a Structure from a mmCIF string. + + Args: + mmcif_string: The string contents of an mmCIF file. + name: Optional name for the structure. If not provided, the name will be + taken from the mmCIF data_ field. + fix_mse_residues: If True, selenium atom sites (SE) in selenomethionine + (MSE) residues will be changed to sulphur atom sites (SD). This is because + methionine (MET) residues are often replaced with MSE to aid X-Ray + crystallography. If False, the SE MSE atom sites won't be modified. + fix_arginines: If True, NH1 and NH2 in arginine will be swapped if needed so + that NH1 is always closer to CD than NH2. If False, no atom sites in + arginine will be touched. Note that HH11, HH12, HH21, HH22 are fixed too. + fix_unknown_dna: If True, residues with name N in DNA chains will have their + res_name replaced with DN. Atoms are not changed. + include_water: If True, water (HOH) molecules will be parsed. Water + molecules may be grouped into chains, where number of residues > 1. Water + molecules are usually grouped into chains but do not necessarily all share + the same chain ID. + include_other: If True, all other atoms that are not included by any of the + above parameters will be included. This covers e.g. "polypeptide(D)" and + "macrolide" entities, as well as all other non-standard types. + include_bonds: If True, bond information will be parsed from the mmCIF and + stored in the Structure. + model_id: Either the integer model ID to parse, or one of ModelID.FIRST to + parse the first model, or ModelID.ALL to parse all models. + + Returns: + A Structure representation of the mmCIF string. + """ + mmcif_object = mmcif.from_string(mmcif_string) + + return from_parsed_mmcif( + mmcif_object, + name=name, + fix_mse_residues=fix_mse_residues, + fix_arginines=fix_arginines, + fix_unknown_dna=fix_unknown_dna, + include_water=include_water, + include_other=include_other, + include_bonds=include_bonds, + model_id=model_id, + ) + + +def from_res_arrays(atom_mask: np.ndarray, **kwargs) -> structure.Structure: + """Returns Structure created from from arrays with a residue dimension. + + All unset fields are filled with defaults (e.g. 1.0 for occupancy) or + unset/unknown values (e.g. UNK for residue type, or '.' for atom element). + + Args: + atom_mask: A array with shape (num_res, num_atom). This is used to decide + which atoms in the atom dimension are present in a given residue. Present + atoms should have a nonzero value, e.g. 1.0 or True. + **kwargs: A mapping from field name to values. For all array-valued fields + these arrays must have a dimension of length num_res. Chain and residue + fields should have this as their only dimension and atom fields should be + shaped (num_res, num_atom). Coordinate fields may also have arbitrary + leading dimensions (they must be the same across all coordinate fields). + See structure.{CHAIN,RESIDUE,ATOM}_FIELDS for a list of allowed fields. + """ + num_res, num_atom = atom_mask.shape + included_indices = np.flatnonzero(atom_mask) + + array_fields = ( + structure.CHAIN_FIELDS.keys() + | structure.RESIDUE_FIELDS.keys() + | structure.ATOM_FIELDS.keys() + ) + initializer_kwargs = {} + fields = {} + for k, val in kwargs.items(): + if k not in array_fields: + # The kwarg key isn't an array field name. Such kwargs are forwarded as-is + # to the constructor. They are expected to be global fields (e.g. name). + # Other values will raise an error when the constructor is called. + if k in structure.TABLE_FIELDS: + raise ValueError(f'Table fields must not be set. Got {k}.') + initializer_kwargs[k] = val + continue + elif val is None: + raise ValueError(f'{k} must be non-None.') + + if not isinstance(val, np.ndarray): + raise TypeError( + f'Value for {k} must be a NumPy array. Got {type(val)}.') + if k in structure.CHAIN_FIELDS or k in structure.RESIDUE_FIELDS: + if val.shape != (num_res,): + raise ValueError( + f'{k} must have shape ({num_res=},). Got {val.shape=}.' + ) + # Do not reshape the chain/residue arrays, they have the shape we need. + fields[k] = val + else: + assert k in structure.ATOM_FIELDS + if val.shape[-2:] != (num_res, num_atom): + raise ValueError( + f'{k} must have final two dimensions of length ' + f'{(num_res, num_atom)=}. Got {val.shape=}.' + ) + leading_dims = val.shape[:-2] + flat_val = val.reshape(leading_dims + (-1,), order='C') + masked_val = flat_val[..., included_indices] + fields[k] = masked_val + + # Get chain IDs or assume this is a single-chain structure. + chain_id = kwargs.get('chain_id', np.array(['A'] * num_res, dtype=object)) + # Find chain starts in res-sized arrays, use these to make chain-sized arrays. + chain_start = np.concatenate( + ([0], np.where(chain_id[1:] != chain_id[:-1])[0] + 1) + ) + if len(set(chain_id)) != len(chain_start): + raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}') + + chain_lengths = np.diff(chain_start, append=len(chain_id)) + chain_key = np.repeat(np.arange(len(chain_start)), chain_lengths) + + chain_entity_id = fields.get('chain_entity_id') + if chain_entity_id is not None: + entity_id = chain_entity_id[chain_entity_id] + else: + entity_id = np.array( + [str(mmcif.str_id_to_int_id(cid)) + for cid in chain_id[chain_start]], + dtype=object, + ) + chain_str_empty = np.full((num_res,), '.', dtype=object) + chains_table = structure_tables.Chains( + key=chain_key[chain_start], + id=chain_id[chain_start], + type=fields.get('chain_type', chain_str_empty)[chain_start], + auth_asym_id=fields.get('chain_auth_asym_id', chain_id)[chain_start], + entity_id=entity_id, + entity_desc=fields.get('chain_entity_desc', chain_str_empty)[ + chain_start], + ) + + # Since all arrays are residue-shaped, we can use them directly. + res_key = np.arange(num_res, dtype=np.int64) + res_id = fields.get('res_id', res_key + 1).astype(np.int32) + residues_table = structure_tables.Residues( + key=res_key, + chain_key=chain_key, + id=res_id, + name=fields.get('res_name', np.full(num_res, 'UNK', dtype=object)), + auth_seq_id=fields.get( + 'res_auth_seq_id', np.char.mod('%d', res_id).astype(object) + ), + insertion_code=fields.get( + 'res_insertion_code', np.full(num_res, '?', dtype=object) + ), + ) + + # The atom-sized arrays have already been masked and reshaped. + num_atoms_per_res = np.sum(atom_mask, axis=1, dtype=np.int32) + num_atoms_total = np.sum(num_atoms_per_res, dtype=np.int32) + # Structure is immutable, so use the same array multiple times to save RAM. + atom_str_empty = np.full(num_atoms_total, '.', dtype=object) + atom_float32_zeros = np.zeros(num_atoms_total, dtype=np.float32) + atom_float32_ones = np.ones(num_atoms_total, dtype=np.float32) + atoms_table = structure_tables.Atoms( + key=np.arange(num_atoms_total, dtype=np.int64), + chain_key=np.repeat(chain_key, num_atoms_per_res), + res_key=np.repeat(res_key, num_atoms_per_res), + name=fields.get('atom_name', atom_str_empty), + element=fields.get('atom_element', atom_str_empty), + x=fields.get('atom_x', atom_float32_zeros), + y=fields.get('atom_y', atom_float32_zeros), + z=fields.get('atom_z', atom_float32_zeros), + b_factor=fields.get('atom_b_factor', atom_float32_zeros), + occupancy=fields.get('atom_occupancy', atom_float32_ones), + ) + + return structure.Structure( + chains=chains_table, + residues=residues_table, + atoms=atoms_table, + bonds=structure_tables.Bonds.make_empty(), # Currently not set. + **initializer_kwargs, + ) + + +def expand_sequence( + sequence: str, chain_type: str, sequence_format: SequenceFormat +) -> Sequence[str]: + """Returns full residue names based on a sequence string. + + Args: + sequence: A string representing the sequence. + chain_type: The chain type of the sequence. + sequence_format: The format of the sequence argument. + """ + match sequence_format: + case SequenceFormat.FASTA: + if not all(c.isalpha() for c in sequence): + raise ValueError( + f'Sequence "{sequence}" has non-alphabetic characters') + match chain_type: + case mmcif_names.PROTEIN_CHAIN: + res_name_map = residue_names.PROTEIN_COMMON_ONE_TO_THREE + default_res_name = residue_names.UNK + case mmcif_names.RNA_CHAIN: + res_name_map = {r: r for r in residue_names.RNA_TYPES} + default_res_name = residue_names.UNK_RNA + case mmcif_names.DNA_CHAIN: + res_name_map = residue_names.DNA_COMMON_ONE_TO_TWO + default_res_name = residue_names.UNK_DNA + case _: + raise ValueError( + f'{chain_type=} not supported for FASTA format.') + return [ + res_name_map.get(one_letter_res, default_res_name) + for one_letter_res in sequence + ] + case SequenceFormat.CCD_CODES: + return sequence.strip('()').split(')(') + case SequenceFormat.LIGAND_SMILES: + ligand_id, _ = sequence.split(':', maxsplit=1) + return [ligand_id] + + +def from_sequences_and_bonds( + sequences: Sequence[str], + chain_types: Sequence[str], + sequence_formats: Sequence[SequenceFormat], + bonded_atom_pairs: Sequence[tuple[BondAtomId, BondAtomId]] | None, + ccd: chemical_components.Ccd, + name: str = 'from_sequences_and_bonds', + bond_type: str | None = None, + **constructor_args, +) -> structure.Structure: + """Returns a minimal structure for the input sequences and bonds. + + The returned structure will have at least one atom per residue. If the + residue has any bonded atoms, according to `bonded_atom_pairs`, then + all (and only) those atoms will be present for that residue. If the residue + is not involved in any bond then an arbitrary atom will be created. + + Args: + sequences: A sequence of strings, each one representing a single chain. + chain_types: The types of each chain, e.g. polypeptide(L). The n-th element + describes the n-th sequence in `sequences`. + sequence_formats: The format of each sequence. The n-th element describes + the n-th sequence in `sequences`. + bonded_atom_pairs: A sequence of bonded atom pairs. Each atom is described + as a tuple of (chain_index, res_index, atom_name), where the first two + values are 0-based indices. The chain_index is the index of the chain in + the `sequences` argument, and the res_index is the index of the residue in + that sequence. The atom_name is the name of the atom in the residue, e.g. + CA. If the atom is not found in the standard atoms for that residue + (according to the CCD) then an error is raised. + ccd: The chemical components dictionary. + name: A name for the returned structure. + bond_type: This type will be used for all bonds in the structure, where type + follows PDB scheme, e.g. unknown (?), hydrog, metalc, covale, disulf. + **constructor_args: These arguments are passed directly to the + structure.Structure constructor. + """ + chain_id = [] + chain_type = [] + chain_res_count = [] + res_id = [] + res_name = [] + res_atom_count = [] + atom_name = [] + atom_element = [] + chem_comp = {} + + num_bonds = len(bonded_atom_pairs or ()) + from_atom_key = np.full((num_bonds,), -1, dtype=np.int64) + dest_atom_key = np.full((num_bonds,), -1, dtype=np.int64) + + # Create map (chain_i, res_i) -> {atom_name -> (from_idxs dest_idxs)}. + # This allows quick lookup of whether a residue has any bonded atoms, and + # which bonds those atoms participate in. + bond_lookup = _create_bond_lookup(bonded_atom_pairs or ()) + + current_atom_key = 0 + for chain_i, (sequence, curr_chain_type, sequence_format) in enumerate( + zip(sequences, chain_types, sequence_formats, strict=True) + ): + current_chain_id = mmcif.int_id_to_str_id(chain_i + 1) + num_chain_residues = 0 + for res_i, full_res_name in enumerate( + expand_sequence(sequence, curr_chain_type, sequence_format) + ): + current_res_id = res_i + 1 + num_res_atoms = 0 + + # Look for bonded atoms in the bond lookup and if any are found, add + # their atom keys to the bond atom_key columns. + if bond_indices_by_atom_name := bond_lookup.get((chain_i, res_i)): + for bond_atom_name, bond_indices in bond_indices_by_atom_name.items(): + atom_name.append(bond_atom_name) + atom_element.append( + _get_atom_element( + ccd=ccd, res_name=full_res_name, atom_name=bond_atom_name + ) + ) + for from_bond_i in bond_indices.from_indices: + from_atom_key[from_bond_i] = current_atom_key + for dest_bond_i in bond_indices.dest_indices: + dest_atom_key[dest_bond_i] = current_atom_key + current_atom_key += 1 + num_res_atoms += 1 + else: + # If this residue has no bonded atoms then we need to add one atom + # like in from_sequences. + assert num_res_atoms == 0 + rep_atom_name, rep_atom_element = _get_representative_atom( + ccd=ccd, + res_name=full_res_name, + chain_type=curr_chain_type, + sequence_format=sequence_format, + ) + atom_name.append(rep_atom_name) + atom_element.append(rep_atom_element) + num_res_atoms += 1 + current_atom_key += 1 + + if sequence_format == SequenceFormat.LIGAND_SMILES: + # Sequence expect to be in the format :, + # which always corresponds to a single-residue chain. + ligand_id, ligand_smiles = sequence.split(':', maxsplit=1) + if ccd.get(ligand_id) is not None: + raise ValueError( + f'Ligand name {ligand_id} is in CCD - it is not supported to give' + ' ligands created from SMILES the same name as CCD components.' + ) + # We need to provide additional chemical components metadata for + # ligands specified via SMILES strings since they might not be in CCD. + _add_ligand_to_chem_comp(chem_comp, ligand_id, ligand_smiles) + + assert num_res_atoms >= 1 + res_atom_count.append(num_res_atoms) + num_chain_residues += 1 + res_id.append(current_res_id) + res_name.append(full_res_name) + + chain_id.append(current_chain_id) + chain_type.append(curr_chain_type) + chain_res_count.append(num_chain_residues) + + chem_comp_data = struc_chem_comps.ChemicalComponentsData(chem_comp) + chem_comp_data = struc_chem_comps.populate_missing_ccd_data( + ccd=ccd, + chemical_components_data=chem_comp_data, + chemical_component_ids=set(res_name), + ) + + if bonded_atom_pairs is not None: + unknown_bond_col = np.full((num_bonds,), '?', dtype=object) + if bond_type is None: + bond_type_col = unknown_bond_col + else: + bond_type_col = np.full((num_bonds,), bond_type, dtype=object) + bonds_table = bonds.Bonds( + key=np.arange(num_bonds, dtype=np.int64), + type=bond_type_col, + role=unknown_bond_col, + from_atom_key=from_atom_key, + dest_atom_key=dest_atom_key, + ) + else: + bonds_table = structure_tables.Bonds.make_empty() + + # 1 chain per sequence. + chain_key = np.arange(len(sequences), dtype=np.int64) + chain_id = np.array(chain_id, dtype=object) + chains_table = structure_tables.Chains( + key=chain_key, + id=chain_id, + type=np.array(chain_type, dtype=object), + auth_asym_id=chain_id, + entity_id=np.char.mod('%d', chain_key + 1).astype(object), + entity_desc=np.array(['.'] * len(chain_key), dtype=object), + ) + + res_key = np.arange(len(res_name), dtype=np.int64) + res_chain_key = np.repeat(chain_key, chain_res_count) + residues_table = structure_tables.Residues( + key=res_key, + chain_key=res_chain_key, + id=np.array(res_id, dtype=np.int32), + name=np.array(res_name, dtype=object), + auth_seq_id=np.char.mod('%d', res_id).astype(object), + insertion_code=np.full(len(res_name), '?', dtype=object), + ) + + num_atoms = current_atom_key + atom_float32_zeros = np.zeros(num_atoms, dtype=np.float32) + atoms_table = structure_tables.Atoms( + key=np.arange(num_atoms, dtype=np.int64), + chain_key=np.repeat(res_chain_key, res_atom_count), + res_key=np.repeat(res_key, res_atom_count), + name=np.array(atom_name, dtype=object), + element=np.array(atom_element, dtype=object), + x=atom_float32_zeros, + y=atom_float32_zeros, + z=atom_float32_zeros, + b_factor=atom_float32_zeros, + occupancy=np.ones(num_atoms, np.float32), + ) + + return structure.Structure( + name=name, + atoms=atoms_table, + residues=residues_table, + chains=chains_table, + bonds=bonds_table, + chemical_components_data=chem_comp_data, + **constructor_args, + ) + + +class _ChainResBuilder: + """Class for incrementally building chain and residue tables.""" + + def __init__( + self, + *, + chain_key_by_chain_id: Mapping[str, int], + entity_id_by_chain_id: Mapping[str, str], + chain_type_by_entity_id: Mapping[str, str], + entity_desc_by_entity_id: Mapping[str, str], + fix_mse_residues: bool, + fix_unknown_dna: bool, + ): + # Len: num_chains. + self.chain_key = [] + self.chain_id = [] + self.chain_type = [] + self.chain_auth_asym_id = [] + self.chain_entity_id = [] + self.chain_entity_desc = [] + + # Len: num_residues. + self.res_key = [] + self.res_chain_key = [] + self.res_id = [] + self.res_name = [] + self.res_auth_seq_id = [] + self.res_insertion_code = [] + + self.chain_key_by_chain_id = chain_key_by_chain_id + self.entity_id_by_chain_id = entity_id_by_chain_id + self.chain_type_by_entity_id = chain_type_by_entity_id + self.entity_desc_by_entity_id = entity_desc_by_entity_id + self.key_for_res: dict[tuple[str, str, str, str], int] = {} + + self._fix_mse_residues = fix_mse_residues + self._fix_unknown_dna = fix_unknown_dna + + def add_residues( + self, + *, + chain_ids: np.ndarray, + chain_auth_asym_ids: np.ndarray, + res_ids: np.ndarray, + res_names: np.ndarray, + res_auth_seq_ids: np.ndarray, + res_ins_codes: np.ndarray, + ): + """Adds a residue (and its chain) to the tables.""" + # Create chain table data. + if chain_ids.size == 0: + return + + chain_ids_with_prev = np.concatenate( + (([self.chain_id[-1] if self.chain_id else None], chain_ids)) + ) + chain_change_mask = chain_ids_with_prev[:-1] != chain_ids_with_prev[1:] + chain_change_ids = chain_ids[chain_change_mask] + chain_keys = string_array.remap( + chain_change_ids, self.chain_key_by_chain_id, inplace=False + ) + self.chain_key.extend(chain_keys) + self.chain_id.extend(chain_change_ids) + self.chain_auth_asym_id.extend(chain_auth_asym_ids[chain_change_mask]) + chain_entity_id = string_array.remap( + chain_change_ids, self.entity_id_by_chain_id, inplace=False + ) + self.chain_entity_id.extend(chain_entity_id) + chain_type = string_array.remap( + chain_entity_id, self.chain_type_by_entity_id, inplace=False + ) + self.chain_type.extend(chain_type) + chain_entity_desc = string_array.remap( + chain_entity_id, self.entity_desc_by_entity_id, inplace=False + ) + self.chain_entity_desc.extend(chain_entity_desc) + + # Create residue table data. + num_prev_res = len(self.res_id) + res_keys = np.arange(num_prev_res, num_prev_res + len(res_ids)) + res_iter = zip( + chain_ids, + res_auth_seq_ids, + res_names, + res_ins_codes, + strict=True, + ) + key_for_res_update = { + res_unique_id: res_key + for res_key, res_unique_id in enumerate(res_iter, num_prev_res) + } + self.key_for_res.update(key_for_res_update) + self.res_key.extend(res_keys) + self.res_chain_key.extend( + string_array.remap( + chain_ids, self.chain_key_by_chain_id, inplace=False) + ) + self.res_id.extend(res_ids) + self.res_name.extend(res_names) + self.res_auth_seq_id.extend(res_auth_seq_ids) + self.res_insertion_code.extend(res_ins_codes) + + def make_chains_table(self) -> structure_tables.Chains: + """Returns the Structure chains table.""" + chain_key = np.array(self.chain_key, dtype=np.int64) + if not np.all(chain_key[:-1] <= chain_key[1:]): + # If the order is inconsistent with the atoms table, sort so that it is. + order = np.argsort(self.chain_key, kind='stable') + return structure_tables.Chains( + key=chain_key[order], + id=np.array(self.chain_id, dtype=object)[order], + type=np.array(self.chain_type, dtype=object)[order], + auth_asym_id=np.array( + self.chain_auth_asym_id, dtype=object)[order], + entity_id=np.array(self.chain_entity_id, dtype=object)[order], + entity_desc=np.array( + self.chain_entity_desc, dtype=object)[order], + ) + return structure_tables.Chains( + key=chain_key, + id=np.array(self.chain_id, dtype=object), + type=np.array(self.chain_type, dtype=object), + auth_asym_id=np.array(self.chain_auth_asym_id, dtype=object), + entity_id=np.array(self.chain_entity_id, dtype=object), + entity_desc=np.array(self.chain_entity_desc, dtype=object), + ) + + def make_residues_table(self) -> structure_tables.Residues: + """Returns the Structure residues table.""" + res_name = np.array(self.res_name, dtype=object) + res_chain_key = np.array(self.res_chain_key, dtype=np.int64) + + if self._fix_mse_residues: + string_array.remap(res_name, mapping={'MSE': 'MET'}, inplace=True) + + if self._fix_unknown_dna: + # Remap residues from N -> DN in DNA chains only. + dna_chain_mask = ( + np.array(self.chain_type, dtype=object) == mmcif_names.DNA_CHAIN + ) + dna_chain_key = np.array(self.chain_key, dtype=object)[ + dna_chain_mask] + res_name[(res_name == 'N') & np.isin( + res_chain_key, dna_chain_key)] = 'DN' + + if not np.all(res_chain_key[:-1] <= res_chain_key[1:]): + # If the order is inconsistent with the atoms table, sort so that it is. + order = np.argsort(res_chain_key, kind='stable') + return structure_tables.Residues( + key=np.array(self.res_key, dtype=np.int64)[order], + chain_key=res_chain_key[order], + id=np.array(self.res_id, dtype=np.int32)[order], + name=res_name[order], + auth_seq_id=np.array(self.res_auth_seq_id, + dtype=object)[order], + insertion_code=np.array( + self.res_insertion_code, dtype=object)[order], + ) + return structure_tables.Residues( + key=np.array(self.res_key, dtype=np.int64), + chain_key=res_chain_key, + id=np.array(self.res_id, dtype=np.int32), + name=res_name, + auth_seq_id=np.array(self.res_auth_seq_id, dtype=object), + insertion_code=np.array(self.res_insertion_code, dtype=object), + ) + + +def _get_string_array_default(cif: mmcif.Mmcif, key: str, default: list[str]): + try: + return cif.get_array(key, dtype=object) + except KeyError: + return default + + +def _generate_required_tables_if_missing( + cif: mmcif.Mmcif, +) -> Mapping[str, Sequence[str]]: + """Generates all required tables and columns if missing.""" + update = {} + + atom_site_entities = _get_string_array_default( + cif, '_atom_site.label_entity_id', [] + ) + + # OpenMM produces files that don't have any of the tables and also have + # _atom_site.label_entity_id set to '?' for all atoms. We infer the entities + # based on the _atom_site.label_asym_id column. We start with cheaper O(1) + # checks to prevent running the expensive O(n) check on most files. + if ( + len(atom_site_entities) > 0 # pylint: disable=g-explicit-length-test + and '_entity.id' not in cif # Ignore if the _entity table exists. + and atom_site_entities[0] == '?' # Cheap check. + and set(atom_site_entities) == {'?'} # Expensive check. + ): + label_asym_ids = cif.get_array( + '_atom_site.label_asym_id', dtype=object) + atom_site_entities = [ + str(mmcif.str_id_to_int_id(cid)) for cid in label_asym_ids + ] + # Update _atom_site.label_entity_id to be consistent with the new tables. + update['_atom_site.label_entity_id'] = atom_site_entities + + # Check table existence by checking the presence of its primary key. + if '_struct_asym.id' not in cif: + # Infer the _struct_asym table using the _atom_site table. + asym_ids = _get_string_array_default( + cif, '_atom_site.label_asym_id', []) + + if len(atom_site_entities) == 0 or len(asym_ids) == 0: # pylint: disable=g-explicit-length-test + raise ValueError( + 'Could not parse an mmCIF with no _struct_asym table and also no ' + '_atom_site.label_entity_id or _atom_site.label_asym_id columns.' + ) + + # Deduplicate, but keep the order intact - dict.fromkeys maintains order. + entity_id_chain_id_pairs = list( + dict.fromkeys(zip(atom_site_entities, asym_ids, strict=True)) + ) + update['_struct_asym.entity_id'] = [ + e for e, _ in entity_id_chain_id_pairs] + update['_struct_asym.id'] = [c for _, c in entity_id_chain_id_pairs] + + if '_entity.id' not in cif: + # Infer the _entity_poly and _entity tables using the _atom_site table. + residues = _get_string_array_default( + cif, '_atom_site.label_comp_id', []) + group_pdb = _get_string_array_default(cif, '_atom_site.group_PDB', []) + if '_atom_site.label_entity_id' in cif: + entities = atom_site_entities + else: + # If _atom_site.label_entity_id not set, use the asym_id -> entity_id map. + asym_to_entity = dict( + zip( + cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True + ) + ) + entities = string_array.remap( + cif.get_array('_atom_site.label_asym_id', dtype=object), + mapping=asym_to_entity, + ) + + entity_ids = [] + entity_types = [] + entity_poly_entity_ids = [] + entity_poly_types = [] + entity_poly_table_missing = '_entity_poly.entity_id' not in cif + for entity_id, group in itertools.groupby( + zip(entities, residues, group_pdb, strict=True), key=lambda e: e[0] + ): + _, entity_residues, entity_group_pdb = zip(*group, strict=True) + entity_type = _guess_entity_type( + chain_residues=entity_residues, atom_types=entity_group_pdb + ) + entity_ids.append(entity_id) + entity_types.append(entity_type) + + if entity_poly_table_missing and entity_type == mmcif_names.POLYMER_CHAIN: + polymer_type = mmcif_names.guess_polymer_type(entity_residues) + entity_poly_entity_ids.append(entity_id) + entity_poly_types.append(polymer_type) + + update['_entity.id'] = entity_ids + update['_entity.type'] = entity_types + if entity_poly_table_missing: + update['_entity_poly.entity_id'] = entity_poly_entity_ids + update['_entity_poly.type'] = entity_poly_types + + if '_atom_site.type_symbol' not in cif: + update['_atom_site.type_symbol'] = mmcif.get_or_infer_type_symbol(cif) + + return update + + +def _maybe_add_missing_scheme_tables( + cif: mmcif.Mmcif, + res_starts: Sequence[int], + label_asym_ids: np.ndarray, + label_seq_ids: np.ndarray, + label_comp_ids: np.ndarray, + auth_seq_ids: np.ndarray, + pdb_ins_codes: np.ndarray, +) -> Mapping[str, Sequence[str]]: + """If missing, infers the scheme tables from the _atom_site table.""" + update = {} + + required_poly_seq_scheme_cols = ( + '_pdbx_poly_seq_scheme.asym_id', + '_pdbx_poly_seq_scheme.pdb_seq_num', + '_pdbx_poly_seq_scheme.pdb_ins_code', + '_pdbx_poly_seq_scheme.seq_id', + '_pdbx_poly_seq_scheme.mon_id', + '_pdbx_poly_seq_scheme.pdb_strand_id', + ) + if not all(col in cif for col in required_poly_seq_scheme_cols): + # Create a mask for atoms where each polymer residue start. + entity_id_by_chain_id = dict( + zip(cif['_struct_asym.id'], + cif['_struct_asym.entity_id'], strict=True) + ) + chain_type_by_entity_id = dict( + zip(cif['_entity.id'], cif['_entity.type'], strict=True) + ) + # Remap asym ID -> entity ID. + chain_type = string_array.remap( + label_asym_ids, mapping=entity_id_by_chain_id, inplace=False + ) + # Remap entity ID -> chain type. + string_array.remap( + chain_type, mapping=chain_type_by_entity_id, inplace=True + ) + res_mask = np.zeros_like(label_seq_ids, dtype=bool) + res_mask[res_starts] = True + res_mask &= chain_type == mmcif_names.POLYMER_CHAIN + + entity_poly_seq_cols = ( + '_entity_poly_seq.entity_id', + '_entity_poly_seq.num', + '_entity_poly_seq.mon_id', + ) + if all(col in cif for col in entity_poly_seq_cols): + # Use _entity_poly_seq if available. + poly_seq_num = cif.get_array('_entity_poly_seq.num', dtype=object) + poly_seq_mon_id = cif.get_array( + '_entity_poly_seq.mon_id', dtype=object) + poly_seq_entity_id = cif.get_array( + '_entity_poly_seq.entity_id', dtype=object + ) + label_seq_id_to_auth_seq_id = dict( + zip(label_seq_ids[res_mask], + auth_seq_ids[res_mask], strict=True) + ) + scheme_pdb_seq_num = string_array.remap( + poly_seq_num, mapping=label_seq_id_to_auth_seq_id, default_value='.' + ) + label_seq_id_to_ins_code = dict( + zip(label_seq_ids[res_mask], + pdb_ins_codes[res_mask], strict=True) + ) + scheme_pdb_ins_code = string_array.remap( + poly_seq_num, mapping=label_seq_id_to_ins_code, default_value='.' + ) + + # The _entity_poly_seq table is entity-based, while _pdbx_poly_seq_scheme + # is chain-based. A single entity could mean multiple chains (asym_ids), + # we therefore need to replicate each entity for all of the chains. + scheme_asym_id = [] + select = [] + indices = np.arange(len(poly_seq_entity_id), dtype=np.int32) + for asym_id, entity_id in zip( + cif['_struct_asym.id'], cif['_struct_asym.entity_id'], strict=True + ): + entity_mask = poly_seq_entity_id == entity_id + select.extend(indices[entity_mask]) + scheme_asym_id.extend([asym_id] * sum(entity_mask)) + + scheme_pdb_strand_id = string_array.remap( + np.array(scheme_asym_id, dtype=object), + mapping=mmcif.get_internal_to_author_chain_id_map(cif), + inplace=False, + ) + + update['_pdbx_poly_seq_scheme.asym_id'] = scheme_asym_id + update['_pdbx_poly_seq_scheme.pdb_strand_id'] = scheme_pdb_strand_id + update['_pdbx_poly_seq_scheme.pdb_seq_num'] = scheme_pdb_seq_num[select] + update['_pdbx_poly_seq_scheme.pdb_ins_code'] = scheme_pdb_ins_code[select] + update['_pdbx_poly_seq_scheme.seq_id'] = poly_seq_num[select] + update['_pdbx_poly_seq_scheme.mon_id'] = poly_seq_mon_id[select] + else: + # _entity_poly_seq not available, fallback to _atom_site. + res_asym_ids = label_asym_ids[res_mask] + res_strand_ids = string_array.remap( + array=res_asym_ids, + mapping=mmcif.get_internal_to_author_chain_id_map(cif), + inplace=False, + ) + update['_pdbx_poly_seq_scheme.asym_id'] = res_asym_ids + update['_pdbx_poly_seq_scheme.pdb_seq_num'] = auth_seq_ids[res_mask] + update['_pdbx_poly_seq_scheme.pdb_ins_code'] = pdb_ins_codes[res_mask] + update['_pdbx_poly_seq_scheme.seq_id'] = label_seq_ids[res_mask] + update['_pdbx_poly_seq_scheme.mon_id'] = label_comp_ids[res_mask] + update['_pdbx_poly_seq_scheme.pdb_strand_id'] = res_strand_ids + + required_nonpoly_scheme_cols = ( + '_pdbx_nonpoly_scheme.mon_id', + '_pdbx_nonpoly_scheme.asym_id', + '_pdbx_nonpoly_scheme.pdb_seq_num', + '_pdbx_nonpoly_scheme.pdb_ins_code', + ) + required_branch_scheme_cols = ( + '_pdbx_branch_scheme.mon_id', + '_pdbx_branch_scheme.asym_id', + '_pdbx_branch_scheme.pdb_seq_num', + ) + + # Generate _pdbx_nonpoly_scheme only if both tables are missing. + if not ( + all(col in cif for col in required_nonpoly_scheme_cols) + or all(col in cif for col in required_branch_scheme_cols) + ): + # To be strictly semantically correct, multi-residue ligands should be + # written in _pdbx_branch_scheme. However, Structure parsing handles + # correctly multi-residue ligands in _pdbx_nonpoly_scheme and the tables + # constructed here live only while parsing, hence this is unnecessary. + entity_id_by_chain_id = dict( + zip(cif['_struct_asym.id'], + cif['_struct_asym.entity_id'], strict=True) + ) + chain_type_by_entity_id = dict( + zip(cif['_entity.id'], cif['_entity.type'], strict=True) + ) + # Remap asym ID -> entity ID. + chain_type = string_array.remap( + label_asym_ids, mapping=entity_id_by_chain_id, inplace=False + ) + # Remap entity ID -> chain type. + string_array.remap( + chain_type, mapping=chain_type_by_entity_id, inplace=True + ) + res_mask = np.zeros_like(label_seq_ids, dtype=bool) + res_mask[res_starts] = True + res_mask &= chain_type != mmcif_names.POLYMER_CHAIN + + if not np.any(res_mask): + return update # Shortcut: no non-polymer residues. + + ins_codes = string_array.remap( + pdb_ins_codes[res_mask], mapping={'?': '.'}, inplace=False + ) + + update['_pdbx_nonpoly_scheme.asym_id'] = label_asym_ids[res_mask] + update['_pdbx_nonpoly_scheme.pdb_seq_num'] = auth_seq_ids[res_mask] + update['_pdbx_nonpoly_scheme.pdb_ins_code'] = ins_codes + update['_pdbx_nonpoly_scheme.mon_id'] = label_comp_ids[res_mask] + + return update + + +def _get_chain_key_by_chain_id( + resolved_chain_ids: np.ndarray, struct_asym_chain_ids: np.ndarray +) -> Mapping[str, int]: + """Returns chain key for each chain ID respecting resolved chain ordering.""" + # Check that all chain IDs found in the (potentially filtered) _atom_site + # table are present in the _struct_asym table. + unique_resolved_chain_ids = set(resolved_chain_ids) + if not unique_resolved_chain_ids.issubset(set(struct_asym_chain_ids)): + unique_resolved_chain_ids = sorted(unique_resolved_chain_ids) + unique_struct_asym_chain_ids = sorted(set(struct_asym_chain_ids)) + raise ValueError( + 'Bad mmCIF: chain IDs in _atom_site.label_asym_id ' + f'{unique_resolved_chain_ids} is not a subset of chain IDs in ' + f'_struct_asym.id {unique_struct_asym_chain_ids}.' + ) + + resolved_mask = string_array.isin( + struct_asym_chain_ids, unique_resolved_chain_ids + ) + # For all resolved chains, use the _atom_site order they appear in. E.g. + # resolved_chain_ids = [B A E D F] + # struct_asym_chain_ids = [A B C D E F] + # consistent_chain_order = [B A C E D F] + # chain_keys = [0 1 2 3 4 5] + consistent_chain_order = struct_asym_chain_ids.copy() + consistent_chain_order[resolved_mask] = resolved_chain_ids + return dict(zip(consistent_chain_order, range(len(struct_asym_chain_ids)))) + + +def get_tables( + cif: mmcif.Mmcif, + fix_mse_residues: bool, + fix_arginines: bool, + fix_unknown_dna: bool, + include_water: bool, + include_other: bool, + model_id: str, +) -> tuple[ + structure_tables.Chains, structure_tables.Residues, structure_tables.Atoms +]: + """Returns chain, residue, and atom tables from a parsed mmcif. + + Args: + cif: A parsed mmcif.Mmcif. + fix_mse_residues: See from_mmcif. + fix_arginines: See from_mmcif. + fix_unknown_dna: See from_mmcif. + include_water: See from_mmcif. + include_other: See from_mmcif. + model_id: A string defining which model ID to use. If set, only coordinates, + b-factors and occupancies for the given model are returned. If empty, + coordinates, b-factors and occupanciesall for models are returned with a + leading dimension of num_models. Note that the model_id argument in + from_mmcif is an integer and has slightly different use (see from_mmcif). + """ + # Add any missing tables and columns we require for parsing. + if cif_update := _generate_required_tables_if_missing(cif): + cif = cif.copy_and_update(cif_update) + + # Resolve alt-locs, selecting only a single option for each residue. Also + # computes the layout, which defines where chain and residue boundaries are. + atom_site_all_models, layout = mmcif_utils.filter( + cif, + include_nucleotides=True, + include_ligands=True, + include_water=include_water, + include_other=include_other, + model_id=model_id, + ) + atom_site_first_model = atom_site_all_models[0] + + # Get atom information from the _atom_site table. + def _first_model_string_array(col: str) -> np.ndarray: + return cif.get_array(col, dtype=object, gather=atom_site_first_model) + + def _requested_models_float_array(col: str) -> np.ndarray: + if not model_id: + # Return data for all models with a leading dimension of num_models. + return cif.get_array(col, dtype=np.float32, gather=atom_site_all_models) + else: + # Return data only for the single requested model. + return cif.get_array(col, dtype=np.float32, gather=atom_site_first_model) + + # These columns are the same for all models, fetch them just for the 1st one. + label_comp_ids = _first_model_string_array('_atom_site.label_comp_id') + label_asym_ids = _first_model_string_array('_atom_site.label_asym_id') + label_seq_ids = _first_model_string_array('_atom_site.label_seq_id') + label_atom_ids = _first_model_string_array('_atom_site.label_atom_id') + if '_atom_site.auth_seq_id' in cif: + auth_seq_ids = _first_model_string_array('_atom_site.auth_seq_id') + else: + # auth_seq_id unset, fallback to label_seq_id. + auth_seq_ids = label_seq_ids + type_symbols = _first_model_string_array('_atom_site.type_symbol') + pdbx_pdb_ins_codes = _first_model_string_array( + '_atom_site.pdbx_PDB_ins_code') + + # These columns are different for all models, fetch them as requested. + atom_x = _requested_models_float_array('_atom_site.Cartn_x') + atom_y = _requested_models_float_array('_atom_site.Cartn_y') + atom_z = _requested_models_float_array('_atom_site.Cartn_z') + atom_b_factor = _requested_models_float_array('_atom_site.B_iso_or_equiv') + atom_occupancy = _requested_models_float_array('_atom_site.occupancy') + + # Make sure the scheme (residue) tables exist in case they are not present. + if cif_update := _maybe_add_missing_scheme_tables( + cif, + res_starts=layout.residue_starts(), + label_asym_ids=label_asym_ids, + label_seq_ids=label_seq_ids, + label_comp_ids=label_comp_ids, + auth_seq_ids=auth_seq_ids, + pdb_ins_codes=pdbx_pdb_ins_codes, + ): + cif = cif.copy_and_update(cif_update) + + # Fix common issues found in mmCIF files, like swapped arginine NH atoms. + mmcif_utils.fix_residues( + layout, + comp_id=label_comp_ids, + atom_id=label_atom_ids, + atom_x=atom_x[0] if not model_id else atom_x, + atom_y=atom_y[0] if not model_id else atom_y, + atom_z=atom_z[0] if not model_id else atom_z, + fix_arg=fix_arginines, + ) + + # Get keys for chains in the order they appear in _atom_site while also + # dealing with empty chains. + resolved_chain_ids = label_asym_ids[layout.chain_starts()] + struct_asym_chain_ids = cif.get_array('_struct_asym.id', dtype=object) + + chain_key_by_chain_id = _get_chain_key_by_chain_id( + resolved_chain_ids=resolved_chain_ids, + struct_asym_chain_ids=struct_asym_chain_ids, + ) + entity_id_by_chain_id = dict( + zip(struct_asym_chain_ids, cif['_struct_asym.entity_id'], strict=True) + ) + entity_description = cif.get( + '_entity.pdbx_description', ['?'] * len(cif['_entity.id']) + ) + entity_desc_by_entity_id = dict( + zip(cif['_entity.id'], entity_description, strict=True) + ) + chain_type_by_entity_id = mmcif.get_chain_type_by_entity_id(cif) + auth_asym_id_by_chain_id = mmcif.get_internal_to_author_chain_id_map(cif) + + chain_res_builder = _ChainResBuilder( + chain_key_by_chain_id=chain_key_by_chain_id, + entity_id_by_chain_id=entity_id_by_chain_id, + chain_type_by_entity_id=chain_type_by_entity_id, + entity_desc_by_entity_id=entity_desc_by_entity_id, + fix_mse_residues=fix_mse_residues, + fix_unknown_dna=fix_unknown_dna, + ) + + # Collect data for polymer chain and residue tables. _pdbx_poly_seq_scheme is + # guaranteed to be present thanks to _maybe_add_missing_scheme_tables. + def _get_poly_seq_scheme_col(col: str) -> np.ndarray: + return cif.get_array(key=f'_pdbx_poly_seq_scheme.{col}', dtype=object) + + poly_seq_asym_ids = _get_poly_seq_scheme_col('asym_id') + poly_seq_pdb_seq_nums = _get_poly_seq_scheme_col('pdb_seq_num') + poly_seq_seq_ids = _get_poly_seq_scheme_col('seq_id') + poly_seq_mon_ids = _get_poly_seq_scheme_col('mon_id') + poly_seq_pdb_strand_ids = _get_poly_seq_scheme_col('pdb_strand_id') + poly_seq_pdb_ins_codes = _get_poly_seq_scheme_col('pdb_ins_code') + string_array.remap( + poly_seq_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True + ) + + # We resolved alt-locs earlier for the atoms table. In cases of heterogeneous + # residues (a residue with an alt-loc that is of different residue type), we + # need to also do the same resolution in the residues table. Compute a mask + # for the residues that were selected in the atoms table. + poly_seq_mask = mmcif_utils.selected_polymer_residue_mask( + layout=layout, + atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()], + atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()], + atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()], + poly_seq_asym_ids=poly_seq_asym_ids, + poly_seq_seq_ids=poly_seq_seq_ids, + poly_seq_mon_ids=poly_seq_mon_ids, + ) + + if not include_other and poly_seq_mask: + # Mask filtered-out residues so that they are not treated as missing. + # Instead, we don't want them included in the chains/residues tables at all. + keep_mask = string_array.remap( + poly_seq_asym_ids, + mapping={cid: True for cid in resolved_chain_ids}, + default_value=False, + inplace=False, + ).astype(bool) + poly_seq_mask &= keep_mask + + chain_res_builder.add_residues( + chain_ids=poly_seq_asym_ids[poly_seq_mask], + chain_auth_asym_ids=poly_seq_pdb_strand_ids[poly_seq_mask], + res_ids=poly_seq_seq_ids[poly_seq_mask].astype(np.int32), + res_names=poly_seq_mon_ids[poly_seq_mask], + res_auth_seq_ids=poly_seq_pdb_seq_nums[poly_seq_mask], + res_ins_codes=poly_seq_pdb_ins_codes[poly_seq_mask], + ) + + # Collect data for ligand chain and residue tables. _pdbx_nonpoly_scheme + # could be empty/unset if there are only branched ligands. + def _get_nonpoly_scheme_col(col: str) -> np.ndarray: + key = f'_pdbx_nonpoly_scheme.{col}' + if f'_pdbx_nonpoly_scheme.{col}' in cif: + return cif.get_array(key=key, dtype=object) + else: + return np.array([], dtype=object) + + nonpoly_asym_ids = _get_nonpoly_scheme_col('asym_id') + nonpoly_auth_seq_ids = _get_nonpoly_scheme_col('pdb_seq_num') + nonpoly_pdb_ins_codes = _get_nonpoly_scheme_col('pdb_ins_code') + nonpoly_mon_ids = _get_nonpoly_scheme_col('mon_id') + nonpoly_auth_asym_id = string_array.remap( + nonpoly_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False + ) + + def _get_branch_scheme_col(col: str) -> np.ndarray: + key = f'_pdbx_branch_scheme.{col}' + if f'_pdbx_branch_scheme.{col}' in cif: + return cif.get_array(key=key, dtype=object) + else: + return np.array([], dtype=object) + + branch_asym_ids = _get_branch_scheme_col('asym_id') + branch_auth_seq_ids = _get_branch_scheme_col('pdb_seq_num') + branch_pdb_ins_codes = _get_branch_scheme_col('pdb_ins_code') + branch_mon_ids = _get_branch_scheme_col('mon_id') + branch_auth_asym_id = string_array.remap( + branch_asym_ids, mapping=auth_asym_id_by_chain_id, inplace=False + ) + + if branch_asym_ids.size > 0 and branch_pdb_ins_codes.size == 0: + branch_pdb_ins_codes = np.array( + ['.'] * branch_asym_ids.size, dtype=object) + + # Compute the heterogeneous residue masks as above, this time for ligands. + nonpoly_mask, branch_mask = mmcif_utils.selected_ligand_residue_mask( + layout=layout, + atom_site_label_asym_ids=label_asym_ids[layout.residue_starts()], + atom_site_label_seq_ids=label_seq_ids[layout.residue_starts()], + atom_site_auth_seq_ids=auth_seq_ids[layout.residue_starts()], + atom_site_label_comp_ids=label_comp_ids[layout.residue_starts()], + atom_site_pdbx_pdb_ins_codes=pdbx_pdb_ins_codes[layout.residue_starts( + )], + nonpoly_asym_ids=nonpoly_asym_ids, + nonpoly_auth_seq_ids=nonpoly_auth_seq_ids, + nonpoly_pdb_ins_codes=nonpoly_pdb_ins_codes, + nonpoly_mon_ids=nonpoly_mon_ids, + branch_asym_ids=branch_asym_ids, + branch_auth_seq_ids=branch_auth_seq_ids, + branch_pdb_ins_codes=branch_pdb_ins_codes, + branch_mon_ids=branch_mon_ids, + ) + + if not include_water: + if nonpoly_mask: + nonpoly_mask &= (nonpoly_mon_ids != 'HOH') & ( + nonpoly_mon_ids != 'DOD') + if branch_mask: + # Fix for bad mmCIFs that have water in the branch scheme table. + branch_mask &= (branch_mon_ids != 'HOH') & ( + branch_mon_ids != 'DOD') + + string_array.remap( + pdbx_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True + ) + string_array.remap( + nonpoly_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True + ) + string_array.remap( + branch_pdb_ins_codes, mapping=_INSERTION_CODE_REMAP, inplace=True + ) + + def _ligand_residue_ids(chain_ids: np.ndarray) -> np.ndarray: + """Computes internal residue ID for ligand residues that don't have it.""" + + # E.g. chain_ids=[A, A, A, B, C, C, D, D, D] -> [1, 2, 3, 1, 1, 2, 1, 2, 3]. + indices = np.arange(chain_ids.size, dtype=np.int32) + return (indices + 1) - np.maximum.accumulate( + indices * (chain_ids != np.roll(chain_ids, 1)) + ) + + branch_residue_ids = _ligand_residue_ids(branch_asym_ids[branch_mask]) + nonpoly_residue_ids = _ligand_residue_ids(nonpoly_asym_ids[nonpoly_mask]) + + chain_res_builder.add_residues( + chain_ids=branch_asym_ids[branch_mask], + chain_auth_asym_ids=branch_auth_asym_id[branch_mask], + res_ids=branch_residue_ids, + res_names=branch_mon_ids[branch_mask], + res_auth_seq_ids=branch_auth_seq_ids[branch_mask], + res_ins_codes=branch_pdb_ins_codes[branch_mask], + ) + + chain_res_builder.add_residues( + chain_ids=nonpoly_asym_ids[nonpoly_mask], + chain_auth_asym_ids=nonpoly_auth_asym_id[nonpoly_mask], + res_ids=nonpoly_residue_ids, + res_names=nonpoly_mon_ids[nonpoly_mask], + res_auth_seq_ids=nonpoly_auth_seq_ids[nonpoly_mask], + res_ins_codes=nonpoly_pdb_ins_codes[nonpoly_mask], + ) + + chains = chain_res_builder.make_chains_table() + residues = chain_res_builder.make_residues_table() + + # Construct foreign residue keys for the atoms table. + res_ends = np.array(layout.residues(), dtype=np.int32) + res_starts = np.array(layout.residue_starts(), dtype=np.int32) + res_lengths = res_ends - res_starts + + # Check just for HOH, DOD can be part e.g. of hydroxycysteine. + if include_water: + res_chain_types = chains.apply_array_to_column( + column_name='type', arr=residues.chain_key + ) + water_mask = res_chain_types != mmcif_names.WATER + if 'HOH' in set(residues.name[water_mask]): + raise ValueError( + 'Bad mmCIF file: non-water entity has water molecules.') + else: + # Include resolved and unresolved residues. + if 'HOH' in set(residues.name) | set(label_comp_ids[res_starts]): + raise ValueError( + 'Bad mmCIF file: non-water entity has water molecules.') + + atom_chain_key = string_array.remap( + label_asym_ids, mapping=chain_res_builder.chain_key_by_chain_id + ).astype(int) + + # If any of the residue lookups failed, the mmCIF is corrupted. + try: + atom_res_key_per_res = string_array.remap_multiple( + ( + label_asym_ids[res_starts], + auth_seq_ids[res_starts], + label_comp_ids[res_starts], + pdbx_pdb_ins_codes[res_starts], + ), + mapping=chain_res_builder.key_for_res, + ) + except KeyError as e: + raise ValueError( + 'Lookup for the following atom from the _atom_site table failed: ' + f'(atom_id, auth_seq_id, res_name, ins_code)={e}. This is ' + 'likely due to a known issue with some multi-model mmCIFs that only ' + 'match the first model in _atom_site table to the _pdbx_poly_scheme, ' + '_pdbx_nonpoly_scheme, or _pdbx_branch_scheme tables.' + ) from e + + # The residue ID will be shared for all atoms within that residue. + atom_res_key = np.repeat(atom_res_key_per_res, repeats=res_lengths) + + if fix_mse_residues: + met_residues_mask = (residues.name == 'MET')[atom_res_key] + unfixed_mse_selenium_mask = met_residues_mask & ( + label_atom_ids == 'SE') + label_atom_ids[unfixed_mse_selenium_mask] = 'SD' + type_symbols[unfixed_mse_selenium_mask] = 'S' + + atoms = structure_tables.Atoms( + key=atom_site_first_model, + chain_key=atom_chain_key, + res_key=atom_res_key, + name=label_atom_ids, + element=type_symbols, + x=atom_x, + y=atom_y, + z=atom_z, + b_factor=atom_b_factor, + occupancy=atom_occupancy, + ) + + return chains, residues, atoms + + +def from_atom_arrays( + *, + res_id: np.ndarray, + name: str = 'unset', + release_date: datetime.date | None = None, + resolution: float | None = None, + structure_method: str | None = None, + all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None, + bioassembly_data: bioassemblies.BioassemblyData | None = None, + chemical_components_data: ( + struc_chem_comps.ChemicalComponentsData | None + ) = None, + bond_table: structure_tables.Bonds | None = None, + chain_id: np.ndarray | None = None, + chain_type: np.ndarray | None = None, + res_name: np.ndarray | None = None, + atom_key: np.ndarray | None = None, + atom_name: np.ndarray | None = None, + atom_element: np.ndarray | None = None, + atom_x: np.ndarray | None = None, + atom_y: np.ndarray | None = None, + atom_z: np.ndarray | None = None, + atom_b_factor: np.ndarray | None = None, + atom_occupancy: np.ndarray | None = None, +) -> structure.Structure: + """Returns a Structure constructed from atom array level data. + + All fields except name and, res_id are optional, all array fields consist of a + value for each atom in the structure - so residue and chain values should hold + the same value for each atom in the chain or residue. Fields which are not + defined are filled with default values. + + Validation is performed by the Structure constructor where possible - but + author_naming scheme and all_residues must be checked in this function. + + It is not possible to construct structures with chains that do not contain + any resolved residues using this function. If this is necessary, use the + structure.Structure constructor directly. + + Args: + res_id: Integer array of shape [num_atom]. The unique residue identifier for + each residue. mmCIF field - _atom_site.label_seq_id. + name: The name of the structure. E.g. a PDB ID. + release_date: The release date of the structure as a `datetime.date`. + resolution: The resolution of the structure in Angstroms. + structure_method: The method used to solve this structure's coordinates. + all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to + a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This + can contain residues that aren't present in the atom arrays. This is + common in experimental data where some residues are not resolved but are + known to be present. + bioassembly_data: An optional instance of bioassembly.BioassemblyData. If + present then a new Structure representing a specific bioassembly can be + extracted using `Structure.generate_bioassembly(assembly_id)`. + chemical_components_data: An optional instance of ChemicalComponentsData. + Its content will be used for providing metadata about chemical components + in this Structure instance. If not specified information will be retrieved + from the standard chemical component dictionary (CCD, for more details see + https://www.wwpdb.org/data/ccd). + bond_table: A table representing manually-specified bonds. This corresponds + to the _struct_conn table in an mmCIF. Atoms are identified by their key, + as specified by the atom_key column. If this table is provided then the + atom_key column must also be defined. + chain_id: String array of shape [num_atom] of unique chain identifiers. + mmCIF field - _atom_site.label_asym_id. + chain_type: String array of shape [num_atom]. The molecular type of the + current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type + OR _entity.type (for non-polymers). + res_name: String array of shape [num_atom].. The name of each residue, + typically a 3 letter string for polypeptides or 1-2 letter strings for + polynucleotides. mmCIF field - _atom_site.label_comp_id. + atom_key: A unique sorted integer array, used only by the bonds table to + identify the atoms participating in each bond. If the bonds table is + specified then this column must be non-None. + atom_name: String array of shape [num_atom]. The name of each atom (e.g CA, + O2', etc.). mmCIF field - _atom_site.label_atom_id. + atom_element: String array of shape [num_atom]. The element type of each + atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol. + atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom + b-factors or equivalent. If there are no extra leading dimensions then + these values are assumed to apply to all coordinates for a given atom. If + there are leading dimensions then these must match those used by the + coordinate fields. + atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom + occupancies or equivalent. If there are no extra leading dimensions then + these values are assumed to apply to all coordinates for a given atom. If + there are leading dimensions then these must match those used by the + coordinate fields. + """ + + atoms, residues, chains = structure_tables.tables_from_atom_arrays( + res_id=res_id, + all_residues=all_residues, + chain_id=chain_id, + chain_type=chain_type, + res_name=res_name, + atom_key=atom_key, + atom_name=atom_name, + atom_element=atom_element, + atom_x=atom_x, + atom_y=atom_y, + atom_z=atom_z, + atom_b_factor=atom_b_factor, + atom_occupancy=atom_occupancy, + ) + + return structure.Structure( + name=name, + release_date=release_date, + resolution=resolution, + structure_method=structure_method, + bioassembly_data=bioassembly_data, + chemical_components_data=chemical_components_data, + atoms=atoms, + chains=chains, + residues=residues, + bonds=bond_table or structure_tables.Bonds.make_empty(), + ) + + +def _guess_entity_type( + chain_residues: Collection[str], atom_types: Collection[str] +) -> str: + """Guess the entity type (polymer/non-polymer/water) based on residues/atoms. + + We treat both arguments as unordered collections since we care only whether + all elements satisfy come conditions. The chain_residues can be either + grouped by residue (length num_res), or it can be raw (length num_atoms). + Atom type is unique for each atom in a residue, so don't group atom_types. + + Args: + chain_residues: A sequence of full residue name (1-letter for DNA, 2-letters + for RNA, 3 for protein). The _atom_site.label_comp_id column in mmCIF. + atom_types: Atom type: ATOM or HETATM. The _atom_site.group_PDB column in + mmCIF. + + Returns: + One of polymer/non-polymer/water based on the following criteria: + * If all atoms are HETATMs and all residues are water -> water. + * If all atoms are HETATMs and not all residues are water -> non-polymer. + * Otherwise -> polymer. + """ + if not chain_residues or not atom_types: + raise ValueError( + f'chain_residues (len {len(chain_residues)}) and atom_types (len ' + f'{len(atom_types)}) must be both non-empty. Got: {chain_residues=} ' + f'and {atom_types=}' + ) + + if all(a == 'HETATM' for a in atom_types): + if all(c in residue_names.WATER_TYPES for c in chain_residues): + return mmcif_names.WATER + return mmcif_names.NON_POLYMER_CHAIN + return mmcif_names.POLYMER_CHAIN diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/sterics.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/sterics.py new file mode 100644 index 000000000..55c7c1783 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/sterics.py @@ -0,0 +1,142 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Functions relating to spatial locations of atoms within a structure.""" + +from collections.abc import Collection, Sequence + +from alphafold3 import structure +from alphafold3.structure import mmcif +import numpy as np +import scipy + + +def _make_atom_has_clash_mask( + kd_query_result: np.ndarray, + struct: structure.Structure, + ignore_chains: Collection[str], +) -> np.ndarray: + """Returns a boolean NumPy array representing whether each atom has a clash. + + Args: + kd_query_result: NumPy array containing N-atoms arrays, each array + containing indices to atoms that clash with the N'th atom. + struct: Structure over which clashes were detected. + ignore_chains: Collection of chains that should not be considered clashing. + A boolean NumPy array of length N atoms. + """ + atom_is_clashing = np.zeros((struct.num_atoms,), dtype=bool) + for atom_index, clashes in enumerate(kd_query_result): + chain_i = struct.chain_id[atom_index] + if chain_i in ignore_chains: + continue + islig_i = struct.is_ligand_mask[atom_index] + for clashing_atom_index in clashes: + chain_c = struct.chain_id[clashing_atom_index] + if chain_c in ignore_chains: + continue + islig_c = struct.is_ligand_mask[clashing_atom_index] + if ( + clashing_atom_index == atom_index + or chain_i == chain_c + or islig_i != islig_c + ): + # Ignore clashes within chain or between ligand and polymer. + continue + atom_is_clashing[atom_index] = True + return atom_is_clashing + + +def find_clashing_chains( + struct: structure.Structure, + clash_thresh_angstrom: float = 1.7, + clash_thresh_fraction: float = 0.3, +) -> Sequence[str]: + """Finds chains that clash with others. + + Clashes are defined by polymer backbone atoms and all ligand atoms. + Ligand-polymer clashes are not dropped. + + Will not find clashes if all coordinates are 0. Coordinates are all 0s if + the structure is generated from sequences only, as done for inference in + dendro for example. + + Args: + struct: The structure defining the chains and atom positions. + clash_thresh_angstrom: Below this distance, atoms are considered clashing. + clash_thresh_fraction: Chains with more than this fraction of their atoms + considered clashing will be dropped. This value should be in the range (0, + 1]. + + Returns: + A sequence of chain ids for chains that clash. + + Raises: + ValueError: If `clash_thresh_fraction` is not in range (0,1]. + """ + if not 0 < clash_thresh_fraction <= 1: + raise ValueError('clash_thresh_fraction must be in range (0,1]') + + struct_backbone = struct.filter_polymers_to_single_atom_per_res() + if struct_backbone.num_chains == 0: + return [] + + # If the coordinates are all 0, do not search for clashes. + if not np.any(struct_backbone.coords): + return [] + + coord_kdtree = scipy.spatial.cKDTree(struct_backbone.coords) + + # For each atom coordinate, find all atoms within the clash thresh radius. + clashing_per_atom = coord_kdtree.query_ball_point( + struct_backbone.coords, r=clash_thresh_angstrom + ) + chain_ids = struct_backbone.chains + if struct_backbone.atom_occupancy is not None: + chain_occupancy = np.array([ + np.mean(struct_backbone.atom_occupancy[start:end]) + for start, end in struct_backbone.iter_chain_ranges() + ]) + else: + chain_occupancy = None + + # Remove chains until no more significant clashing. + chains_to_remove = set() + for _ in range(len(chain_ids)): + # Calculate maximally clashing. + atom_has_clash = _make_atom_has_clash_mask( + clashing_per_atom, struct_backbone, chains_to_remove + ) + clashes_per_chain = np.array([ + atom_has_clash[start:end].mean() + for start, end in struct_backbone.iter_chain_ranges() + ]) + max_clash = np.max(clashes_per_chain) + if max_clash <= clash_thresh_fraction: + # None of the remaining chains exceed the clash fraction threshold, so + # we can exit. + break + + # Greedily remove worst with the lowest occupancy. + most_clashes = np.nonzero(clashes_per_chain == max_clash)[0] + if chain_occupancy is not None: + occupancy_clashing = chain_occupancy[most_clashes] + last_lowest_occupancy = ( + len(occupancy_clashing) - + np.argmin(occupancy_clashing[::-1]) - 1 + ) + worst_and_last = most_clashes[last_lowest_occupancy] + else: + worst_and_last = most_clashes[-1] + + chains_to_remove.add(chain_ids[worst_and_last]) + + return sorted(chains_to_remove, key=mmcif.str_id_to_int_id) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure.py new file mode 100644 index 000000000..8d66b52ec --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure.py @@ -0,0 +1,3180 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Structure class for representing and processing molecular structures.""" + +import collections +from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence, Set +import dataclasses +import datetime +import enum +import functools +import itertools +import typing +from typing_extensions import Any, ClassVar, Final, Literal, NamedTuple, Self, TypeAlias, TypeVar +import numpy as np +from alphafold3.constants import atom_types +from alphafold3.constants import chemical_components +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.cpp import membership +from alphafold3.cpp import string_array +from alphafold3.structure import bioassemblies +from alphafold3.structure import chemical_components as struct_chem_comps +from alphafold3.structure import mmcif +from alphafold3.structure import structure_tables +from alphafold3.structure import table + +# Controls the default number of decimal places for coordinates when writing to +# mmCIF. +_COORDS_DECIMAL_PLACES: Final[int] = 3 + + +@enum.unique +class CascadeDelete(enum.Enum): + NONE = 0 + FULL = 1 + CHAINS = 2 + + +# See www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions +class _UnsetSentinel(enum.Enum): + UNSET = object() + + +_UNSET = _UnsetSentinel.UNSET + + +class Bond(NamedTuple): + """Describes a bond between two atoms.""" + + from_atom: Mapping[str, str | int | float | np.ndarray] + dest_atom: Mapping[str, str | int | float | np.ndarray] + bond_info: Mapping[str, str | int] + + +class MissingAtomError(Exception): + """Error raised when an atom is missing during alignment.""" + + +class MissingAuthorResidueIdError(Exception): + """Raised when author naming data is missing for a residue. + + This can occur in certain edge cases where missing residue data is provided + without also providing author IDs for those missing residues. + """ + + +# AllResidues is a mapping from label_asym_id to a sequence of (label_comp_id, +# label_seq_id) pairs. These represent the full sequence including residues +# that might be missing (e.g. unresolved residues in X-ray data). +AllResidues: TypeAlias = Mapping[str, Sequence[tuple[str, int]]] +AuthorNamingScheme: TypeAlias = structure_tables.AuthorNamingScheme + + +# External residue ID given to missing residues that don't have an ID +# already provided. In mmCIFs this data is found in _pdbx_poly_seq_scheme. +MISSING_AUTH_SEQ_ID: Final[str] = '.' + + +# Maps from structure fields to column names in the relevant table. +CHAIN_FIELDS: Final[Mapping[str, str]] = { + 'chain_id': 'id', + 'chain_type': 'type', + 'chain_auth_asym_id': 'auth_asym_id', + 'chain_entity_id': 'entity_id', + 'chain_entity_desc': 'entity_desc', +} + + +RESIDUE_FIELDS: Final[Mapping[str, str]] = { + 'res_id': 'id', + 'res_name': 'name', + 'res_auth_seq_id': 'auth_seq_id', + 'res_insertion_code': 'insertion_code', +} + +ATOM_FIELDS: Final[Mapping[str, str]] = { + 'atom_name': 'name', + 'atom_element': 'element', + 'atom_x': 'x', + 'atom_y': 'y', + 'atom_z': 'z', + 'atom_b_factor': 'b_factor', + 'atom_occupancy': 'occupancy', + 'atom_key': 'key', +} + +# Fields in structure. +ARRAY_FIELDS = frozenset({ + 'atom_b_factor', + 'atom_element', + 'atom_key', + 'atom_name', + 'atom_occupancy', + 'atom_x', + 'atom_y', + 'atom_z', + 'chain_id', + 'chain_type', + 'res_id', + 'res_name', +}) + +GLOBAL_FIELDS = frozenset({ + 'name', + 'release_date', + 'resolution', + 'structure_method', + 'bioassembly_data', + 'chemical_components_data', +}) + +# Fields which can be updated in copy_and_update. +_UPDATEABLE_FIELDS: Final[Set[str]] = frozenset({ + 'all_residues', + 'atom_b_factor', + 'atom_element', + 'atom_key', + 'atom_name', + 'atom_occupancy', + 'atom_x', + 'atom_y', + 'atom_z', + 'bioassembly_data', + 'bonds', + 'chain_id', + 'chain_type', + 'chemical_components_data', + 'name', + 'release_date', + 'res_id', + 'res_name', + 'resolution', + 'structure_method', +}) + + +def fix_non_standard_polymer_residues( + res_names: np.ndarray, chain_type: str +) -> np.ndarray: + """Remaps residue names to the closest standard protein/RNA/DNA residue. + + If residue name is already a standard type, it is not altered. + If a match cannot be found, returns 'UNK' for protein chainresidues and 'N' + for RNA/DNA chain residue. + + Args: + res_names: A numpy array of string residue names (CCD monomer codes). E.g. + 'ARG' (protein), 'DT' (DNA), 'N' (RNA). + chain_type: The type of the chain, must be PROTEIN_CHAIN, RNA_CHAIN or + DNA_CHAIN. + + Returns: + An array remapped so that its elements are all from + PROTEIN_TYPES_WITH_UNKNOWN | RNA_TYPES | DNA_TYPES | {'N'}. + + Raises: + ValueError: If chain_type not in PEPTIDE_CHAIN_TYPES or + {OTHER_CHAIN, RNA_CHAIN, DNA_CHAIN, DNA_RNA_HYBRID_CHAIN}. + """ + # Map to one letter code, then back to common res_names. + one_letter_codes = string_array.remap( + res_names, mapping=residue_names.CCD_NAME_TO_ONE_LETTER, default_value='X' + ) + + if ( + chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES + or chain_type == mmcif_names.OTHER_CHAIN + ): + mapping = residue_names.PROTEIN_COMMON_ONE_TO_THREE + default_value = 'UNK' + elif chain_type == mmcif_names.RNA_CHAIN: + # RNA has single-letter CCD monomer codes. + mapping = {r: r for r in residue_names.RNA_TYPES} + default_value = 'N' + elif chain_type == mmcif_names.DNA_CHAIN: + mapping = residue_names.DNA_COMMON_ONE_TO_TWO + default_value = 'N' + elif chain_type == mmcif_names.DNA_RNA_HYBRID_CHAIN: + mapping = {r: r for r in residue_names.NUCLEIC_TYPES_WITH_UNKNOWN} + default_value = 'N' + else: + raise ValueError( + f'Expected a protein/DNA/RNA chain but got {chain_type}') + + return string_array.remap( + one_letter_codes, mapping=mapping, default_value=default_value + ) + + +def _get_change_indices(arr: np.ndarray) -> np.ndarray: + if arr.size == 0: + return np.array([], dtype=np.int32) + else: + changing_idxs = np.where(arr[1:] != arr[:-1])[0] + 1 + return np.concatenate(([0], changing_idxs), axis=0) + + +def _unpack_filter_predicates( + predicate_by_field_name: Mapping[str, table.FilterPredicate], +) -> tuple[ + Mapping[str, table.FilterPredicate], + Mapping[str, table.FilterPredicate], + Mapping[str, table.FilterPredicate], +]: + """Unpacks filter kwargs into predicates for each table.""" + chain_predicates = {} + res_predicates = {} + atom_predicates = {} + for k, pred in predicate_by_field_name.items(): + if col := CHAIN_FIELDS.get(k): + chain_predicates[col] = pred + elif col := RESIDUE_FIELDS.get(k): + res_predicates[col] = pred + elif col := ATOM_FIELDS.get(k): + atom_predicates[col] = pred + else: + raise ValueError(k) + return chain_predicates, res_predicates, atom_predicates + + +_T = TypeVar('_T') + + +SCALAR_FIELDS: Final[Collection[str]] = frozenset({ + 'name', + 'release_date', + 'resolution', + 'structure_method', + 'bioassembly_data', + 'chemical_components_data', +}) + + +TABLE_FIELDS: Final[Collection[str]] = frozenset( + {'chains', 'residues', 'atoms', 'bonds'} +) + + +V2_FIELDS: Final[Collection[str]] = frozenset({*SCALAR_FIELDS, *TABLE_FIELDS}) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class StructureTables: + chains: structure_tables.Chains + residues: structure_tables.Residues + atoms: structure_tables.Atoms + bonds: structure_tables.Bonds + + +class Structure(table.Database): + """Structure class for representing and processing molecular structures.""" + + tables: ClassVar[Collection[str]] = TABLE_FIELDS + + foreign_keys: ClassVar[Mapping[str, Collection[tuple[str, str]]]] = { + 'residues': (('chain_key', 'chains'),), + 'atoms': (('chain_key', 'chains'), ('res_key', 'residues')), + 'bonds': (('from_atom_key', 'atoms'), ('dest_atom_key', 'atoms')), + } + + def __init__( + self, + *, + name: str = 'unset', + release_date: datetime.date | None = None, + resolution: float | None = None, + structure_method: str | None = None, + bioassembly_data: bioassemblies.BioassemblyData | None = None, + chemical_components_data: ( + struct_chem_comps.ChemicalComponentsData | None + ) = None, + chains: structure_tables.Chains, + residues: structure_tables.Residues, + atoms: structure_tables.Atoms, + bonds: structure_tables.Bonds, + skip_validation: bool = False, + ): + # Version number is written to mmCIF and should be incremented when changes + # are made to mmCIF writing or internals that affect this. + # b/345221494 Rename this variable when structure_v1 compatibility code + # is removed. + self._VERSION = '2.0.0' # pylint: disable=invalid-name + self._name = name + self._release_date = release_date + self._resolution = resolution + self._structure_method = structure_method + self._bioassembly_data = bioassembly_data + self._chemical_components_data = chemical_components_data + + self._chains = chains + self._residues = residues + self._atoms = atoms + self._bonds = bonds + + if not skip_validation: + self._validate_table_foreign_keys() + self._validate_consistent_table_ordering() + + def _validate_table_foreign_keys(self): + """Validates that all foreign keys are present in the referred tables.""" + residue_keys = set(self._residues.key) + chain_keys = set(self._chains.key) + if np.any(membership.isin(self._atoms.res_key, residue_keys, invert=True)): + raise ValueError( + 'Atom residue keys not in the residues table: ' + f'{set(self._atoms.res_key).difference(self._residues.key)}' + ) + if np.any(membership.isin(self._atoms.chain_key, chain_keys, invert=True)): + raise ValueError( + 'Atom chain keys not in the chains table: ' + f'{set(self._atoms.chain_key).difference(self._chains.key)}' + ) + if np.any( + membership.isin(self._residues.chain_key, chain_keys, invert=True) + ): + raise ValueError( + 'Residue chain keys not in the chains table: ' + f'{set(self._residues.chain_key).difference(self._chains.key)}' + ) + + def _validate_consistent_table_ordering(self): + """Validates that all tables have the same ordering.""" + atom_chain_keys = self._atoms.chain_key[self.chain_boundaries] + atom_res_keys = self._atoms.res_key[self.res_boundaries] + + if not np.array_equal(self.present_chains.key, atom_chain_keys): + raise ValueError( + f'Atom table chain order\n{atom_chain_keys}\ndoes not match the ' + f'chain table order\n{self._chains.key}' + ) + if not np.array_equal(self.present_residues.key, atom_res_keys): + raise ValueError( + f'Atom table residue order\n{atom_res_keys}\ndoes not match the ' + f'present residue table order\n{self.present_residues.key}' + ) + + def get_table(self, table_name: str) -> table.Table: + match table_name: + case 'chains': + return self.chains_table + case 'residues': + return self.residues_table + case 'atoms': + return self.atoms_table + case 'bonds': + return self.bonds_table + case _: + raise ValueError(table_name) + + @property + def chains_table(self) -> structure_tables.Chains: + """Chains table.""" + return self._chains + + @property + def residues_table(self) -> structure_tables.Residues: + """Residues table.""" + return self._residues + + @property + def atoms_table(self) -> structure_tables.Atoms: + """Atoms table.""" + return self._atoms + + @property + def bonds_table(self) -> structure_tables.Bonds: + """Bonds table.""" + return self._bonds + + @property + def name(self) -> str: + return self._name + + @property + def release_date(self) -> datetime.date | None: + return self._release_date + + @property + def resolution(self) -> float | None: + return self._resolution + + @property + def structure_method(self) -> str | None: + return self._structure_method + + @property + def bioassembly_data(self) -> bioassemblies.BioassemblyData | None: + return self._bioassembly_data + + @property + def chemical_components_data( + self, + ) -> struct_chem_comps.ChemicalComponentsData | None: + return self._chemical_components_data + + @property + def bonds(self) -> structure_tables.Bonds: + return self._bonds + + @functools.cached_property + def author_naming_scheme(self) -> AuthorNamingScheme: + auth_asym_id = {} + entity_id = {} + entity_desc = {} + auth_seq_id = collections.defaultdict(dict) + insertion_code = collections.defaultdict(dict) + + for chain_i in range(self._chains.size): + chain_id = self._chains.id[chain_i] + auth_asym_id[chain_id] = self._chains.auth_asym_id[chain_i] + chain_entity_id = self._chains.entity_id[chain_i] + entity_id[chain_id] = chain_entity_id + entity_desc[chain_entity_id] = self._chains.entity_desc[chain_i] + + chain_index_by_key = self._chains.index_by_key + for res_i in range(self._residues.size): + chain_key = self._residues.chain_key[res_i] + chain_id = self._chains.id[chain_index_by_key[chain_key]] + res_id = self._residues.id[res_i] + res_auth_seq_id = self._residues.auth_seq_id[res_i] + if res_auth_seq_id == MISSING_AUTH_SEQ_ID: + continue + auth_seq_id[chain_id][res_id] = res_auth_seq_id + ins_code = self._residues.insertion_code[res_i] + # Compatibility with Structure v1 which used None to represent . or ?. + insertion_code[chain_id][res_id] = ( + ins_code if ins_code not in {'.', '?'} else None + ) + + return AuthorNamingScheme( + auth_asym_id=auth_asym_id, + entity_id=entity_id, + entity_desc=entity_desc, + auth_seq_id=dict(auth_seq_id), + insertion_code=dict(insertion_code), + ) + + @functools.cached_property + def all_residues(self) -> AllResidues: + chain_id_by_key = dict(zip(self._chains.key, self._chains.id)) + residue_chain_boundaries = _get_change_indices( + self._residues.chain_key) + boundaries = self._iter_residue_ranges( + residue_chain_boundaries, count_unresolved=True + ) + return { + chain_id_by_key[self._residues.chain_key[start]]: list( + zip(self._residues.name[start:end], + self._residues.id[start:end]) + ) + for start, end in boundaries + } + + @functools.cached_property + def label_asym_id_to_entity_id(self) -> Mapping[str, str]: + return dict(zip(self._chains.id, self._chains.entity_id)) + + @functools.cached_property + def chain_entity_id(self) -> np.ndarray: + """Returns the entity ID for each atom in the structure.""" + return self.chains_table.apply_array_to_column( + 'entity_id', self._atoms.chain_key + ) + + @functools.cached_property + def chain_entity_desc(self) -> np.ndarray: + """Returns the entity description for each atom in the structure.""" + return self.chains_table.apply_array_to_column( + 'entity_desc', self._atoms.chain_key + ) + + @functools.cached_property + def chain_auth_asym_id(self) -> np.ndarray: + """Returns the chain auth asym ID for each atom in the structure.""" + return self.chains_table.apply_array_to_column( + 'auth_asym_id', self._atoms.chain_key + ) + + @functools.cached_property + def chain_id(self) -> np.ndarray: + chain_index_by_key = self._chains.index_by_key + return self._chains.id[chain_index_by_key[self._atoms.chain_key]] + + @functools.cached_property + def chain_type(self) -> np.ndarray: + chain_index_by_key = self._chains.index_by_key + return self._chains.type[chain_index_by_key[self._atoms.chain_key]] + + @functools.cached_property + def res_id(self) -> np.ndarray: + return self._residues['id', self._atoms.res_key] + + @functools.cached_property + def res_name(self) -> np.ndarray: + return self._residues['name', self._atoms.res_key] + + @functools.cached_property + def res_auth_seq_id(self) -> np.ndarray: + """Returns the residue auth seq ID for each atom in the structure.""" + return self.residues_table.apply_array_to_column( + 'auth_seq_id', self._atoms.res_key + ) + + @functools.cached_property + def res_insertion_code(self) -> np.ndarray: + """Returns the residue insertion code for each atom in the structure.""" + return self.residues_table.apply_array_to_column( + 'insertion_code', self._atoms.res_key + ) + + @property + def atom_key(self) -> np.ndarray: + return self._atoms.key + + @property + def atom_name(self) -> np.ndarray: + return self._atoms.name + + @property + def atom_element(self) -> np.ndarray: + return self._atoms.element + + @property + def atom_x(self) -> np.ndarray: + return self._atoms.x + + @property + def atom_y(self) -> np.ndarray: + return self._atoms.y + + @property + def atom_z(self) -> np.ndarray: + return self._atoms.z + + @property + def atom_b_factor(self) -> np.ndarray: + return self._atoms.b_factor + + @property + def atom_occupancy(self) -> np.ndarray: + return self._atoms.occupancy + + @functools.cached_property + def chain_boundaries(self) -> np.ndarray: + """The indices in the atom fields where each chain begins.""" + return _get_change_indices(self._atoms.chain_key) + + @functools.cached_property + def res_boundaries(self) -> np.ndarray: + """The indices in the atom fields where each residue begins.""" + return _get_change_indices(self._atoms.res_key) + + @functools.cached_property + def present_chains(self) -> structure_tables.Chains: + """Returns table of chains which have at least 1 resolved atom.""" + is_present_mask = np.isin(self._chains.key, self._atoms.chain_key) + return typing.cast(structure_tables.Chains, self._chains[is_present_mask]) + + @functools.cached_property + def present_residues(self) -> structure_tables.Residues: + """Returns table of residues which have at least 1 resolved atom.""" + is_present_mask = np.isin(self._residues.key, self._atoms.res_key) + return typing.cast( + structure_tables.Residues, self._residues[is_present_mask] + ) + + @functools.cached_property + def unresolved_residues(self) -> structure_tables.Residues: + """Returns table of residues which have at least 1 resolved atom.""" + is_unresolved_mask = np.isin( + self._residues.key, self._atoms.res_key, invert=True + ) + return typing.cast( + structure_tables.Residues, self._residues[is_unresolved_mask] + ) + + def __getitem__(self, field: str) -> Any: + """Gets raw field data using field name as a string.""" + if field in TABLE_FIELDS: + return self.get_table(field) + else: + return getattr(self, field) + + def __getstate__(self) -> dict[str, Any]: + """Pickle calls this on dump. + + Returns: + Members with cached properties removed. + """ + cached_props = { + k + for k, v in self.__class__.__dict__.items() + if isinstance(v, functools.cached_property) + } + return {k: v for k, v in self.__dict__.items() if k not in cached_props} + + def __repr__(self): + return ( + f'Structure({self._name}: {self.num_chains} chains, ' + f'{self.num_residues(count_unresolved=False)} residues, ' + f'{self.num_atoms} atoms)' + ) + + @property + def num_atoms(self) -> int: + return self._atoms.size + + def num_residues(self, *, count_unresolved: bool) -> int: + """Returns the number of residues in this Structure. + + Args: + count_unresolved: Whether to include unresolved (empty) residues. + + Returns: + Number of residues in the Structure. + """ + if count_unresolved: + return self._residues.size + else: + return self.present_residues.size + + @property + def num_chains(self) -> int: + return self._chains.size + + @property + def num_models(self) -> int: + """The number of models of this Structure.""" + return self._atoms.num_models + + def _atom_mask(self, entities: Set[str]) -> np.ndarray: + """Boolean label indicating if each atom is from entities or not.""" + mask = np.zeros(self.num_atoms, dtype=bool) + chain_index_by_key = self._chains.index_by_key + for start, end in self.iter_chain_ranges(): + chain_index = chain_index_by_key[self._atoms.chain_key[start]] + chain_type = self._chains.type[chain_index] + mask[start:end] = chain_type in entities + return mask + + @functools.cached_property + def is_protein_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is from protein or not.""" + return self._atom_mask(entities={mmcif_names.PROTEIN_CHAIN}) + + @functools.cached_property + def is_dna_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is from DNA or not.""" + return self._atom_mask(entities={mmcif_names.DNA_CHAIN}) + + @functools.cached_property + def is_rna_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is from RNA or not.""" + return self._atom_mask(entities={mmcif_names.RNA_CHAIN}) + + @functools.cached_property + def is_nucleic_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is a nucleic acid or not.""" + return self._atom_mask(entities=mmcif_names.NUCLEIC_ACID_CHAIN_TYPES) + + @functools.cached_property + def is_ligand_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is a ligand or not.""" + return self._atom_mask(entities=mmcif_names.LIGAND_CHAIN_TYPES) + + @functools.cached_property + def is_water_mask(self) -> np.ndarray: + """Boolean label indicating if each atom is from water or not.""" + return self._atom_mask(entities={mmcif_names.WATER}) + + def iter_atoms(self) -> Iterator[Mapping[str, Any]]: + """Iterates over the atoms in the structure.""" + if self._atoms.size == 0: + return + + current_chain = self._chains.get_row_by_key( + column_name_map=CHAIN_FIELDS, key=self._atoms.chain_key[0] + ) + current_chain_key = self._atoms.chain_key[0] + current_res = self._residues.get_row_by_key( + column_name_map=RESIDUE_FIELDS, key=self._atoms.res_key[0] + ) + current_res_key = self._atoms.res_key[0] + for atom_i in range(self._atoms.size): + atom_chain_key = self._atoms.chain_key[atom_i] + atom_res_key = self._atoms.res_key[atom_i] + + if atom_chain_key != current_chain_key: + chain_index = self._chains.index_by_key[atom_chain_key] + current_chain = { + 'chain_id': self._chains.id[chain_index], + 'chain_type': self._chains.type[chain_index], + 'chain_auth_asym_id': self._chains.auth_asym_id[chain_index], + 'chain_entity_id': self._chains.entity_id[chain_index], + 'chain_entity_desc': self._chains.entity_desc[chain_index], + } + current_chain_key = atom_chain_key + if atom_res_key != current_res_key: + res_index = self._residues.index_by_key[atom_res_key] + current_res = { + 'res_id': self._residues.id[res_index], + 'res_name': self._residues.name[res_index], + 'res_auth_seq_id': self._residues.auth_seq_id[res_index], + 'res_insertion_code': self._residues.insertion_code[res_index], + } + current_res_key = atom_res_key + + yield { + 'atom_name': self._atoms.name[atom_i], + 'atom_element': self._atoms.element[atom_i], + 'atom_x': self._atoms.x[..., atom_i], + 'atom_y': self._atoms.y[..., atom_i], + 'atom_z': self._atoms.z[..., atom_i], + 'atom_b_factor': self._atoms.b_factor[..., atom_i], + 'atom_occupancy': self._atoms.occupancy[..., atom_i], + 'atom_key': self._atoms.key[atom_i], + **current_res, + **current_chain, + } + + def iter_residues( + self, + include_unresolved: bool = False, + ) -> Iterator[Mapping[str, Any]]: + """Iterates over the residues in the structure.""" + res_table = self._residues if include_unresolved else self.present_residues + if res_table.size == 0: + return + + current_chain = self._chains.get_row_by_key( + column_name_map=CHAIN_FIELDS, key=res_table.chain_key[0] + ) + current_chain_key = res_table.chain_key[0] + for res_i in range(res_table.size): + res_chain_key = res_table.chain_key[res_i] + + if res_chain_key != current_chain_key: + current_chain = self._chains.get_row_by_key( + column_name_map=CHAIN_FIELDS, key=res_table.chain_key[res_i] + ) + current_chain_key = res_chain_key + + row = { + 'res_id': res_table.id[res_i], + 'res_name': res_table.name[res_i], + 'res_auth_seq_id': res_table.auth_seq_id[res_i], + 'res_insertion_code': res_table.insertion_code[res_i], + } + yield row | current_chain + + def _iter_atom_ranges( + self, boundaries: Sequence[int] + ) -> Iterator[tuple[int, int]]: + """Iterator for (start, end) pairs from an array of start indices.""" + yield from itertools.pairwise(boundaries) + # Use explicit length test as boundaries can be a NumPy array. + if len(boundaries) > 0: # pylint: disable=g-explicit-length-test + yield boundaries[-1], self.num_atoms + + def _iter_residue_ranges( + self, + boundaries: Sequence[int], + *, + count_unresolved: bool, + ) -> Iterator[tuple[int, int]]: + """Iterator for (start, end) pairs from an array of start indices.""" + yield from itertools.pairwise(boundaries) + # Use explicit length test as boundaries can be a NumPy array. + if len(boundaries) > 0: # pylint: disable=g-explicit-length-test + yield boundaries[-1], self.num_residues(count_unresolved=count_unresolved) + + def iter_chain_ranges(self) -> Iterator[tuple[int, int]]: + """Iterates pairs of (chain_start, chain_end) indices. + + Yields: + Pairs of (start, end) indices for each chain, where end is not inclusive. + i.e. struct.chain_id[start:end] would be a constant array with length + equal to the number of atoms in the chain. + """ + yield from self._iter_atom_ranges(self.chain_boundaries) + + def iter_residue_ranges(self) -> Iterator[tuple[int, int]]: + """Iterates pairs of (residue_start, residue_end) indices. + + Yields: + Pairs of (start, end) indices for each residue, where end is not + inclusive. i.e. struct.res_id[start:end] would be a constant array with + length equal to the number of atoms in the residue. + """ + yield from self._iter_atom_ranges(self.res_boundaries) + + def iter_chains(self) -> Iterator[Mapping[str, Any]]: + """Iterates over the chains in the structure.""" + for chain_i in range(self.present_chains.size): + yield { + 'chain_id': self.present_chains.id[chain_i], + 'chain_type': self.present_chains.type[chain_i], + 'chain_auth_asym_id': self.present_chains.auth_asym_id[chain_i], + 'chain_entity_id': self.present_chains.entity_id[chain_i], + 'chain_entity_desc': self.present_chains.entity_desc[chain_i], + } + + def iter_bonds(self) -> Iterator[Bond]: + """Iterates over the atoms and bond information. + + Example usage: + + ``` + for from_atom, dest_atom, bond_info in struct.iter_bonds(): + print( + f'From atom: name={from_atom["atom_name"]}, ' + f'chain={from_atom["chain_id"]}, ...' + ) + # Same for dest_atom + print(f'Bond info: type={bond_info["type"]}, role={bond_info["role"]}') + ``` + + Yields: + A `Bond` NamedTuple for each bond in the bonds table. + These have fields `from_atom`, `dest_atom`, `bond_info` where each + is a dictionary. The first two have the same keys as the atom dicts + returned by self.iter_atoms() -- i.e. one key per non-None field. + The final dict has the same keys as self.bonds.iterrows() -- i.e. one + key per column in the bonds table. + """ + from_atom_iter = self._atoms.iterrows( + row_keys=self._bonds.from_atom_key, + column_name_map=ATOM_FIELDS, + chain_key=self._chains.with_column_names(CHAIN_FIELDS), + res_key=self._residues.with_column_names(RESIDUE_FIELDS), + ) + dest_atom_iter = self._atoms.iterrows( + row_keys=self._bonds.dest_atom_key, + column_name_map=ATOM_FIELDS, + chain_key=self._chains.with_column_names(CHAIN_FIELDS), + res_key=self._residues.with_column_names(RESIDUE_FIELDS), + ) + + for from_atom, dest_atom, bond_info in zip( + from_atom_iter, dest_atom_iter, self._bonds.iterrows(), strict=True + ): + yield Bond(from_atom=from_atom, dest_atom=dest_atom, bond_info=bond_info) + + def _apply_atom_index_array( + self, + index_arr: np.ndarray, + chain_boundaries: np.ndarray | None = None, + res_boundaries: np.ndarray | None = None, + skip_validation: bool = False, + ) -> Self: + """Applies index_arr to the atom table using NumPy-style array indexing. + + Args: + index_arr: A 1D NumPy array that will be used to index into the atoms + table. This can either be a boolean array to act as a mask, or an + integer array to perform a gather operation. + chain_boundaries: Unused in structure v2. + res_boundaries: Unused in structure v2. + skip_validation: Whether to skip the validation step that checks internal + consistency after applying atom index array. Do not set to True unless + you are certain the transform is safe, e.g. when the order of atoms is + guaranteed to not change. + + Returns: + A new Structure with an updated atoms table. + """ + del chain_boundaries, res_boundaries + + if index_arr.ndim != 1: + raise ValueError( + f'index_arr must be a 1D NumPy array, but has shape {index_arr.shape}' + ) + + if index_arr.dtype == bool and np.all(index_arr): + # Shortcut: The operation is a no-op, so just return itself. + return self + + atoms = structure_tables.Atoms( + **{col: self._atoms[col][..., index_arr] for col in self._atoms.columns} + ) + updated_tables = self._cascade_delete(atoms=atoms) + return self.copy_and_update( + atoms=updated_tables.atoms, + bonds=updated_tables.bonds, + skip_validation=skip_validation, + ) + + @property + def group_by_residue(self) -> Self: + """Returns a Structure with one atom per residue. + + e.g. restypes = struct.group_by_residue['res_id'] + + Returns: + A new Structure with one atom per residue such that per-atom arrays + such as res_name (i.e. Structure v1 fields) have one element per residue. + """ + # This use of _apply_atom_index_array is safe because the chain/residue/atom + # ordering won't change (essentially applying a residue start mask). + return self._apply_atom_index_array( + self.res_boundaries, skip_validation=True + ) + + @property + def group_by_chain(self) -> Self: + """Returns a Structure where all fields are per-chain. + + e.g. chains = struct.group_by_chain['chain_id'] + + Returns: + A new Structure with one atom per chain such that per-atom arrays + such as res_name (i.e. Structure v1 fields) have one element per chain. + """ + # This use of _apply_atom_index_array is safe because the chain/residue/atom + # ordering won't change (essentially applying a chain start mask). + return self._apply_atom_index_array( + self.chain_boundaries, skip_validation=True + ) + + @property + def with_sorted_chains(self) -> Self: + """Returns a new structure with the chains are in reverse spreadsheet style. + + This is the usual order to write chains in an mmCIF: + (A < B < ... < AA < BA < CA < ... < AB < BB < CB ...) + + NB: this method will fail if chains do not conform to this mmCIF naming + convention. + + Only to be used for third party metrics that rely on the chain order. + Elsewhere chains should be identified by name and code should be agnostic to + the order. + """ + sorted_chains = sorted(self.chains, key=mmcif.str_id_to_int_id) + return self.reorder_chains(new_order=sorted_chains) + + @functools.cached_property + def atom_ids(self) -> Sequence[tuple[str, str, None, str]]: + """Gets a list of atom ID tuples from Structure class arrays. + + Returns: + A list of tuples of (chain_id, res_id, insertion_code, atom_name) where + insertion code is always None. There is one element per atom, and the + list is ordered according to the order of atoms in the input arrays. + """ + # Convert to Numpy strings, then to Python strings (dtype=object). + res_ids = self.residues_table.id.astype(str).astype(object) + res_ids = res_ids[ + self.residues_table.index_by_key[self.atoms_table.res_key] + ] + ins_codes = [None] * self.num_atoms + return list( + zip(self.chain_id, res_ids, ins_codes, self.atom_name, strict=True) + ) + + def order_and_drop_atoms_to_match( + self, + other: 'Structure', + *, + allow_missing_atoms: bool = False, + ) -> Self: + """Returns a new structure with atoms ordered & dropped to match another's. + + This performs two operations simultaneously: + * Ordering the atoms in this structure to match the order in the other. + * Dropping atoms in this structure that do not appear in the other. + + Example: + Consider a prediction and ground truth with the following atoms, described + using tuples of `(chain_id, res_id, atom_name)`: + * `prediction: [(A, 1, CA), (A, 1, N), (A, 2, CA), (B, 1, CA)]` + * `ground_truth: [(B, 1, CA), (A, 1, N), (A, 1, CA)]` + Note how the ground truth is missing the `(A, 2, CA)` atom and also + has the atoms in a different order. This method returns a modified + prediction that has reordered atoms and without any atoms not in the ground + truth so that its atom list looks the same as the ground truth atom list. + This means `prediction.coords` and `ground_truth.coords` now have the + same shape and can be compared across the atom dimension. + + Note that matching residues with no atoms and matching chains with no + residues will also be kept. E.g. in the example above, if prediction and + ground truth both had an unresolved residue (A, 3), the output structure + will also have an unresolved residue (A, 3). + + Args: + other: Another `Structure`. This provides the reference ordering that is + used to sort this structure's atom arrays. + allow_missing_atoms: Whether to skip atoms present in `other` but not this + structure and return a structure containing a subset of the atoms in the + other structure. + + Returns: + A new `Structure`, based on this structure, which, if + `allow_missing_atoms` is False, contains exactly the same atoms as in + the `other` structure and which matches the `other` structure in terms + of the order of the atoms in the field arrays. Otherwise, if missing + atoms are allowed then the resulting structure contains a subset of + those atoms in the other structure. + + Raises: + MissingAtomError: If there are atoms present in the other structure that + cannot be found in this structure. + """ + atom_index_map = {atom_id: i for i, + atom_id in enumerate(self.atom_ids)} + try: + if allow_missing_atoms: + # Only include atoms that were found in the other structure. + atom_indices = [ + atom_index + for atom_id in other.atom_ids + if (atom_index := atom_index_map.get(atom_id)) is not None + ] + else: + atom_indices = [ + atom_index_map[atom_id] # Hard fail on missing. + for atom_id in other.atom_ids + ] + except KeyError as e: + if len(e.args[0]) == 4: + chain_id, res_id, ins_code, atom_name = e.args[0] + raise MissingAtomError( + f'No atom in this structure (name: {self._name}) matches atom in ' + f'other structure (name: {other.name}) with internal (label) chain ' + f'ID {chain_id}, residue ID {res_id}, insertion code {ins_code} ' + f'and atom name {atom_name}.' + ) from e + else: + raise + + def _iter_residues(struct: Self) -> Iterable[tuple[str, str]]: + yield from zip( + struct.chains_table['id', struct.residues_table.chain_key], + struct.residues_table.id, + strict=True, + ) + + chain_index_map = { + chain_id: i for i, chain_id in enumerate(self._chains.id) + } + chain_indices = [ + chain_index + for chain_id in other.chains_table.id + if (chain_index := chain_index_map.get(chain_id)) is not None + ] + residue_index_map = { + res_id: i for i, res_id in enumerate(_iter_residues(self)) + } + res_indices = [ + residue_index + for res_id in _iter_residues(other) + if (residue_index := residue_index_map.get(res_id)) is not None + ] + + # Reorder all tables. + chains = self._chains.apply_index( + np.array(chain_indices, dtype=np.int64)) + residues = self._residues.apply_index( + np.array(res_indices, dtype=np.int64)) + atoms = self._atoms.apply_index(np.array(atom_indices, dtype=np.int64)) + + # Get chain keys in the order they appear in the atoms table. + new_chain_boundaries = _get_change_indices(atoms.chain_key) + new_chain_key_order = atoms.chain_key[new_chain_boundaries] + if len(new_chain_key_order) != len(set(new_chain_key_order)): + raise ValueError( + f'Chain keys not contiguous after reordering: {new_chain_key_order}' + ) + + # Get residue keys in the order they appear in the atoms table. + new_res_boundaries = _get_change_indices(atoms.res_key) + new_res_key_order = atoms.res_key[new_res_boundaries] + if len(new_res_key_order) != len(set(new_res_key_order)): + raise ValueError( + f'Residue keys not contiguous after reordering: {new_res_key_order}' + ) + + # If any atoms were deleted, propagate that into the bonds table. + updated_tables = self._cascade_delete( + chains=chains, + residues=residues, + atoms=atoms, + ) + return self.copy_and_update( + chains=chains, + residues=residues, + atoms=updated_tables.atoms, + bonds=updated_tables.bonds, + ) + + def copy_and_update( + self, + *, + name: str | Literal[_UNSET] = _UNSET, + release_date: datetime.date | None | Literal[_UNSET] = _UNSET, + resolution: float | None | Literal[_UNSET] = _UNSET, + structure_method: str | None | Literal[_UNSET] = _UNSET, + bioassembly_data: ( + bioassemblies.BioassemblyData | None | Literal[_UNSET] + ) = _UNSET, + chemical_components_data: ( + struct_chem_comps.ChemicalComponentsData | None | Literal[_UNSET] + ) = _UNSET, + chains: structure_tables.Chains | None | Literal[_UNSET] = _UNSET, + residues: structure_tables.Residues | None | Literal[_UNSET] = _UNSET, + atoms: structure_tables.Atoms | None | Literal[_UNSET] = _UNSET, + bonds: structure_tables.Bonds | None | Literal[_UNSET] = _UNSET, + skip_validation: bool = False, + ) -> Self: + """Performs a shallow copy but with specified fields updated.""" + + def all_unset(fields): + return all(field == _UNSET for field in fields) + + if all_unset((chains, residues, atoms, bonds)): + if all_unset(( + name, + release_date, + resolution, + structure_method, + bioassembly_data, + chemical_components_data, + )): + raise ValueError( + 'Unnecessary call to copy_and_update with no changes. As Structure' + ' and its component tables are immutable, there is no need to copy' + ' it. Any subsequent operation that modifies structure will return' + ' a new object.' + ) + else: + raise ValueError( + 'When only changing global fields, prefer to use the specialised ' + 'copy_and_update_globals.' + ) + + def select(field, default): + return field if field != _UNSET else default + + return Structure( + name=select(name, self.name), + release_date=select(release_date, self.release_date), + resolution=select(resolution, self.resolution), + structure_method=select(structure_method, self.structure_method), + bioassembly_data=select(bioassembly_data, self.bioassembly_data), + chemical_components_data=select( + chemical_components_data, self.chemical_components_data + ), + chains=select(chains, self._chains), + residues=select(residues, self._residues), + atoms=select(atoms, self._atoms), + bonds=select(bonds, self._bonds), + skip_validation=skip_validation, + ) + + def _copy_and_update( + self, skip_validation: bool = False, **changes: Any + ) -> Self: + """Performs a shallow copy but with specified fields updated.""" + if not changes: + raise ValueError( + 'Unnecessary call to copy_and_update with no changes. As Structure ' + 'and its component tables are immutable, there is no need to copy ' + 'it. Any subsequent operation that modifies structure will return a ' + 'new object.' + ) + + if 'author_naming_scheme' in changes: + raise ValueError( + 'Updating using author_naming_scheme is not supported. Update ' + 'auth_asym_id, entity_id, entity_desc fields directly in the chains ' + 'table and auth_seq_id, insertion_code in the residues table.' + ) + + if all(k in GLOBAL_FIELDS for k in changes): + raise ValueError( + 'When only changing global fields, prefer to use the specialised ' + 'copy_and_update_globals.' + ) + + if all(k in V2_FIELDS for k in changes): + constructor_kwargs = {field: self[field] for field in V2_FIELDS} + constructor_kwargs.update(changes) + elif any(k in ('atoms', 'residues', 'chains') for k in changes): + raise ValueError( + 'Cannot specify atoms/chains/residues table changes with non-v2' + f' constructor params: {changes.keys()}' + ) + elif all(k in ATOM_FIELDS for k in changes): + if 'atom_key' not in changes: + raise ValueError( + 'When only changing atom fields, prefer to use the specialised ' + 'copy_and_update_atoms.' + ) + # Only atom fields are being updated, do that directly on the atoms table. + updated_atoms = self._atoms.copy_and_update( + **{ATOM_FIELDS[k]: v for k, v in changes.items()} + ) + constructor_kwargs = { + field: self[field] for field in V2_FIELDS if field != 'atoms' + } + constructor_kwargs['atoms'] = updated_atoms + else: + constructor_kwargs = {field: self[field] + for field in _UPDATEABLE_FIELDS} + constructor_kwargs.update(changes) + return Structure(skip_validation=skip_validation, **constructor_kwargs) + + def copy_and_update_coords(self, coords: np.ndarray) -> Self: + """Performs a shallow copy but with coordinates updated.""" + if coords.shape[-2:] != (self.num_atoms, 3): + raise ValueError( + f'{coords.shape=} does not have last dimensions ({self.num_atoms}, 3)' + ) + updated_atoms = self._atoms.copy_and_update_coords(coords) + return self.copy_and_update(atoms=updated_atoms, skip_validation=True) + + def copy_and_update_from_res_arrays(self, **changes: np.ndarray) -> Self: + """Like copy_and_update but changes are arrays of length num_residues. + + These changes are first scattered into arrays of length num_atoms such + that each value is repeated across the residue at that index, then they + are used as the new values of these fields. + + E.g. + * This structure's res_id: 1, 1, 1, 2, 3, 3 (3 res, 6 atoms) + * new atom_b_factor: 7, 8, 9 + * Returned structure's atom_b_factor: 7, 7, 7, 8, 9, 9 + + Args: + **changes: kwargs corresponding to atom array fields, e.g. atom_x or + atom_b_factor, but with length num_residues rather than num_atoms. Note + that changing atom_key this way is is not supported. + + Returns: + A new `Structure` with all fields other than those specified as kwargs + shallow copied from this structure. The values of the kwargs are + scattered across the atom arrays and then used to overwrite these + fields for the returned structure. + """ + # We create scatter indices by (1) starting from zeros, then (2) setting + # the position where each residue starts to 1 and then (3) doing a + # cumulative sum. Finally, since self.res_boundaries always starts with 0 + # the result of the cumulative sum will start from 1, so (4) we subtract + # 1 to get the final array of zero-based indices. + # Example, 6 atoms, 3 residues at indices 0, 2 and 5. + # (1) 0 0 0 0 0 0 + # (2) 1 0 1 0 0 1 + # (3) 1 1 2 2 2 3 + # (4) 0 0 1 1 1 2 + if not all(c in set(ATOM_FIELDS) - {'atom_key'} for c in changes): + raise ValueError( + 'Changes must only be to atom fields, got changes to' + f' {changes.keys()}' + ) + scatter_idxs = np.zeros((self.num_atoms,), dtype=int) + scatter_idxs[self.res_boundaries] = 1 + scatter_idxs = scatter_idxs.cumsum() - 1 + atom_array_changes = { + ATOM_FIELDS[field]: new_val[scatter_idxs] + for field, new_val in changes.items() + } + updated_atoms = self._atoms.copy_and_update(**atom_array_changes) + return self.copy_and_update(atoms=updated_atoms, skip_validation=True) + + def copy_and_update_globals( + self, + *, + name: str | Literal[_UNSET] = _UNSET, + release_date: datetime.date | Literal[_UNSET] | None = _UNSET, + resolution: float | Literal[_UNSET] | None = _UNSET, + structure_method: str | Literal[_UNSET] | None = _UNSET, + bioassembly_data: ( + bioassemblies.BioassemblyData | Literal[_UNSET] | None + ) = _UNSET, + chemical_components_data: ( + struct_chem_comps.ChemicalComponentsData | Literal[_UNSET] | None + ) = _UNSET, + ) -> Self: + """Returns a shallow copy with the global columns updated.""" + + def select(field, default): + return field if field != _UNSET else default + + name = select(name, self.name) + release_date = select(release_date, self.release_date) + resolution = select(resolution, self.resolution) + structure_method = select(structure_method, self.structure_method) + bioassembly_data = select(bioassembly_data, self.bioassembly_data) + chem_data = select(chemical_components_data, + self.chemical_components_data) + + return Structure( + name=name, + release_date=release_date, + resolution=resolution, + structure_method=structure_method, + bioassembly_data=bioassembly_data, + chemical_components_data=chem_data, + atoms=self._atoms, + residues=self._residues, + chains=self._chains, + bonds=self._bonds, + ) + + def copy_and_update_atoms( + self, + *, + atom_name: np.ndarray | None = None, + atom_element: np.ndarray | None = None, + atom_x: np.ndarray | None = None, + atom_y: np.ndarray | None = None, + atom_z: np.ndarray | None = None, + atom_b_factor: np.ndarray | None = None, + atom_occupancy: np.ndarray | None = None, + ) -> Self: + """Returns a shallow copy with the atoms table updated.""" + new_atoms = structure_tables.Atoms( + key=self._atoms.key, + res_key=self._atoms.res_key, + chain_key=self._atoms.chain_key, + name=atom_name if atom_name is not None else self.atom_name, + element=atom_element if atom_element is not None else self.atom_element, + x=atom_x if atom_x is not None else self.atom_x, + y=atom_y if atom_y is not None else self.atom_y, + z=atom_z if atom_z is not None else self.atom_z, + b_factor=( + atom_b_factor if atom_b_factor is not None else self.atom_b_factor + ), + occupancy=( + atom_occupancy + if atom_occupancy is not None + else self.atom_occupancy + ), + ) + return self.copy_and_update(atoms=new_atoms) + + def _cascade_delete( + self, + *, + chains: structure_tables.Chains | None = None, + residues: structure_tables.Residues | None = None, + atoms: structure_tables.Atoms | None = None, + bonds: structure_tables.Bonds | None = None, + ) -> StructureTables: + """Performs a cascade delete operation on the structure's tables. + + Cascade delete ensures all the tables are consistent after any table fields + are being updated by cascading any deletions down the hierarchy of tables: + chains > residues > atoms > bonds. + + E.g.: if a row from residues table is removed then all the atoms in that + residue will also be removed from the atoms table. In turn this cascades + also to the bond table, by removing any bond row which involves any of those + removed atoms. However the chains table will not be modified, even if + that was the only residue in its chain, because the chains table is above + the residues table in the hierarchy. + + Args: + chains: An optional new chains table. + residues: An optional new residues table. + atoms: An optional new atoms table. + bonds: An optional new bonds table. + + Returns: + A StructureTables object with the updated tables. + """ + if chains_unchanged := chains is None: + chains = self._chains + if residues_unchanged := residues is None: + residues = self._residues + if atoms_unchanged := atoms is None: + atoms = self._atoms + if bonds is None: + bonds = self._bonds + + if not chains_unchanged: + residues_mask = membership.isin(residues.chain_key, set( + chains.key)) # pylint:disable=attribute-error + if not np.all(residues_mask): # Only apply if this is not a no-op. + residues = residues[residues_mask] + residues_unchanged = False + if not residues_unchanged: + atoms_mask = membership.isin(atoms.res_key, set( + residues.key)) # pylint:disable=attribute-error + if not np.all(atoms_mask): # Only apply if this is not a no-op. + atoms = atoms[atoms_mask] + atoms_unchanged = False + if not atoms_unchanged: + bonds = bonds.restrict_to_atoms(atoms.key) + return StructureTables( + chains=chains, residues=residues, atoms=atoms, bonds=bonds + ) + + def filter( + self, + mask: np.ndarray | None = None, + *, + apply_per_element: bool = False, + invert: bool = False, + cascade_delete: CascadeDelete = CascadeDelete.CHAINS, + **predicate_by_field_name: table.FilterPredicate, + ) -> Self: + """Filters the structure by field values and returns a new structure. + + Predicates are specified as keyword arguments, with names following the + pattern: _, where table_name := (chain|res|atom). + For instance the auth_seq_id column in the residues table can be filtered + by passing `res_auth_seq_id=pred_value`. The full list of valid options + are defined in the `col_by_field_name` fields on the different Table + dataclasses. + + Predicate values can be either: + 1. A constant value, e.g. 'CA'. In this case then only rows that match + this value for the given field are retained. + 2. A (non-string) iterable e.g. ('A', 'B'). In this + case then rows are retained if they match any of the provided values for + the given field. + 3. A boolean function e.g. lambda b_fac: b_fac < 100.0. + In this case then only rows that evaluate to True are retained. By + default this function's parameter is expected to be an array, unless + apply_per_element=True. + + Example usage: + # Filter to backbone atoms in residues up to 100 in chain B. + filtered_struct = struct.filter( + chain_id='B', + atom_name=('N', 'CA', 'C'), + res_id=lambda res_id: res_id < 100) + + Example usage where predicate must be applied per-element: + # Filter to residues with IDs in either [1, 100) or [300, 400). + ranges = ((1, 100), (300, 400)) + filtered_struct = struct.filter( + res_id=lambda i: np.any([start <= i < end for start, end in ranges]), + apply_per_element=True) + + Example usage of providing a raw mask: + filtered_struct = struct.filter(struct.atom_b_factor < 10.0) + + Args: + mask: An optional boolean NumPy array with length equal to num_atoms. If + provided then this will be combined with the other predicates so that an + atom is included if it is masked-in *and* matches all the predicates. + apply_per_element: Whether apply predicates to each element individually, + or to pass the whole column array to the predicate. + invert: Whether to remove, rather than retain, the entities which match + the specified predicates. + cascade_delete: Whether to remove residues and chains which are left + unresolved in a cascade. filter operates on the atoms table, removing + atoms which match the predicate. If all atoms in a residue are removed, + the residue is "unresolved". The value of this argument then determines + whether such residues and their parent chains should be deleted. FULL + implies that all unresolved residues should be deleted, and any chains + which are left with no resolved residues should be deleted. CHAINS is + the default behaviour - only chains with no resolved residues, and their + child residues are deleted. Unresolved residues in partially resolved + chains remain. NONE implies that no unresolved residues or chains should + be deleted. + **predicate_by_field_name: A mapping from field name to a predicate. + Filtered columns must be 1D arrays. If multiple fields are provided as + keyword arguments then each predicate is applied and the results are + combined using a boolean AND operation, so an atom is only retained if + it passes all predicates. + + Returns: + A new structure representing a filtered version of the current structure. + + Raises: + ValueError: If mask is provided and is not a bool array with shape + (num_atoms,). + """ + chain_predicates, res_predicates, atom_predicates = ( + _unpack_filter_predicates(predicate_by_field_name) + ) + # Get boolean masks for each table. These are None if none of the filter + # parameters affect the table in question. + chain_mask = self._chains.make_filter_mask( + **chain_predicates, apply_per_element=apply_per_element + ) + res_mask = self._residues.make_filter_mask( + **res_predicates, apply_per_element=apply_per_element + ) + atom_mask = self._atoms.make_filter_mask( + mask, **atom_predicates, apply_per_element=apply_per_element + ) + if atom_mask is None: + atom_mask = np.ones((self._atoms.size,), dtype=bool) + + # Remove atoms that belong to filtered out chains. + if chain_mask is not None: + atom_chain_mask = membership.isin( + self._atoms.chain_key, set(self._chains.key[chain_mask]) + ) + np.logical_and(atom_mask, atom_chain_mask, out=atom_mask) + + # Remove atoms that belong to filtered out residues. + if res_mask is not None: + atom_res_mask = membership.isin( + self._atoms.res_key, set(self._residues.key[res_mask]) + ) + np.logical_and(atom_mask, atom_res_mask, out=atom_mask) + + final_atom_mask = ~atom_mask if invert else atom_mask + + if cascade_delete == CascadeDelete.NONE and np.all(final_atom_mask): + # Shortcut: The filter is a no-op, so just return itself. + return self + + filtered_atoms = typing.cast( + structure_tables.Atoms, self._atoms[final_atom_mask] + ) + + match cascade_delete: + case CascadeDelete.FULL: + nonempty_residues_mask = np.isin( + self._residues.key, filtered_atoms.res_key + ) + filtered_residues = self._residues[nonempty_residues_mask] + nonempty_chain_mask = np.isin( + self._chains.key, filtered_atoms.chain_key + ) + filtered_chains = self._chains[nonempty_chain_mask] + updated_tables = self._cascade_delete( + chains=filtered_chains, + residues=filtered_residues, + atoms=filtered_atoms, + ) + case CascadeDelete.CHAINS: + # To match v1 behavior we remove chains that have no atoms remaining, + # and we remove residues in those chains. + # NB we do not remove empty residues. + nonempty_chain_mask = membership.isin( + self._chains.key, set(filtered_atoms.chain_key) + ) + filtered_chains = self._chains[nonempty_chain_mask] + updated_tables = self._cascade_delete( + chains=filtered_chains, atoms=filtered_atoms + ) + case CascadeDelete.NONE: + updated_tables = self._cascade_delete(atoms=filtered_atoms) + case _: + raise ValueError( + f'Unknown cascade_delete behaviour: {cascade_delete}') + return self.copy_and_update( + chains=updated_tables.chains, + residues=updated_tables.residues, + atoms=updated_tables.atoms, + bonds=updated_tables.bonds, + skip_validation=True, + ) + + def filter_out(self, *args, **kwargs) -> Self: + """Returns a new structure with the specified elements removed.""" + return self.filter(*args, invert=True, **kwargs) + + def filter_to_entity_type( + self, + *, + protein: bool = False, + rna: bool = False, + dna: bool = False, + dna_rna_hybrid: bool = False, + ligand: bool = False, + water: bool = False, + ) -> Self: + """Filters the structure to only include the selected entity types. + + This convenience method abstracts away the specifics of mmCIF entity + type names which, especially for ligands, are non-trivial. + + Args: + protein: Whether to include protein (polypeptide(L)) chains. + rna: Whether to include RNA chains. + dna: Whether to include DNA chains. + dna_rna_hybrid: Whether to include DNA RNA hybrid chains. + ligand: Whether to include ligand (i.e. not polymer) chains. + water: Whether to include water chains. + + Returns: + The filtered structure. + """ + include_types = [] + if protein: + include_types.append(mmcif_names.PROTEIN_CHAIN) + if rna: + include_types.append(mmcif_names.RNA_CHAIN) + if dna: + include_types.append(mmcif_names.DNA_CHAIN) + if dna_rna_hybrid: + include_types.append(mmcif_names.DNA_RNA_HYBRID_CHAIN) + if ligand: + include_types.extend(mmcif_names.LIGAND_CHAIN_TYPES) + if water: + include_types.append(mmcif_names.WATER) + return self.filter(chain_type=include_types) + + def get_stoichiometry( + self, *, fix_non_standard_polymer_res: bool = False + ) -> Sequence[int]: + """Returns the structure's stoichiometry using chain_res_name_sequence. + + Note that everything is considered (protein, RNA, DNA, ligands) except for + water molecules. If you are interested only in a certain type of entities, + filter them out before calling this method. + + Args: + fix_non_standard_polymer_res: If True, maps non standard residues in + protein / RNA / DNA chains to standard residues (e.g. MSE -> MET) or UNK + / N if a match is not found. + + Returns: + A list of integers, one for each unique chain in the structure, + determining the number of that chain appearing in the structure. The + numbers are sorted highest to lowest. E.g. for an A3B2 protein this method + will return [3, 2]. + """ + filtered = self.filter_to_entity_type( + protein=True, + rna=True, + dna=True, + dna_rna_hybrid=True, + ligand=True, + water=False, + ) + seqs = filtered.chain_res_name_sequence( + include_missing_residues=True, + fix_non_standard_polymer_res=fix_non_standard_polymer_res, + ) + + unique_seq_counts = collections.Counter(seqs.values()) + return sorted(unique_seq_counts.values(), reverse=True) + + def without_hydrogen(self) -> Self: + """Returns the structure without hydrogen atoms.""" + return self.filter( + np.logical_and(self._atoms.element != 'H', + self._atoms.element != 'D') + ) + + def without_terminal_oxygens(self) -> Self: + """Returns the structure without terminal oxygen atoms.""" + terminal_oxygen_filter = np.zeros(self.num_atoms, dtype=bool) + for chain_type, atom_name in mmcif_names.TERMINAL_OXYGENS.items(): + chain_keys = self._chains.key[self._chains.type == chain_type] + chain_atom_filter = np.logical_and( + self._atoms.name == atom_name, + np.isin(self._atoms.chain_key, chain_keys), + ) + np.logical_or( + terminal_oxygen_filter, chain_atom_filter, out=terminal_oxygen_filter + ) + return self.filter_out(terminal_oxygen_filter) + + def reset_author_naming_scheme(self) -> Self: + """Remove author chain/residue ids, entity info and use internal ids.""" + new_chains = structure_tables.Chains( + key=self._chains.key, + id=self._chains.id, + type=self._chains.type, + auth_asym_id=self._chains.id, + entity_id=np.arange(1, self.num_chains + + 1).astype(str).astype(object), + entity_desc=np.full(self.num_chains, '.', dtype=object), + ) + new_residues = structure_tables.Residues( + key=self._residues.key, + chain_key=self._residues.chain_key, + id=self._residues.id, + name=self._residues.name, + auth_seq_id=self._residues.id.astype(str).astype(object), + insertion_code=np.full( + self.num_residues(count_unresolved=True), '?', dtype=object + ), + ) + return self.copy_and_update( + chains=new_chains, residues=new_residues, skip_validation=True + ) + + def filter_residues(self, res_mask: np.ndarray) -> Self: + """Filter resolved residues using a boolean mask.""" + required_shape = (self.num_residues(count_unresolved=False),) + if res_mask.shape != required_shape: + raise ValueError( + f'res_mask must have shape {required_shape}. Got: {res_mask.shape}.' + ) + if res_mask.dtype != bool: + raise ValueError( + f'res_mask must have dtype bool. Got: {res_mask.dtype}.') + + filtered_residues = self.present_residues.filter(res_mask) + atom_mask = np.isin(self._atoms.res_key, filtered_residues.key) + return self.filter(atom_mask) + + def filter_coords( + self, coord_predicate: Callable[[np.ndarray], bool] + ) -> Self: + """Filter a structure's atoms by a function of their coordinates. + + Args: + coord_predicate: A boolean function of coordinate vectors (shape (3,)). + + Returns: + A Structure filtered so that only atoms with coords passing the predicate + function are present. + + Raises: + ValueError: If the coords are not shaped (num_atom, 3). + """ + coords = self.coords + if coords.ndim != 2 or coords.shape[-1] != 3: + raise ValueError( + f'coords should have shape (num_atom, 3). Got {coords.shape}.' + ) + mask = np.vectorize(coord_predicate, signature='(n)->()')(coords) + # This use of _apply_atom_index_array is safe because a boolean mask is + # used, which means the chain/residue/atom ordering will stay unchanged. + return self._apply_atom_index_array(mask, skip_validation=True) + + def filter_polymers_to_single_atom_per_res( + self, + representative_atom_by_chain_type: Mapping[ + str, str + ] = mmcif_names.RESIDUE_REPRESENTATIVE_ATOMS, + ) -> Self: + """Filter to one representative atom per polymer residue, ligands unchanged. + + Args: + representative_atom_by_chain_type: Chain type str to atom name, only atoms + with this name will be kept for this chain type. Chains types from the + structure not found in this mapping will keep all their atoms. + + Returns: + A Structure filtered so that per chain types, only specified atoms are + present. + """ + polymer_chain_keys = self._chains.key[ + string_array.isin( + self._chains.type, set(representative_atom_by_chain_type) + ) + ] + polymer_atoms_mask = np.isin(self._atoms.chain_key, polymer_chain_keys) + + wanted_atom_by_chain_key = { + chain_key: representative_atom_by_chain_type.get(chain_type, None) + for chain_key, chain_type in zip(self._chains.key, self._chains.type) + } + wanted_atoms = string_array.remap( + self._atoms.chain_key.astype(object), mapping=wanted_atom_by_chain_key + ) + + representative_polymer_atoms_mask = polymer_atoms_mask & ( + wanted_atoms == self._atoms.name + ) + + return self.filter(representative_polymer_atoms_mask | ~polymer_atoms_mask) + + def drop_non_standard_protein_atoms(self, *, drop_oxt: bool = True) -> Self: + """Drops non-standard atom names from protein chains. + + Args: + drop_oxt: If True, also drop terminal oxygens (OXT). + + Returns: + A new Structure object where the protein chains have been filtered to + only contain atoms with names listed in `atom_types` + (including OXT unless `drop_oxt` is `True`). Non-protein chains are + unaltered. + """ + allowed_names = set(atom_types.ATOM37) + if drop_oxt: + allowed_names = {n for n in allowed_names if n != atom_types.OXT} + + return self.filter_out( + chain_type=mmcif_names.PROTEIN_CHAIN, + atom_name=lambda n: string_array.isin( + n, allowed_names, invert=True), + ) + + def drop_non_standard_atoms( + self, + *, + ccd: chemical_components.Ccd, + drop_unk: bool, + drop_non_ccd: bool, + drop_terminal_oxygens: bool = False, + ) -> Self: + """Drops atoms that are not in the CCD for the given residue type.""" + + # We don't remove any atoms in UNL, as it has no standard atoms. + def _keep(atom_index: int) -> bool: + atom_name = self._atoms.name[atom_index] + res_name = self._residues.name[ + self._residues.index_by_key[self._atoms.res_key[atom_index]] + ] + if drop_unk and res_name in residue_names.UNKNOWN_TYPES: + return False + else: + return ( + (not drop_non_ccd and not ccd.get(res_name)) + or atom_name in struct_chem_comps.get_res_atom_names(ccd, res_name) + or res_name == residue_names.UNL + ) + + standard_atom_mask = np.array( + [_keep(atom_i) for atom_i in range(self.num_atoms)], dtype=bool + ) + standard_atoms = self.filter(mask=standard_atom_mask) + if drop_terminal_oxygens: + standard_atoms = standard_atoms.without_terminal_oxygens() + return standard_atoms + + def find_chains_with_unknown_sequence(self) -> Sequence[str]: + """Returns a sequence of chain IDs that contain only unknown residues.""" + unknown_sequences = [] + for start, end in self.iter_chain_ranges(): + try: + unknown_id = residue_names.UNKNOWN_TYPES.index( + self.res_name[start]) + if start + 1 == end or np.all( + self.res_name[start + 1: end] + == residue_names.UNKNOWN_TYPES[unknown_id] + ): + unknown_sequences.append(self.chain_id[start]) + except ValueError: + pass + return unknown_sequences + + def add_bonds( + self, + bonded_atom_pairs: Sequence[ + tuple[tuple[str, int, str], tuple[str, int, str]], + ], + bond_type: str | None = None, + ) -> Self: + """Returns a structure with new bonds added. + + Args: + bonded_atom_pairs: A sequence of pairs of atoms, with one pair per bond. + Each element of the pair is a tuple of (chain_id, res_id, atom_name), + matching values from the respective fields of this structure. The first + element is the start atom, and the second atom is the end atom of the + bond. + bond_type: This type will be used for all bonds in the structure, where + type follows PDB scheme, e.g. unknown (?), hydrog, metalc, covale, + disulf. + + Returns: + A copy of this structure with the new bonds added. If this structure has + bonds already then the new bonds are concatenated onto the end of the + old bonds. NB: bonds are not deduplicated. + """ + atom_key_lookup: dict[tuple[str, str, None, str], int] = dict( + zip(self.atom_ids, self._atoms.key, strict=True) + ) + + # iter_atoms returns a 4-tuple (chain_id, res_id, ins_code, atom_name) but + # the insertion code is always None. It also uses string residue IDs. + def _to_internal_res_id( + bonded_atom_id: tuple[str, int, str], + ) -> tuple[str, str, None, str]: + return bonded_atom_id[0], str(bonded_atom_id[1]), None, bonded_atom_id[2] + + from_atom_key = [] + dest_atom_key = [] + for from_atom, dest_atom in bonded_atom_pairs: + from_atom_key.append( + atom_key_lookup[_to_internal_res_id(from_atom)]) + dest_atom_key.append( + atom_key_lookup[_to_internal_res_id(dest_atom)]) + num_bonds = len(bonded_atom_pairs) + bonds_key = np.arange(num_bonds, dtype=np.int64) + from_atom_key = np.array(from_atom_key, dtype=np.int64) + dest_atom_key = np.array(dest_atom_key, dtype=np.int64) + all_unk_col = np.array(['?'] * num_bonds, dtype=object) + if bond_type is None: + bond_type_col = all_unk_col + else: + bond_type_col = np.full((num_bonds,), bond_type, dtype=object) + + max_key = -1 if not self._bonds.size else np.max(self._bonds.key) + new_bonds = structure_tables.Bonds( + key=np.concatenate([self._bonds.key, bonds_key + max_key + 1]), + from_atom_key=np.concatenate( + [self._bonds.from_atom_key, from_atom_key] + ), + dest_atom_key=np.concatenate( + [self._bonds.dest_atom_key, dest_atom_key] + ), + type=np.concatenate([self._bonds.type, bond_type_col]), + role=np.concatenate([self._bonds.role, all_unk_col]), + ) + return self.copy_and_update(bonds=new_bonds) + + @property + def coords(self) -> np.ndarray: + """A [..., num_atom, 3] shaped array of atom coordinates.""" + return np.stack([self._atoms.x, self._atoms.y, self._atoms.z], axis=-1) + + def chain_single_letter_sequence( + self, include_missing_residues: bool = True + ) -> Mapping[str, str]: + """Returns a mapping from chain ID to a single letter residue sequence. + + Args: + include_missing_residues: Whether to include residues that have no atoms. + """ + res_table = ( + self._residues if include_missing_residues else self.present_residues + ) + residue_chain_boundaries = _get_change_indices(res_table.chain_key) + boundaries = self._iter_residue_ranges( + residue_chain_boundaries, + count_unresolved=include_missing_residues, + ) + chain_keys = res_table.chain_key[residue_chain_boundaries] + chain_ids = self._chains.apply_array_to_column('id', chain_keys) + chain_types = self._chains.apply_array_to_column('type', chain_keys) + chain_seqs = {} + for idx, (start, end) in enumerate(boundaries): + chain_id = chain_ids[idx] + chain_type = chain_types[idx] + chain_res = res_table.name[start:end] + if chain_type in mmcif_names.PEPTIDE_CHAIN_TYPES: + unknown_default = 'X' + elif chain_type in mmcif_names.NUCLEIC_ACID_CHAIN_TYPES: + unknown_default = 'N' + else: + chain_seqs[chain_id] = 'X' * chain_res.size + continue + + chain_res = string_array.remap( + chain_res, + mapping=residue_names.CCD_NAME_TO_ONE_LETTER, + inplace=False, + default_value=unknown_default, + ) + chain_seqs[chain_id] = ''.join(chain_res.tolist()) + + return chain_seqs + + def polymer_auth_asym_id_to_label_asym_id( + self, + *, + protein: bool = True, + rna: bool = True, + dna: bool = True, + other: bool = True, + ) -> Mapping[str, str]: + """Mapping from author chain ID to internal chain ID, polymers only. + + This mapping is well defined only for polymers (protein, DNA, RNA), but not + for ligands or water. + + E.g. if a structure had the following internal chain IDs (label_asym_id): + A (protein), B (DNA), C (ligand bound to A), D (ligand bound to A), + E (ligand bound to B). + + Such structure would have this internal chain ID (label_asym_id) -> author + chain ID (auth_asym_id) mapping: + A -> A, B -> B, C -> A, D -> A, E -> B + + This is a bijection only for polymers (A, B), but not for ligands. + + Args: + protein: Whether to include protein (polypeptide(L)) chains. + rna: Whether to include RNA chains. + dna: Whether to include DNA chains. + other: Whether to include other polymer chains, e.g. RNA/DNA hybrid or + polypeptide(D). Note that include_other=True must be set in from_mmcif. + + Returns: + A mapping from author chain ID to the internal (label) chain ID for the + given polymer types in the Structure, ligands/water are ignored. + + Raises: + ValueError: If the mapping from internal chain IDs to author chain IDs is + not a bijection for polymer chains. + """ + allowed_types = set() + if protein: + allowed_types.add(mmcif_names.PROTEIN_CHAIN) + if rna: + allowed_types.add(mmcif_names.RNA_CHAIN) + if dna: + allowed_types.add(mmcif_names.DNA_CHAIN) + if other: + non_standard_chain_types = ( + mmcif_names.POLYMER_CHAIN_TYPES + - mmcif_names.STANDARD_POLYMER_CHAIN_TYPES + ) + allowed_types |= non_standard_chain_types + + auth_asym_id_to_label_asym_id = {} + for chain in self.iter_chains(): + if chain['chain_type'] not in allowed_types: + continue + label_asym_id = chain['chain_id'] + auth_asym_id = chain['chain_auth_asym_id'] + # The mapping from author chain id to label chain id is only one-to-one if + # we restrict our attention to polymers. But check nevertheless. + if auth_asym_id in auth_asym_id_to_label_asym_id: + raise ValueError( + f'Author chain ID "{auth_asym_id}" does not have a unique mapping ' + f'to internal chain ID "{label_asym_id}", it is already mapped to ' + f'"{auth_asym_id_to_label_asym_id[auth_asym_id]}".' + ) + auth_asym_id_to_label_asym_id[auth_asym_id] = label_asym_id + + return auth_asym_id_to_label_asym_id + + def polymer_author_chain_single_letter_sequence( + self, + *, + include_missing_residues: bool = True, + protein: bool = True, + rna: bool = True, + dna: bool = True, + other: bool = True, + ) -> Mapping[str, str]: + """Mapping from author chain ID to single letter aa sequence, polymers only. + + This mapping is well defined only for polymers (protein, DNA, RNA), but not + for ligands or water. + + Args: + include_missing_residues: If True then all residues will be returned for + each polymer chain present in the structure. This uses the all_residues + field and will include residues missing due to filtering operations as + well as e.g. unresolved residues specified in an mmCIF header. + protein: Whether to include protein (polypeptide(L)) chains. + rna: Whether to include RNA chains. + dna: Whether to include DNA chains. + other: Whether to include other polymer chains, e.g. RNA/DNA hybrid or + polypeptide(D). Note that include_other=True must be set in from_mmcif. + + Returns: + A mapping from (author) chain IDs to their single-letter sequences for all + polymers in the Structure, ligands/water are ignored. + + Raises: + ValueError: If the mapping from internal chain IDs to author chain IDs is + not a bijection for polymer chains. + """ + label_chain_id_to_seq = self.chain_single_letter_sequence( + include_missing_residues=include_missing_residues + ) + auth_to_label = self.polymer_auth_asym_id_to_label_asym_id( + protein=protein, rna=rna, dna=dna, other=other + ) + return { + auth: label_chain_id_to_seq[label] + for auth, label in auth_to_label.items() + } + + def chain_res_name_sequence( + self, + *, + include_missing_residues: bool = True, + fix_non_standard_polymer_res: bool = False, + ) -> Mapping[str, Sequence[str]]: + """A mapping from internal chain ID to a sequence of residue names. + + The residue names are the full residue names rather than single letter + codes. For instance, for proteins these are the 3 letter CCD codes. + + Args: + include_missing_residues: Whether to include residues with no atoms in the + returned sequences. + fix_non_standard_polymer_res: Whether to map non standard residues in + protein / RNA / DNA chains to standard residues (e.g. MSE -> MET) or UNK + / N if a match is not found. + + Returns: + A mapping from (internal) chain IDs to a sequence of residue names. + """ + res_table = ( + self._residues if include_missing_residues else self.present_residues + ) + residue_chain_boundaries = _get_change_indices(res_table.chain_key) + boundaries = self._iter_residue_ranges( + residue_chain_boundaries, count_unresolved=include_missing_residues + ) + chain_keys = res_table.chain_key[residue_chain_boundaries] + chain_ids = self._chains.apply_array_to_column('id', chain_keys) + chain_types = self._chains.apply_array_to_column('type', chain_keys) + chain_seqs = {} + for idx, (start, end) in enumerate(boundaries): + chain_id = chain_ids[idx] + chain_type = chain_types[idx] + chain_res = res_table.name[start:end] + if ( + fix_non_standard_polymer_res + and chain_type in mmcif_names.POLYMER_CHAIN_TYPES + ): + chain_seqs[chain_id] = tuple( + fix_non_standard_polymer_residues( + res_names=chain_res, chain_type=chain_type + ) + ) + else: + chain_seqs[chain_id] = tuple(chain_res) + + return chain_seqs + + def fix_non_standard_polymer_res( + self, + res_mapper: Callable[ + [np.ndarray, str], np.ndarray + ] = fix_non_standard_polymer_residues, + ) -> Self: + """Replaces non-standard polymer residues with standard alternatives or UNK. + + e.g. maps 'ACE' -> 'UNK', 'MSE' -> 'MET'. + + NB: Only fixes the residue names, but does not fix the atom names. + E.g., 'MSE' will be renamed to 'MET' but its 'SE' atom will not be renamed + to 'S'. Fixing MSE should be done during conversion from mmcif with the + `fix_mse_residues` flag. + + Args: + res_mapper: An optional function that accepts a numpy array of residue + names and chain_type, and returns an array with fixed res_names. This + defaults to fix_non_standard_polymer_residues. + + Returns: + A Structure containing only standard residue types (or 'UNK') in its + polymer chains. + """ + fixed_res_name = self._residues.name.copy() + chain_change_indices = _get_change_indices(self._residues.chain_key) + for start, end in self._iter_atom_ranges(chain_change_indices): + chain_key = self._residues.chain_key[start] + chain_type = self._chains.type[self._chains.index_by_key[chain_key]] + if chain_type not in mmcif_names.POLYMER_CHAIN_TYPES: + continue # We don't need to change anything for non-polymers. + fixed_res_name[start:end] = res_mapper( + fixed_res_name[start:end], chain_type + ) + fixed_residues = self._residues.copy_and_update(name=fixed_res_name) + return self.copy_and_update(residues=fixed_residues, skip_validation=True) + + @property + def slice_leading_dims(self) -> '_LeadingDimSlice': + """Used to create a new Structure by slicing into the leading dimensions. + + Example usage 1: + + ``` + final_state = multi_state_struct.slice_leading_dims[-1] + ``` + + Example usage 2: + + ``` + # Structure has leading batch and time dimensions. + # Get final 3 time frames from first two batch elements. + sliced_strucs = batched_trajectories.slice_leading_dims[:2, -3:] + ``` + """ + return _LeadingDimSlice(self) + + def unstack(self, axis: int = 0) -> Sequence[Self]: + """Unstacks a multi-model structure into a list of Structures. + + This method is the inverse of `stack`. + + Example usage: + ``` + structs = multi_dim_struct.unstack(axis=0) + ``` + + Args: + axis: The axis to unstack over. The structures in the returned list won't + have this axis in their coordinate of b-factor fields. + + Returns: + A list of `Structure`s with length equal to the size of the specified + axis in the coordinate field arrays. + + Raises: + IndexError: If axis does not refer to one of the leading dimensions of + `self.atoms_table.size`. + """ + ndim = self._atoms.ndim + if not (-ndim <= axis < ndim): + raise IndexError( + f'{axis=} is out of range for atom coordinate fields with {ndim=}.' + ) + elif axis < 0: + axis += ndim + if axis == ndim - 1: + raise IndexError( + 'axis must refer to one of the leading dimensions, not the final ' + f'dimension. The atom fields have {ndim=} and {axis=} was specified.' + ) + unstacked = [] + leading_dim_slice = self.slice_leading_dims # Compute once here. + for i in range(self._atoms.shape[axis]): + slice_i = (slice(None),) * axis + (i,) + unstacked.append(leading_dim_slice[slice_i]) + return unstacked + + def split_by_chain(self) -> Sequence[Self]: + """Splits a Structure into single-chain Structures, one for each chain. + + The obtained structures can be merged back together into the original + structure using the `concat` function. + + Returns: + A list of `Structure`s, one for each chain. The order is the same as the + chain order in the original Structure. + """ + return [self.filter(chain_id=chain_id) for chain_id in self.chains] + + def transform_states_to_chains(self) -> Self: + """Transforms states to chains. + + A multi-state protein structure will be transformed to a multi-chain + single-state protein structure. Useful for visualising multiples states to + examine diversity. This structure's coordinate fields must have shape + `(num_states, num_atoms)`. + + Returns: + A new `Structure`, based on this structure, but with the multiple states + now represented as `num_states * num_chains` chains in a + single-state protein. + + Raises: + ValueError: If this structure's array fields don't have shape + `(num_states, num_atoms)`. + """ + if self._atoms.ndim != 2: + raise ValueError( + 'Coordinate field tensor must have 2 dimensions: ' + f'(num_states, num_atoms), got {self._atoms.ndim}.' + ) + return concat(self.unstack(axis=0)) + + def merge_chains( + self, + *, + chain_groups: Sequence[Sequence[str]], + chain_group_ids: Sequence[str] | None = None, + chain_group_types: Sequence[str] | None = None, + ) -> Self: + """Merges chains in each group into a single chain. + + If a Structure has chains A, B, C, D, E, and + `merge_chains([[A, C], [B, D], [E]])` is called, the new Structure will have + 3 chains A, B, C, the first being concatenation of A+C, the second B+D, the + third just the original chain E. + + Args: + chain_groups: Each group defines what chains should be merged into a + single chain. The output structure will therefore have len(chain_groups) + chains. Residue IDs are renumbered to preserve uniqueness within new + chains. Order of chain groups and within each group matters. + chain_group_ids: Optional sequence of new chain IDs for each group. If not + given, the new internal chain IDs (label_asym_id) are assigned in the + standard mmCIF order (i.e. A, B, ..., Z, AA, BA, CA, ...). Author chain + names (auth_asym_id) are set to be equal to the new internal chain IDs. + chain_group_types: Optional sequence of new chain types for each group. If + not given, only chains with the same type can be merged. + + Returns: + A new `Structure` with chains merged together into a single chain within + each chain group. + + Raises: + ValueError: If chain_group_ids or chain_group_types are given but don't + match the length of chain_groups. + ValueError: If the chain IDs in the flattened chain_groups don't match the + chain IDs in the Structure. + ValueError: If chains in any of the groups don't have the same chain type. + """ + if chain_group_ids and len(chain_group_ids) != len(chain_groups): + raise ValueError( + 'chain_group_ids must the same length as chain_groups: ' + f'{len(chain_group_ids)=} != {len(chain_groups)=}' + ) + if chain_group_types and len(chain_group_types) != len(chain_groups): + raise ValueError( + 'chain_group_types must the same length as chain_groups: ' + f'{len(chain_group_types)=} != {len(chain_groups)=}' + ) + flattened = sorted(itertools.chain.from_iterable(chain_groups)) + if flattened != sorted(self.chains): + raise ValueError( + 'IDs in chain groups do not match Structure chain IDs: ' + f'{chain_groups=}, chains={self.chains}' + ) + + new_chain_key_by_chain_id = {} + for new_chain_key, group_chain_ids in enumerate(chain_groups): + for chain_id in group_chain_ids: + new_chain_key_by_chain_id[chain_id] = new_chain_key + + chain_key_remap = {} + new_chain_type_by_chain_key = {} + for old_chain_key, old_chain_id, old_chain_type in zip( + self._chains.key, self._chains.id, self._chains.type + ): + new_chain_key = new_chain_key_by_chain_id[old_chain_id] + chain_key_remap[old_chain_key] = new_chain_key + + if new_chain_key not in new_chain_type_by_chain_key: + new_chain_type_by_chain_key[new_chain_key] = old_chain_type + elif not chain_group_types: + if new_chain_type_by_chain_key[new_chain_key] != old_chain_type: + bad_types = [ + f'{cid}: {self._chains.type[np.where(self._chains.id == cid)][0]}' + for cid in chain_groups[new_chain_key] + ] + raise ValueError( + 'Inconsistent chain types within group:\n' + + '\n'.join(bad_types) + ) + + new_chain_key = np.arange(len(chain_groups), dtype=np.int64) + if chain_group_ids: + new_chain_id = np.array(chain_group_ids, dtype=object) + else: + new_chain_id = np.array( + [mmcif.int_id_to_str_id(k) for k in new_chain_key + 1], dtype=object + ) + if chain_group_types: + new_chain_type = np.array(chain_group_types, dtype=object) + else: + new_chain_type = np.array( + [new_chain_type_by_chain_key[k] for k in new_chain_key], dtype=object + ) + new_chains = structure_tables.Chains( + key=new_chain_key, + id=new_chain_id, + type=new_chain_type, + auth_asym_id=new_chain_id, + entity_id=np.char.mod('%d', new_chain_key + 1).astype(object), + entity_desc=np.full(len(chain_groups), + fill_value='.', dtype=object), + ) + + # Remap chain keys and sort residues to match the chain table order. + new_residues = self._residues.copy_and_remap(chain_key=chain_key_remap) + new_residues = new_residues.apply_index( + np.argsort(new_residues.chain_key, kind='stable') + ) + # Renumber uniquely residues in each chain. + indices = np.arange(new_residues.chain_key.size, dtype=np.int32) + new_res_ids = (indices + 1) - np.maximum.accumulate( + indices * (new_residues.chain_key != + np.roll(new_residues.chain_key, 1)) + ) + new_residues = new_residues.copy_and_update(id=new_res_ids) + + # Remap chain keys and sort atoms to match the chain table order. + new_atoms = self._atoms.copy_and_remap(chain_key=chain_key_remap) + new_atoms = new_atoms.apply_index( + np.argsort(new_atoms.chain_key, kind='stable') + ) + + return self.copy_and_update( + chains=new_chains, + residues=new_residues, + atoms=new_atoms, + bonds=self._bonds, + ) + + def to_res_arrays( + self, + *, + include_missing_residues: bool, + atom_order: Mapping[str, int] = atom_types.ATOM37_ORDER, + ) -> tuple[np.ndarray, np.ndarray]: + """Returns an atom position and atom mask array with a num_res dimension. + + NB: All residues in the structure will appear in the residue + dimension but atoms will only have a True (1.0) mask value if + they are defined in `atom_order`. + + Args: + include_missing_residues: If True then the res arrays will include rows + for missing residues where all atoms will be masked out. Otherwise these + will simply be skipped. + atom_order: Atom order mapping atom names to their index in the atom + dimension of the returned arrays. Default is atom_order for proteins, + choose atom_types.ATOM29_ORDER for nucleics. + + Returns: + A pair of arrays: + * atom_positions: [num_res, atom_type_num, 3] float32 array of coords. + * atom_mask: [num_res, atom_type_num] float32 atom mask denoting + which atoms are present in this Structure. + """ + num_res = self.num_residues(count_unresolved=include_missing_residues) + atom_type_num = len(atom_order) + atom_positions = np.zeros( + (num_res, atom_type_num, 3), dtype=np.float32) + atom_mask = np.zeros((num_res, atom_type_num), dtype=np.float32) + + all_residues = None if not include_missing_residues else self.all_residues + for i, atom in enumerate_residues(self.iter_atoms(), all_residues): + atom_idx = atom_order.get(atom['atom_name']) + if atom_idx is not None: + atom_positions[i, atom_idx, 0] = atom['atom_x'] + atom_positions[i, atom_idx, 1] = atom['atom_y'] + atom_positions[i, atom_idx, 2] = atom['atom_z'] + atom_mask[i, atom_idx] = 1.0 + + return atom_positions, atom_mask + + def to_res_atom_lists( + self, *, include_missing_residues: bool + ) -> Sequence[Sequence[Mapping[str, Any]]]: + """Returns list of atom dictionaries grouped by residue. + + If this is a multi-model structure, each atom will store its fields + atom_x, atom_y, atom_z, and atom_b_factor as Numpy arrays of shape of the + leading dimension(s). If this is a single-mode structure, these fields will + just be scalars. + + Args: + include_missing_residues: If True, then the output list will contain an + empty list of atoms for missing residues. Otherwise missing residues + will simply be skipped. + + Returns: + A list of size `num_res`. Each element in the list represents atoms of one + residue. If a residue is present is present, the list will contain an atom + dictionary for every atom present in that residue. If a residue is missing + and `include_missing_residues=True`, the list for that missing residue + will be empty. + """ + num_res = self.num_residues(count_unresolved=include_missing_residues) + residue_atoms = [[] for _ in range(num_res)] + all_residues = None if not include_missing_residues else self.all_residues + + # We could yield directly in this loop but the code would be more complex. + # Let's optimise if memory usage is an issue. + for res_index, atom in enumerate_residues(self.iter_atoms(), all_residues): + residue_atoms[res_index].append(atom) + + return residue_atoms + + def reorder_chains(self, new_order: Sequence[str]) -> Self: + """Reorders tables so that the label_asym_ids are in the given order. + + This method changes the order of the chains, residues, and atoms tables so + that they are all consistent with each other. Moreover, it remaps chain keys + so that they stay monotonically increasing in chains/residues/atoms tables. + + Args: + new_order: The order in which the chain IDs (label_asym_id) should be. + This must be a permutation of the current chain IDs. + + Returns: + A structure with chains reordered. + """ + if len(new_order) != len(self.chains): + raise ValueError( + f'The new number of chains ({len(new_order)}) does not match the ' + f'current number of chains ({len(self.chains)}).' + ) + new_chain_set = set(new_order) + if len(new_chain_set) != len(new_order): + raise ValueError( + f'The new order {new_order} contains non-unique IDs.') + if new_chain_set.symmetric_difference(set(self.chains)): + raise ValueError( + f'New chain IDs {new_order} do not match the old {set(self.chains)}' + ) + + if self.chains == tuple(new_order): + # Shortcut: the new order is the same as the current one. + return self + + desired_chain_id_pos = {chain_id: i for i, + chain_id in enumerate(new_order)} + + current_chain_index_order = np.empty(self.num_chains, dtype=np.int64) + for index, old_chain_id in enumerate(self._chains.id): + current_chain_index_order[index] = desired_chain_id_pos[old_chain_id] + chain_reorder = np.argsort(current_chain_index_order, kind='stable') + chain_key_map = dict( + zip(self._chains.key[chain_reorder], range(self.num_chains)) + ) + chains = self._chains.apply_index(chain_reorder) + chains = chains.copy_and_remap(key=chain_key_map) + + # The stable sort keeps the original residue ordering within each chain. + residues = self._residues.copy_and_remap(chain_key=chain_key_map) + residue_reorder = np.argsort(residues.chain_key, kind='stable') + residues = residues.apply_index(residue_reorder) + + # The stable sort keeps the original atom ordering within each chain. + atoms = self._atoms.copy_and_remap(chain_key=chain_key_map) + atoms_reorder = np.argsort(atoms.chain_key, kind='stable') + atoms = atoms.apply_index(atoms_reorder) + + # Bonds unchanged - each references 2 atom keys, hence ordering not defined. + return self.copy_and_update(chains=chains, residues=residues, atoms=atoms) + + def rename_auth_asym_ids(self, new_id_by_old_id: Mapping[str, str]) -> Self: + """Returns a new structure with renamed auth_asym_ids. + + Args: + new_id_by_old_id: A mapping from original auth_asym_ids to their new + values. Any auth_asym_ids in this structure that are not in the mapping + will remain unchanged. + + Raises: + ValueError: If any two previously distinct polymer chains do not have + unique names anymore after the rename. + """ + mapped_chains = self._chains.copy_and_remap( + auth_asym_id=new_id_by_old_id) + mapped_polymer_ids = mapped_chains.filter( + type=mmcif_names.POLYMER_CHAIN_TYPES + ).auth_asym_id + if len(mapped_polymer_ids) != len(set(mapped_polymer_ids)): + raise ValueError( + 'The new polymer auth_asym_ids are not unique:' + f' {sorted(mapped_polymer_ids)}.' + ) + return self.copy_and_update(chains=mapped_chains, skip_validation=True) + + def rename_chain_ids(self, new_id_by_old_id: Mapping[str, str]) -> Self: + """Returns a new structure with renamed chain IDs (label_asym_ids). + + The chains' auth_asym_ids will be updated to be identical to the chain ID + since there isn't one unambiguous way to maintain the auth_asym_ids after + renaming the chain IDs (depending on whether you view the auth_asym_id as + more strongly associated with a given physical chain, or with a given + chain ID). + + The residues' auth_seq_id will be updated to be identical to the residue ID + since they are strongly tied to the original author chain naming and keeping + them would be misleading. + + Args: + new_id_by_old_id: A mapping from original chain ID to their new values. + Any chain IDs in this structure that are not in this mapping will remain + unchanged. + + Returns: + A new structure with renamed chains (and bioassembly data if it is + present). + + Raises: + ValueError: If any two previously distinct chains do not have unique names + anymore after the rename. + """ + new_chain_id = string_array.remap(self._chains.id, new_id_by_old_id) + if len(new_chain_id) != len(set(new_chain_id)): + raise ValueError( + f"New chain names aren't unique: {sorted(new_chain_id)}") + + # Map label_asym_ids in the bioassembly data. + if self._bioassembly_data is None: + new_bioassembly_data = None + else: + new_bioassembly_data = self._bioassembly_data.rename_label_asym_ids( + new_id_by_old_id, present_chains=set(self.present_chains.id) + ) + + # Set author residue IDs to be the string version of internal residue IDs. + new_residues = self._residues.copy_and_update( + auth_seq_id=self._residues.id.astype(str).astype(object) + ) + + new_chains = self._chains.copy_and_update( + id=new_chain_id, auth_asym_id=new_chain_id + ) + + return self.copy_and_update( + bioassembly_data=new_bioassembly_data, + chains=new_chains, + residues=new_residues, + skip_validation=True, + ) + + @functools.cached_property + def chains(self) -> tuple[str, ...]: + """Ordered internal chain IDs (label_asym_id) present in the Structure.""" + return tuple(self._chains.id) + + def rename_res_name( + self, + res_name_map: Mapping[str, str], + fail_if_not_found: bool = True, + ) -> Self: + """Returns a copy of this structure with residues renamed. + + Residue names in chemical components data will also be renamed. + + Args: + res_name_map: A mapping from old residue names to new residue names. Any + residues that are not in this mapping will be left unchanged. + fail_if_not_found: Whether to fail if keys in the res_name_map mapping are + not found in this structure's residues' `name` column. + + Raises: + ValueError: If `fail_if_not_found=True` and a residue name isn't found in + the residues table's `name` field. + """ + res_name_set = set(self._residues.name) + if fail_if_not_found: + for res_name in res_name_map: + if res_name not in res_name_set: + raise ValueError( + f'"{res_name}" not found in this structure.') + new_residues = self._residues.copy_and_remap(name=res_name_map) + + if self._chemical_components_data is not None: + chem_comp = { + res_name_map.get(res_name, res_name): data + for res_name, data in self._chemical_components_data.chem_comp.items() + } + new_chem_comp = struct_chem_comps.ChemicalComponentsData(chem_comp) + else: + new_chem_comp = None + + return self.copy_and_update( + residues=new_residues, + chemical_components_data=new_chem_comp, + skip_validation=True, + ) + + def rename_chains_to_match( + self, + other: 'Structure', + *, + fuzzy_match_non_standard_res: bool = True, + ) -> Self: + """Returns a new structure with renamed chains to match another's. + + Example: + This structure has chains: {'A': 'DEEP', 'B': 'MIND', 'C': 'MIND'} + Other structure has chains: {'X': 'DEEP', 'Z': 'MIND', 'Y': 'MIND'} + + After calling this method, you will get a structure that has chains named: + {'X': 'DEEP', 'Z': 'MIND', Y: 'MIND'} + + Args: + other: Another `Structure`. This provides the reference chain names that + is used to rename this structure's chains. + fuzzy_match_non_standard_res: If True, protein/RNA/DNA chains with the + same one letter sequence will be matched. e.g. "MET-MET-UNK1" will match + "MET-MSE-UNK2", since both will be mapped to "MMX". If False, we require + the full res_names to match. + + Returns: + A new `Structure`, based on this structure, which has chains renamed to + match the other structure. + """ + sequences = self.chain_res_name_sequence( + include_missing_residues=True, + fix_non_standard_polymer_res=fuzzy_match_non_standard_res, + ) + + other_sequences = other.chain_res_name_sequence( + include_missing_residues=True, + fix_non_standard_polymer_res=fuzzy_match_non_standard_res, + ) + + # Check that the sequences are the same. + sequence_counts = collections.Counter(sequences.values()) + other_sequence_counts = collections.Counter(other_sequences.values()) + if other_sequence_counts != sequence_counts: + raise ValueError( + 'The other structure does not have the same sequences\n' + f' other: {other_sequence_counts}\n self: {sequence_counts}' + ) + + new_decoy_id_by_old_id = {} + used_chain_ids = set() + # Sort self keys and take min over other to make matching deterministic. + # The matching is arbitrary but this helps debugging. + for self_chain_id, self_seq in sorted(sequences.items()): + # Find corresponding chains in the other structure. + other_chain_id = min( + k + for k, v in other_sequences.items() + if v == self_seq and k not in used_chain_ids + ) + + new_decoy_id_by_old_id[self_chain_id] = other_chain_id + used_chain_ids.add(other_chain_id) + + return self.rename_chain_ids(new_decoy_id_by_old_id) + + def _apply_bioassembly_transform( + self, transform: bioassemblies.Transform + ) -> Self: + """Applies a bioassembly transform to this structure.""" + base_struct = self.filter(chain_id=transform.chain_ids) + transformed_atoms = base_struct.atoms_table.copy_and_update_coords( + transform.apply_to_coords(base_struct.coords) + ) + transformed_chains = base_struct.chains_table.copy_and_remap( + id=transform.chain_id_rename_map + ) + # Set the transformed author chain ID to match the label chain ID. + transformed_chains = transformed_chains.copy_and_update( + auth_asym_id=transformed_chains.id + ) + return base_struct.copy_and_update( + chains=transformed_chains, + atoms=transformed_atoms, + skip_validation=True, + ) + + def generate_bioassembly(self, assembly_id: str | None = None) -> Self: + """Generates a biological assembly as a new `Structure`. + + When no assembly ID is provided this method produces a default assembly. + If this structure has no `bioassembly_data` then this returns itself + unchanged. Otherwise a default assembly ID is picked with + `BioassemblyData.get_default_assembly_id()`. + + Args: + assembly_id: The assembly ID to generate, or None to generate a default + bioassembly. + + Returns: + A new `Structure`, based on this one, representing the specified + bioassembly. Note that if the bioassembly contains copies of chains + in the original structure then they will be given new unique chain IDs. + + Raises: + ValueError: If this structure's `bioassembly_data` is `None` and + `assembly_id` is not `None`. + """ + if self._bioassembly_data is None: + if assembly_id is None: + return self + else: + raise ValueError( + f'Unset bioassembly_data, cannot generate assembly {assembly_id}' + ) + + if assembly_id is None: + assembly_id = self._bioassembly_data.get_default_assembly_id() + + transformed_structs = [ + self._apply_bioassembly_transform(transform) + for transform in self._bioassembly_data.get_transforms(assembly_id) + ] + + # We don't need to assign unique chain IDs because the bioassembly + # transform takes care of remapping chain IDs to be unique. + concatenated = concat(transformed_structs, + assign_unique_chain_ids=False) + + # Copy over all scalar fields (e.g. name, release date, etc.) other than + # bioassembly_data because it relates only to the pre-transformed structure. + return concatenated.copy_and_update_globals( + name=self.name, + release_date=self.release_date, + resolution=self.resolution, + structure_method=self.structure_method, + bioassembly_data=None, + chemical_components_data=self.chemical_components_data, + ) + + def _to_mmcif_header(self) -> Mapping[str, Sequence[str]]: + raw_mmcif = collections.defaultdict(list) + raw_mmcif['data_'] = [self._name] + raw_mmcif['_entry.id'] = [self._name] + + if self._release_date is not None: + date = [datetime.datetime.strftime(self._release_date, '%Y-%m-%d')] + raw_mmcif['_pdbx_audit_revision_history.revision_date'] = date + raw_mmcif['_pdbx_database_status.recvd_initial_deposition_date'] = date + + if self._resolution is not None: + raw_mmcif['_refine.ls_d_res_high'] = ['%.2f' % self._resolution] + + if self._structure_method is not None: + for method in self._structure_method.split(','): + raw_mmcif['_exptl.method'].append(method) + + if self._bioassembly_data is not None: + raw_mmcif.update(self._bioassembly_data.to_mmcif_dict()) + + # Populate chemical components data for all residues of this Structure. + if self._chemical_components_data: + raw_mmcif.update(self._chemical_components_data.to_mmcif_dict()) + + # Add _software table to store version number used to generate mmCIF. + # Only required data items are used (+ _software.version). + raw_mmcif['_software.pdbx_ordinal'] = ['1'] + raw_mmcif['_software.name'] = ['DeepMind Structure Class'] + raw_mmcif['_software.version'] = [self._VERSION] + raw_mmcif['_software.classification'] = ['other'] # Required. + + return raw_mmcif + + def to_mmcif_dict( + self, + *, + coords_decimal_places: int = _COORDS_DECIMAL_PLACES, + ) -> mmcif.Mmcif: + """Returns an Mmcif representing the structure.""" + header = self._to_mmcif_header() + sequence_tables = structure_tables.to_mmcif_sequence_and_entity_tables( + self._chains, self._residues, self._atoms.res_key + ) + atom_and_bond_tables = structure_tables.to_mmcif_atom_site_and_bonds_table( + chains=self._chains, + residues=self._residues, + atoms=self._atoms, + bonds=self._bonds, + coords_decimal_places=coords_decimal_places, + ) + return mmcif.Mmcif({**header, **sequence_tables, **atom_and_bond_tables}) + + def to_mmcif( + self, *, coords_decimal_places: int = _COORDS_DECIMAL_PLACES + ) -> str: + """Returns an mmCIF string representing the structure. + + Args: + coords_decimal_places: The number of decimal places to keep for atom + coordinates, including trailing zeros. + """ + return self.to_mmcif_dict( + coords_decimal_places=coords_decimal_places + ).to_string() + + +class _LeadingDimSlice: + """Helper class for slicing the leading dimensions of a `Structure`. + + Wraps a `Structure` instance and applies a slice operation to the coordinate + fields and other fields that may have leading dimensions (e.g. b_factor). + + Example usage: + t0_struct = multi_state_struct.slice_leading_dims[0] + """ + + def __init__(self, struct: Structure): + self._struct = struct + + def __getitem__(self, *args, **kwargs) -> Structure: + sliced_atom_cols = {} + for col_name in structure_tables.Atoms.multimodel_cols: + if (col := self._struct.atoms_table.get_column(col_name)).ndim > 1: + sliced_col = col.__getitem__(*args, **kwargs) + if ( + not sliced_col.shape + or sliced_col.shape[-1] != self._struct.num_atoms + ): + raise ValueError( + 'Coordinate slice cannot change final (atom) dimension.' + ) + sliced_atom_cols[col_name] = sliced_col + sliced_atoms = self._struct.atoms_table.copy_and_update( + **sliced_atom_cols) + return self._struct.copy_and_update(atoms=sliced_atoms, skip_validation=True) + + +def stack(structs: Sequence[Structure], axis: int = 0) -> Structure: + """Stacks multiple structures into a single multi-model Structure. + + This function is the inverse of `Structure.unstack()`. + + NB: this function assumes that every structure in `structs` is identical + other than the coordinates and b-factors. Under this assumption we can safely + copy all these identical fields from the first element of structs w.l.o.g. + However this is not checked in full detail as full comparison is expensive. + Instead this only checks that the `atom_name` field is identical, and that + the coordinates have the same shape. + + Usage example: + ``` + multi_model_struct = structure.stack(structs, axis=0) + ``` + + Args: + structs: A sequence of structures, each with the same atoms, but they may + have different coordinates and b-factors. If any b-factors are not None + then they must have the same shape as each of the coordinate fields. + axis: The axis in the returned structure that represents the different + structures in `structs` and will have size `len(structs)`. This cannot be + the final dimension as this is reserved for `num_atoms`. + + Returns: + A `Structure` with the same atoms as the structures in `structs` but with + all of their coordinates stacked into a new leading axis. + + Raises: + ValueError: If `structs` is empty. + ValueError: If `structs` do not all have the same `atom_name` field. + """ + if not structs: + raise ValueError('Need at least one Structure to stack.') + struct_0, *other_structs = structs + for i, struct in enumerate(other_structs, start=1): + # Check that every structure has the same atom name column. + # This check is intended to catch cases where the input structures might + # contain the same atoms, but in different orders. This won't catch every + # such case, e.g. if these are carbon-alpha-only structures, but should + # catch most cases. + if np.any(struct.atoms_table.name != struct_0.atoms_table.name): + raise ValueError( + f'structs[0] and structs[{i}] have mismatching atom name columns.' + ) + + stacked_atoms = struct_0.atoms_table.copy_and_update( + x=np.stack([s.atoms_table.x for s in structs], axis=axis), + y=np.stack([s.atoms_table.y for s in structs], axis=axis), + z=np.stack([s.atoms_table.z for s in structs], axis=axis), + b_factor=np.stack([s.atoms_table.b_factor for s in structs], axis=axis), + occupancy=np.stack( + [s.atoms_table.occupancy for s in structs], axis=axis), + ) + return struct_0.copy_and_update(atoms=stacked_atoms, skip_validation=True) + + +def _assign_unique_chain_ids( + structs: Iterable[Structure], +) -> Sequence[Structure]: + """Creates a sequence of `Structure` objects with unique chain IDs. + + Let e.g. [A, B] denote a structure of two chains A and B, then this function + performs the following kind of renaming operation: + + e.g.: [Z], [C], [B, C] -> [A], [B], [C, D] + + NB: This function uses Structure.rename_chain_ids which will define each + structure's chains.auth_asym_id to be identical to its chains.id columns. + + Args: + structs: Structures whose chains ids are to be uniquified. + + Returns: + A sequence with the same number of elements as `structs` but where each + element has had its chains renamed so that they aren't shared with any + other `Structure` in the sequence. + """ + # Start counting at 1 because mmcif.int_id_to_str_id expects integers >= 1. + chain_counter = 1 + structs_with_new_chain_ids = [] + for struct in structs: + rename_map = {} + for chain_id in struct.chains: + rename_map[chain_id] = mmcif.int_id_to_str_id(chain_counter) + chain_counter += 1 + renamed = struct.rename_chain_ids(rename_map) + structs_with_new_chain_ids.append(renamed) + return structs_with_new_chain_ids + + +def concat( + structs: Sequence[Structure], + *, + name: str | None = None, + assign_unique_chain_ids: bool = True, +) -> Structure: + """Concatenates structures along the atom dimension. + + NB: By default this function will first assign unique chain IDs to all chains + in `structs` so that the resulting structure does not contain duplicate chain + IDs. This will also fix entity IDs and author chain IDs. If this is disabled + via `assign_unique_chain_ids=False` the user must ensure that there are no + duplicate chains (label_asym_id). However, duplicate entity IDs and author + chain IDs are allowed as that might be the desired behavior. + + If `assign_unique_chain_ids=True`, note also that the chain_ids may be + overwritten even if they are already unique. + + Let e.g. [A, B] denote a structure of two chains A and B, then this function + performs the following kind of concatenation operation: + + assign_unique_chain_ids=True: + label chain IDS : [Z], [C], [B, C] -> [A, B, C, D] + author chain IDS: [U], [V], [V, C] -> [A, B, C, D] + entity IDs : [1], [1], [3, 3] -> [1, 2, 3, 4] + assign_unique_chain_ids=False: + label chain IDS : [D], [B], [C, A] -> [D, B, C, A] (inputs must be unique) + author chain IDS: [U], [V], [V, A] -> [U, V, V, A] + entity IDs : [1], [1], [3, 3] -> [1, 1, 3, 3] + + NB: This operation loses some information from the elements of `structs`, + namely the `name`, `resolution`, `release_date` and `bioassembly_data` fields. + + Args: + structs: The `Structure` instances to concatenate. These should all have the + same number and shape of leading dimensions (i.e. if any are multi-model + structures then they should all have the same number of models). + name: Optional name to give to the concatenated structure. If None, the name + will be concatenation of names of all concatenated structures. + assign_unique_chain_ids: Whether this function will first assign new unique + chain IDs, entity IDs and author chain IDs to every chain in `structs`. If + `False` then users must ensure chain IDs are already unique, otherwise an + exception is raised. See `_assign_unique_chain_ids` for more information + on how this is performed. + + Returns: + A new concatenated `Structure` with all of the chains in `structs` combined + into one new structure. The new structure will be named by joining the + names of `structs` with underscores. + + Raises: + ValueError: If `structs` is empty. + ValueError: If `assign_unique_chain_ids=False` and not all chains in + `structs` have unique chain IDs. + """ + if not structs: + raise ValueError('Need at least one Structure to concatenate.') + if assign_unique_chain_ids: + structs = _assign_unique_chain_ids(structs) + + chemical_components_data = {} + seen_label_chain_ids = set() + for i, struct in enumerate(structs): + if not assign_unique_chain_ids: + if seen_cid := seen_label_chain_ids.intersection(struct.chains): + raise ValueError( + f'Chain IDs {seen_cid} from structs[{i}] also exist in other' + ' members of structs. All given structures must have unique chain' + ' IDs. Consider setting assign_unique_chain_ids=True.' + ) + seen_label_chain_ids.update(struct.chains) + + if struct.chemical_components_data is not None: + # pytype: disable=attribute-error # always-use-property-annotation + chemical_components_data.update( + struct.chemical_components_data.chem_comp) + + concatted_struct = table.concat_databases(structs) + name = name if name is not None else '_'.join(s.name for s in structs) + # Chain IDs (label and author) are fixed at this point, fix also entity IDs. + if assign_unique_chain_ids: + entity_id = np.char.mod('%d', np.arange( + 1, concatted_struct.num_chains + 1)) + chains = concatted_struct.chains_table.copy_and_update( + entity_id=entity_id) + else: + chains = concatted_struct.chains_table + return concatted_struct.copy_and_update( + name=name, + release_date=None, + resolution=None, + structure_method=None, + bioassembly_data=None, + chemical_components_data=( + struct_chem_comps.ChemicalComponentsData(chemical_components_data) + if chemical_components_data + else None + ), + chains=chains, + skip_validation=True, # Already validated by table.concat_databases. + ) + + +def multichain_residue_index( + struct: Structure, chain_offset: int = 9000, between_chain_buffer: int = 1000 +) -> np.ndarray: + """Compute a residue index array that is monotonic across all chains. + + Lots of metrics (lddt, l1_long, etc) require computing a + distance-along-chain between two residues. For multimers we want to ensure + that any residues on different chains have a high along-chain distance + (i.e. they should always count as long-range contacts for example). To + do this we add 10000 to the residue indices of each chain, and enforce that + the residue index is monotonically increasing across the whole complex. + + Note: This returns the same as struct.res_id for monomers. + + Args: + struct: The structure to make a multichain residue index for. + chain_offset: The start of each chain is offset by at least this amount. + This must be larger than the absolute range of standard residue IDs. + between_chain_buffer: The final residue in one chain will have at least this + much of a buffer before the first residue in the next chain. + + Returns: + A monotonically increasing residue index, with at least + `between_chain_buffer` residues in between each chain. + """ + if struct.num_atoms: + res_id_range = np.max(struct.res_id) - np.min(struct.res_id) + assert res_id_range < chain_offset + chain_id_int = struct.chain_id + monotonic_chain_id_int = np.concatenate( + ([0], np.cumsum(chain_id_int[1:] != chain_id_int[:-1])) + ) + return struct.res_id + monotonic_chain_id_int * ( + chain_offset + between_chain_buffer + ) + + +def make_empty_structure() -> Structure: + """Returns a new structure consisting of empty array fields.""" + return Structure( + chains=structure_tables.Chains.make_empty(), + residues=structure_tables.Residues.make_empty(), + atoms=structure_tables.Atoms.make_empty(), + bonds=structure_tables.Bonds.make_empty(), + ) + + +def enumerate_residues( + atom_iter: Iterable[Mapping[str, Any]], + all_residues: AllResidues | None = None, +) -> Iterator[tuple[int, Mapping[str, Any]]]: + """Provides a zero-indexed enumeration of residues in an atom iterable. + + Args: + atom_iter: An iterable of atom dicts as returned by Structure.iter_atoms(). + all_residues: (Optional) A structure's all_residues field. If present then + this will be used to count missing residues by adding appropriate gaps in + the residue enumeration. + + Yields: + (res_i, atom) pairs where atom is the unmodified atom dict and res_i is a + zero-based index for the residue that the atom belongs to. + """ + if all_residues is None: + prev_res = None + res_i = -1 + for atom in atom_iter: + res = (atom['chain_id'], atom['res_id']) + if res != prev_res: + prev_res = res + res_i += 1 + yield res_i, atom + else: + all_res_seq = [] # Sequence of (chain_id, res_id) for all chains. + prev_chain = None + res_i = 0 + for atom in atom_iter: + chain_id = atom['chain_id'] + if chain_id not in all_residues: + raise ValueError( + f'Atom {atom} does not belong to any residue in all_residues.' + ) + if chain_id != prev_chain: + prev_chain = chain_id + all_res_seq.extend( + (chain_id, res_id) for (_, res_id) in all_residues[chain_id] + ) + res = (chain_id, atom['res_id']) + while res_i < len(all_res_seq) and res != all_res_seq[res_i]: + res_i += 1 + if res_i == len(all_res_seq): + raise ValueError( + f'Atom {atom} does not belong to a residue in all_residues.' + ) + yield res_i, atom diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure_tables.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure_tables.py new file mode 100644 index 000000000..2867d8582 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure_tables.py @@ -0,0 +1,842 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Table implementations for the Structure class.""" + +import collections +from collections.abc import Mapping, Sequence +import dataclasses +import functools +import itertools +import typing +from typing_extensions import Any, ClassVar, Self +import numpy as np +from alphafold3.constants import mmcif_names +from alphafold3.constants import residue_names +from alphafold3.cpp import aggregation +from alphafold3.cpp import string_array +from alphafold3.structure import bonds as bonds_module +from alphafold3.structure import mmcif +from alphafold3.structure import table + + +Bonds = bonds_module.Bonds + + +def _residue_name_to_record_name( + residue_name: np.ndarray, + polymer_mask: np.ndarray, +) -> np.ndarray: + """Returns record names (ATOM/HETATM) given residue names and polymer mask.""" + record_name = np.array(['HETATM'] * len(residue_name), dtype=object) + record_name[polymer_mask] = string_array.remap( + residue_name[polymer_mask], + mapping={r: 'ATOM' for r in residue_names.STANDARD_POLYMER_TYPES}, + default_value='HETATM', + ) + return record_name + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class AuthorNamingScheme: + """A mapping from internal values to author values in a mmCIF. + + Fields: + auth_asym_id: A mapping from label_asym_id to auth_asym_id. + auth_seq_id: A mapping from label_asym_id to a mapping from + label_seq_id to auth_seq_id. + insertion_code: A mapping from label_asym_id to a mapping from + label_seq_id to insertion codes. + entity_id: A mapping from label_asym_id to _entity.id. + entity_desc: A mapping from _entity.id to _entity.pdbx_description. + """ + + auth_asym_id: Mapping[str, str] + auth_seq_id: Mapping[str, Mapping[int, str]] + insertion_code: Mapping[str, Mapping[int, str | None]] + entity_id: Mapping[str, str] + entity_desc: Mapping[str, str] + + +def _default( + candidate_value: np.ndarray | None, default_value: Sequence[Any], dtype: Any +) -> np.ndarray: + if candidate_value is None: + return np.array(default_value, dtype=dtype) + return np.array(candidate_value, dtype=dtype) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Atoms(table.Table): + """Table of atoms in a Structure.""" + + chain_key: np.ndarray + res_key: np.ndarray + name: np.ndarray + element: np.ndarray + x: np.ndarray + y: np.ndarray + z: np.ndarray + b_factor: np.ndarray + occupancy: np.ndarray + multimodel_cols: ClassVar[tuple[str, ...]] = ( + 'x', + 'y', + 'z', + 'b_factor', + 'occupancy', + ) + + def __post_init__(self): + # Validates that the atom coordinates, b-factors and occupancies are finite. + for column_name in ('x', 'y', 'z', 'b_factor', 'occupancy'): + column = self.get_column(column_name) + if not np.isfinite(column).all(): + raise ValueError( + f'Column {column_name} must not contain NaN/inf values.' + ) + # super().__post_init__() can't be used as that causes the following error: + # TypeError: super(type, obj): obj must be an instance or subtype of type + super(Atoms, self).__post_init__() + + @classmethod + def make_empty(cls) -> Self: + return cls( + key=np.array([], dtype=np.int64), + chain_key=np.array([], dtype=np.int64), + res_key=np.array([], dtype=np.int64), + name=np.array([], dtype=object), + element=np.array([], dtype=object), + x=np.array([], dtype=np.float32), + y=np.array([], dtype=np.float32), + z=np.array([], dtype=np.float32), + b_factor=np.array([], dtype=np.float32), + occupancy=np.array([], dtype=np.float32), + ) + + @classmethod + def from_defaults( + cls, + *, + chain_key: np.ndarray, + res_key: np.ndarray, + key: np.ndarray | None = None, + name: np.ndarray | None = None, + element: np.ndarray | None = None, + x: np.ndarray | None = None, + y: np.ndarray | None = None, + z: np.ndarray | None = None, + b_factor: np.ndarray | None = None, + occupancy: np.ndarray | None = None, + ) -> Self: + """Create an Atoms table with minimal user inputs.""" + num_atoms = len(chain_key) + if not num_atoms: + return cls.make_empty() + return Atoms( + chain_key=chain_key, + res_key=res_key, + key=_default(key, np.arange(num_atoms), np.int64), + name=_default(name, ['?'] * num_atoms, object), + element=_default(element, ['?'] * num_atoms, object), + x=_default(x, [0.0] * num_atoms, np.float32), + y=_default(y, [0.0] * num_atoms, np.float32), + z=_default(z, [0.0] * num_atoms, np.float32), + b_factor=_default(b_factor, [0.0] * num_atoms, np.float32), + occupancy=_default(occupancy, [1.0] * num_atoms, np.float32), + ) + + def get_value_by_index( + self, column_name: str, index: int + ) -> table.TableEntry | np.ndarray: + if column_name in self.multimodel_cols: + return self.get_column(column_name)[..., index] + else: + return self.get_column(column_name)[index] + + def copy_and_update_coords(self, coords: np.ndarray) -> Self: + """Returns a copy with the x, y and z columns updated.""" + if coords.shape[-1] != 3: + raise ValueError( + f'Expecting 3-dimensional coordinates, got {coords.shape}' + ) + return typing.cast( + Atoms, + self.copy_and_update( + x=coords[..., 0], y=coords[..., 1], z=coords[..., 2] + ), + ) + + @property + def shape(self) -> tuple[int, ...]: + return self.x.shape + + @property + def ndim(self) -> int: + return len(self.shape) + + @functools.cached_property + def num_models(self) -> int: + """The number of models of this Structure.""" + leading_dims = self.shape[:-1] + match leading_dims: + case(): + return 1 + case(single_leading_dim_size,): + return single_leading_dim_size + case _: + raise ValueError( + 'num_models not defined for atom tables with more than one ' + 'leading dimension.' + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Residues(table.Table): + """Table of residues in a Structure.""" + + chain_key: np.ndarray + id: np.ndarray + name: np.ndarray + auth_seq_id: np.ndarray + insertion_code: np.ndarray + + @classmethod + def make_empty(cls) -> Self: + return cls( + key=np.array([], dtype=np.int64), + chain_key=np.array([], dtype=np.int64), + id=np.array([], dtype=np.int32), + name=np.array([], dtype=object), + auth_seq_id=np.array([], dtype=object), + insertion_code=np.array([], dtype=object), + ) + + @classmethod + def from_defaults( + cls, + *, + id: np.ndarray, # pylint:disable=redefined-builtin + chain_key: np.ndarray, + key: np.ndarray | None = None, + name: np.ndarray | None = None, + auth_seq_id: np.ndarray | None = None, + insertion_code: np.ndarray | None = None, + ) -> Self: + """Create a Residues table with minimal user inputs.""" + num_res = len(id) + if not num_res: + return cls.make_empty() + return Residues( + key=_default(key, np.arange(num_res), np.int64), + id=id, + chain_key=chain_key, + name=_default(name, ['UNK'] * num_res, object), + auth_seq_id=_default(auth_seq_id, id.astype(str), object), + insertion_code=_default(insertion_code, ['?'] * num_res, object), + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Chains(table.Table): + """Table of chains in a Structure.""" + + id: np.ndarray + type: np.ndarray + auth_asym_id: np.ndarray + entity_id: np.ndarray + entity_desc: np.ndarray + + @classmethod + def make_empty(cls) -> Self: + return cls( + key=np.array([], dtype=np.int64), + id=np.array([], dtype=object), + type=np.array([], dtype=object), + auth_asym_id=np.array([], dtype=object), + entity_id=np.array([], dtype=object), + entity_desc=np.array([], dtype=object), + ) + + @classmethod + def from_defaults( + cls, + *, + id: np.ndarray, # pylint:disable=redefined-builtin + key: np.ndarray | None = None, + type: np.ndarray | None = None, # pylint:disable=redefined-builtin + auth_asym_id: np.ndarray | None = None, + entity_id: np.ndarray | None = None, + entity_desc: np.ndarray | None = None, + ) -> Self: + """Create a Chains table with minimal user inputs.""" + num_chains = len(id) + if not num_chains: + return cls.make_empty() + + return Chains( + key=_default(key, np.arange(num_chains), np.int64), + id=id, + type=_default(type, [mmcif_names.PROTEIN_CHAIN] + * num_chains, object), + auth_asym_id=_default(auth_asym_id, id, object), + entity_id=_default( + entity_id, np.arange(1, num_chains + 1).astype(str), object + ), + entity_desc=_default(entity_desc, ['.'] * num_chains, object), + ) + + +def to_mmcif_sequence_and_entity_tables( + chains: Chains, + residues: Residues, + atom_res_key: np.ndarray, +) -> Mapping[str, Sequence[str]]: + """Returns raw sequence and entity mmCIF tables.""" + raw_mmcif = collections.defaultdict(list) + chains_by_entity_id = {} + written_entity_poly_seq_ids = set() + present_res_keys = set(atom_res_key) + + # Performance optimisation: Find residue indices for each chain in advance, so + # that we don't have to do redundant masking work for each chain. + res_indices_for_chain = aggregation.indices_grouped_by_value( + residues.chain_key + ) + + for chain in chains.iterrows(): + # Add all chain information to the _struct_asym table. + chain_id = chain['id'] # Saves multiple dict lookups. + auth_asym_id = chain['auth_asym_id'] + entity_id = chain['entity_id'] + chains_by_entity_id.setdefault(entity_id, []).append(chain) + raw_mmcif['_struct_asym.id'].append(chain_id) + raw_mmcif['_struct_asym.entity_id'].append(entity_id) + + res_chain_indices = res_indices_for_chain[chain['key']] + chain_type = chain['type'] + is_polymer = chain_type in mmcif_names.POLYMER_CHAIN_TYPES + is_water = chain_type == mmcif_names.WATER + is_branched = len( + res_chain_indices) > 1 and not is_polymer and not is_water + write_entity_poly_seq = entity_id not in written_entity_poly_seq_ids + + # Iterate over the individual masked residue table columns, as that doesn't + # create a copy (only a view), while residues[res_chain_indices] does. + for res_key, res_name, res_id, pdb_seq_num, res_ins_code in zip( + residues.key[res_chain_indices], + residues.name[res_chain_indices], + residues.id[res_chain_indices], + residues.auth_seq_id[res_chain_indices], + residues.insertion_code[res_chain_indices], + strict=True, + ): + is_missing = res_key not in present_res_keys + str_res_id = str(res_id) + # While atom_site uses "?" for insertion codes, scheme tables use ".". + ins_code = (res_ins_code or '.').replace('?', '.') + auth_seq_num = '?' if is_missing else pdb_seq_num + + if is_polymer: + raw_mmcif['_pdbx_poly_seq_scheme.asym_id'].append(chain_id) + raw_mmcif['_pdbx_poly_seq_scheme.entity_id'].append(entity_id) + raw_mmcif['_pdbx_poly_seq_scheme.seq_id'].append(str_res_id) + raw_mmcif['_pdbx_poly_seq_scheme.mon_id'].append(res_name) + raw_mmcif['_pdbx_poly_seq_scheme.pdb_seq_num'].append( + pdb_seq_num) + raw_mmcif['_pdbx_poly_seq_scheme.auth_seq_num'].append( + auth_seq_num) + raw_mmcif['_pdbx_poly_seq_scheme.pdb_strand_id'].append( + auth_asym_id) + raw_mmcif['_pdbx_poly_seq_scheme.pdb_ins_code'].append( + ins_code) + # Structure doesn't support heterogeneous sequences. + raw_mmcif['_pdbx_poly_seq_scheme.hetero'].append('n') + if write_entity_poly_seq: + raw_mmcif['_entity_poly_seq.entity_id'].append(entity_id) + raw_mmcif['_entity_poly_seq.num'].append(str_res_id) + raw_mmcif['_entity_poly_seq.mon_id'].append(res_name) + # Structure doesn't support heterogeneous sequences. + raw_mmcif['_entity_poly_seq.hetero'].append('n') + written_entity_poly_seq_ids.add(entity_id) + elif is_branched: + raw_mmcif['_pdbx_branch_scheme.asym_id'].append(chain_id) + raw_mmcif['_pdbx_branch_scheme.entity_id'].append(entity_id) + raw_mmcif['_pdbx_branch_scheme.mon_id'].append(res_name) + raw_mmcif['_pdbx_branch_scheme.num'].append(str_res_id) + raw_mmcif['_pdbx_branch_scheme.pdb_asym_id'].append( + auth_asym_id) + raw_mmcif['_pdbx_branch_scheme.pdb_seq_num'].append( + pdb_seq_num) + raw_mmcif['_pdbx_branch_scheme.auth_asym_id'].append( + auth_asym_id) + raw_mmcif['_pdbx_branch_scheme.auth_seq_num'].append( + auth_seq_num) + raw_mmcif['_pdbx_branch_scheme.pdb_ins_code'].append(ins_code) + # Structure doesn't support heterogeneous sequences. + raw_mmcif['_pdbx_branch_scheme.hetero'].append('n') + else: + raw_mmcif['_pdbx_nonpoly_scheme.asym_id'].append(chain_id) + raw_mmcif['_pdbx_nonpoly_scheme.entity_id'].append(entity_id) + raw_mmcif['_pdbx_nonpoly_scheme.mon_id'].append(res_name) + raw_mmcif['_pdbx_nonpoly_scheme.pdb_seq_num'].append( + pdb_seq_num) + raw_mmcif['_pdbx_nonpoly_scheme.auth_seq_num'].append( + auth_seq_num) + raw_mmcif['_pdbx_nonpoly_scheme.pdb_strand_id'].append( + auth_asym_id) + raw_mmcif['_pdbx_nonpoly_scheme.pdb_ins_code'].append(ins_code) + + # Add _entity and _entity_poly tables. + for entity_id, chains in chains_by_entity_id.items(): + # chains should always be a non-empty list because of how we constructed + # chains_by_entity_id. + assert chains + # All chains for a given entity should have the same type and sequence + # so we can pick the first one without losing information. + key_chain = chains[0] + raw_mmcif['_entity.id'].append(entity_id) + raw_mmcif['_entity.pdbx_description'].append(key_chain['entity_desc']) + entity_type = key_chain['type'] + if entity_type not in mmcif_names.POLYMER_CHAIN_TYPES: + raw_mmcif['_entity.type'].append(entity_type) + else: + raw_mmcif['_entity.type'].append('polymer') + raw_mmcif['_entity_poly.entity_id'].append(entity_id) + raw_mmcif['_entity_poly.type'].append(entity_type) + + # _entity_poly.pdbx_strand_id is a comma-separated list of + # auth_asym_ids that are part of the entity. + raw_mmcif['_entity_poly.pdbx_strand_id'].append( + ','.join(chain['auth_asym_id'] for chain in chains) + ) + return raw_mmcif + + +def to_mmcif_atom_site_and_bonds_table( + *, + chains: Chains, + residues: Residues, + atoms: Atoms, + bonds: Bonds, + coords_decimal_places: int, +) -> Mapping[str, Sequence[str]]: + """Returns raw _atom_site and _struct_conn mmCIF tables.""" + raw_mmcif = collections.defaultdict(list) + # Use [value] * num wherever possible since it is about 10x faster than list + # comprehension in such cases. Also use f-strings instead of str() - faster. + total_atoms = atoms.size * atoms.num_models + raw_mmcif['_atom_site.id'] = [f'{i}' for i in range(1, total_atoms + 1)] + raw_mmcif['_atom_site.label_alt_id'] = ['.'] * total_atoms + # Use format_float_array instead of list comprehension for performance. + raw_mmcif['_atom_site.Cartn_x'] = mmcif.format_float_array( + values=atoms.x.ravel(), num_decimal_places=coords_decimal_places + ) + raw_mmcif['_atom_site.Cartn_y'] = mmcif.format_float_array( + values=atoms.y.ravel(), num_decimal_places=coords_decimal_places + ) + raw_mmcif['_atom_site.Cartn_z'] = mmcif.format_float_array( + values=atoms.z.ravel(), num_decimal_places=coords_decimal_places + ) + + # atoms.b_factor or atoms.occupancy can be flat even when the coordinates have + # leading dimensions. In this case we tile it to match. + if atoms.b_factor.ndim == 1: + atom_b_factor = np.tile(atoms.b_factor, atoms.num_models) + else: + atom_b_factor = atoms.b_factor.ravel() + raw_mmcif['_atom_site.B_iso_or_equiv'] = mmcif.format_float_array( + values=atom_b_factor, num_decimal_places=2 + ) + + if atoms.occupancy.ndim == 1: + atom_occupancy = np.tile(atoms.occupancy, atoms.num_models) + else: + atom_occupancy = atoms.occupancy.ravel() + raw_mmcif['_atom_site.occupancy'] = mmcif.format_float_array( + values=atom_occupancy.ravel(), num_decimal_places=2 + ) + + label_atom_id = atoms.name + type_symbol = atoms.element + label_comp_id = residues.apply_array_to_column('name', atoms.res_key) + label_asym_id = chains.apply_array_to_column('id', atoms.chain_key) + label_entity_id = chains.apply_array_to_column( + 'entity_id', atoms.chain_key) + # Performance optimisation: Do the int->str conversion on num_residue-sized, + # array, then select instead of selecting and then converting. + label_seq_id = residues.id.astype('str').astype(object)[ + ..., residues.index_by_key[atoms.res_key] + ] + + # _atom_site.label_seq_id is '.' for non-polymers. + non_polymer_chain_mask = string_array.isin( + chains.type, mmcif_names.POLYMER_CHAIN_TYPES, invert=True + ) + non_polymer_chain_keys = chains.key[non_polymer_chain_mask] + non_polymer_atom_mask = np.isin(atoms.chain_key, non_polymer_chain_keys) + label_seq_id[non_polymer_atom_mask] = '.' + + auth_asym_id = chains.apply_array_to_column( + 'auth_asym_id', atoms.chain_key) + auth_seq_id = residues.apply_array_to_column('auth_seq_id', atoms.res_key) + pdbx_pdb_ins_code = residues.apply_array_to_column( + 'insertion_code', atoms.res_key + ) + string_array.remap(pdbx_pdb_ins_code, mapping={None: '?'}, inplace=True) + + group_pdb = _residue_name_to_record_name( + residue_name=label_comp_id, polymer_mask=~non_polymer_atom_mask + ) + + def tile_for_models(arr: np.ndarray) -> list[str]: + if atoms.num_models == 1: + # Memory optimisation: np.tile(arr, 1) does a copy. + return arr.tolist() + return np.tile(arr, atoms.num_models).tolist() + + raw_mmcif['_atom_site.group_PDB'] = tile_for_models(group_pdb) + raw_mmcif['_atom_site.label_atom_id'] = tile_for_models(label_atom_id) + raw_mmcif['_atom_site.type_symbol'] = tile_for_models(type_symbol) + raw_mmcif['_atom_site.label_comp_id'] = tile_for_models(label_comp_id) + raw_mmcif['_atom_site.label_asym_id'] = tile_for_models(label_asym_id) + raw_mmcif['_atom_site.label_entity_id'] = tile_for_models(label_entity_id) + raw_mmcif['_atom_site.label_seq_id'] = tile_for_models(label_seq_id) + raw_mmcif['_atom_site.auth_asym_id'] = tile_for_models(auth_asym_id) + raw_mmcif['_atom_site.auth_seq_id'] = tile_for_models(auth_seq_id) + raw_mmcif['_atom_site.pdbx_PDB_ins_code'] = tile_for_models( + pdbx_pdb_ins_code) + model_id = np.array( + [str(i + 1) for i in range(atoms.num_models)], dtype=object + ) + raw_mmcif['_atom_site.pdbx_PDB_model_num'] = np.repeat( + model_id, [atoms.size] * atoms.num_models + ).tolist() + + if bonds.key.size > 0: + raw_mmcif.update( + bonds.to_mmcif_dict_from_atom_arrays( + atom_key=atoms.key, + chain_id=label_asym_id, + res_id=label_seq_id, + res_name=label_comp_id, + atom_name=label_atom_id, + auth_asym_id=auth_asym_id, + auth_seq_id=auth_seq_id, + insertion_code=np.array(pdbx_pdb_ins_code), + ) + ) + return raw_mmcif + + +def _flatten_author_naming_scheme_table( + res_table: Mapping[str, Mapping[int, str]], + chain_ids: np.ndarray, + res_chain_ids: np.ndarray, + res_ids: np.ndarray, + default_if_missing: str, + table_name: str, +) -> np.ndarray: + """Flattens an author naming scheme table consistently with res_ids.""" + if not set(chain_ids).issubset(res_table): + raise ValueError( + f'Chain IDs in the chain_id array must be a subset of {table_name} in ' + 'author naming scheme:\n' + f'chain_ids: {sorted(chain_ids)}\n' + f'{table_name} keys: {sorted(res_table.keys())}' + ) + + chain_change_mask = res_chain_ids[1:] != res_chain_ids[:-1] + res_chain_boundaries = np.concatenate( + ([0], np.where(chain_change_mask)[0] + 1, [len(res_chain_ids)]) + ) + + flat_vals = np.empty(len(res_ids), dtype=object) + for chain_start, chain_end in itertools.pairwise(res_chain_boundaries): + chain_id = res_chain_ids[chain_start] + chain_res_ids = res_ids[chain_start:chain_end] + chain_mapping = res_table[chain_id] + flat_vals[chain_start:chain_end] = [ + chain_mapping.get(r, default_if_missing) for r in chain_res_ids + ] + + return flat_vals + + +def tables_from_atom_arrays( + *, + res_id: np.ndarray, + author_naming_scheme: AuthorNamingScheme | None = None, + all_residues: Mapping[str, Sequence[tuple[str, int]]] | None = None, + chain_id: np.ndarray | None = None, + chain_type: np.ndarray | None = None, + res_name: np.ndarray | None = None, + atom_key: np.ndarray | None = None, + atom_name: np.ndarray | None = None, + atom_element: np.ndarray | None = None, + atom_x: np.ndarray | None = None, + atom_y: np.ndarray | None = None, + atom_z: np.ndarray | None = None, + atom_b_factor: np.ndarray | None = None, + atom_occupancy: np.ndarray | None = None, +) -> tuple[Atoms, Residues, Chains]: + """Returns Structure tables constructed from atom array level data. + + All fields except name and, res_id are optional, all array fields consist of a + value for each atom in the structure - so residue and chain values should hold + the same value for each atom in the chain or residue. Fields which are not + defined are filled with default values. + + Validation is performed by the Structure constructor where possible - but + author_naming scheme and all_residues must be checked in this function. + + It is not possible to construct structures with chains that do not contain + any resolved residues using this function. If this is necessary, use the + structure.Structure constructor directly. + + Args: + res_id: Integer array of shape [num_atom]. The unique residue identifier for + each residue. mmCIF field - _atom_site.label_seq_id. + author_naming_scheme: An optional instance of AuthorNamingScheme to use when + converting this structure to mmCIF. + all_residues: An optional mapping from each chain ID (i.e. label_asym_id) to + a sequence of (label_comp_id, label_seq_id) tuples, one per residue. This + can contain residues that aren't present in the atom arrays. This is + common in experimental data where some residues are not resolved but are + known to be present. + chain_id: String array of shape [num_atom] of unique chain identifiers. + mmCIF field - _atom_site.label_asym_id. + chain_type: String array of shape [num_atom]. The molecular type of the + current chain (e.g. polyribonucleotide). mmCIF field - _entity_poly.type + OR _entity.type (for non-polymers). + res_name: String array of shape [num_atom].. The name of each residue, + typically a 3 letter string for polypeptides or 1-2 letter strings for + polynucleotides. mmCIF field - _atom_site.label_comp_id. + atom_key: A unique sorted integer array, used only by the bonds table to + identify the atoms participating in each bond. If the bonds table is + specified then this column must be non-None. + atom_name: String array of shape [num_atom]. The name of each atom (e.g CA, + O2', etc.). mmCIF field - _atom_site.label_atom_id. + atom_element: String array of shape [num_atom]. The element type of each + atom (e.g. C, O, N, etc.). mmCIF field - _atom_site.type_symbol. + atom_x: Float array of shape [..., num_atom] of atom x coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_y: Float array of shape [..., num_atom] of atom y coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_z: Float array of shape [..., num_atom] of atom z coordinates. May have + arbitrary leading dimensions, provided that these are consistent across + all coordinate fields. + atom_b_factor: Float array of shape [..., num_atom] or [num_atom] of atom + b-factors or equivalent. If there are no extra leading dimensions then + these values are assumed to apply to all coordinates for a given atom. If + there are leading dimensions then these must match those used by the + coordinate fields. + atom_occupancy: Float array of shape [..., num_atom] or [num_atom] of atom + occupancies or equivalent. If there are no extra leading dimensions then + these values are assumed to apply to all coordinates for a given atom. If + there are leading dimensions then these must match those used by the + coordinate fields. + """ + num_atoms = len(res_id) + + for arr_name, array, dtype in ( + ('chain_id', chain_id, object), + ('chain_type', chain_type, object), + ('res_id', res_id, np.int32), + ('res_name', res_name, object), + ('atom_key', atom_key, np.int64), + ('atom_name', atom_name, object), + ('atom_element', atom_element, object), + ): + if array is not None and array.shape != (num_atoms,): + raise ValueError( + f'{arr_name} shape {array.shape} != ({num_atoms},)') + if array is not None and array.dtype != dtype: + raise ValueError(f'{arr_name} dtype {array.dtype} != {dtype}') + + for arr_name, array in ( + ('atom_x', atom_x), + ('atom_y', atom_y), + ('atom_z', atom_z), + ('atom_b_factor', atom_b_factor), + ('atom_occupancy', atom_occupancy), + ): + if array is not None and array.shape[-1] != num_atoms: + raise ValueError( + f'{arr_name} last dim {array.shape[-1]} != {num_atoms=}') + if ( + array is not None + and array.dtype != np.float32 + and array.dtype != np.float64 + ): + raise ValueError( + f'{arr_name} must be np.float32 or np.float64, got {array.dtype=}' + ) + + if all_residues is not None and (res_name is None or res_id is None): + raise ValueError( + 'If all_residues != None, res_name and res_id must not be None either.' + ) + + if num_atoms == 0: + return Atoms.make_empty(), Residues.make_empty(), Chains.make_empty() + + if chain_id is None: + chain_id = np.full(shape=num_atoms, fill_value='A', dtype=object) + if res_name is None: + res_name = np.full(shape=num_atoms, fill_value='UNK', dtype=object) + + chain_change_mask = chain_id[1:] != chain_id[:-1] + chain_start = np.concatenate(([0], np.where(chain_change_mask)[0] + 1)) + res_start = np.concatenate( + ([0], np.where((res_id[1:] != res_id[:-1]) | chain_change_mask)[0] + 1) + ) + + if len(set(chain_id)) != len(chain_start): + raise ValueError(f'Chain IDs must be contiguous, but got {chain_id}') + + # We do not support chains with unresolved residues-only in this function. + chain_ids = chain_id[chain_start] + if all_residues and set(all_residues.keys()) != set(chain_ids): + raise ValueError( + 'all_residues must contain the same set of chain IDs as the chain_id ' + f'array:\nall_residues keys: {sorted(all_residues.keys())}\n' + f'chain_ids: {sorted(chain_ids)}.' + ) + # Make sure all_residue ordering is consistent with chain_id. + if all_residues and np.any(list(all_residues.keys()) != chain_ids): + all_residues = {cid: all_residues[cid] for cid in chain_ids} + + # Create the chains table. + num_chains = len(chain_ids) + chain_keys = np.arange(num_chains, dtype=np.int64) + chain_key_by_chain_id = dict(zip(chain_ids, chain_keys, strict=True)) + + if chain_type is not None: + chain_types = chain_type[chain_start] + else: + chain_types = np.full( + num_chains, mmcif_names.PROTEIN_CHAIN, dtype=object) + + if author_naming_scheme is not None: + auth_asym_id = string_array.remap( + chain_ids, author_naming_scheme.auth_asym_id + ) + entity_id = string_array.remap( + chain_ids, author_naming_scheme.entity_id, default_value='.' + ) + entity_desc = string_array.remap( + entity_id, author_naming_scheme.entity_desc, default_value='.' + ) + else: + auth_asym_id = chain_ids + entity_id = (chain_keys + 1).astype(str).astype(object) + entity_desc = np.full(num_chains, '.', dtype=object) + + chains = Chains( + key=chain_keys, + id=chain_ids, + type=chain_types, + auth_asym_id=auth_asym_id, + entity_id=entity_id, + entity_desc=entity_desc, + ) + + # Create the residues table. + if all_residues is not None: + residue_order = [] + for cid, residues in all_residues.items(): + residue_order.extend((cid, rname, int(rid)) + for (rname, rid) in residues) + res_chain_ids, res_names, res_ids = zip(*residue_order) + res_chain_ids = np.array(res_chain_ids, dtype=object) + res_ids = np.array(res_ids, dtype=np.int32) + res_names = np.array(res_names, dtype=object) + else: + res_chain_ids = chain_id[res_start] + res_ids = res_id[res_start] + res_names = res_name[res_start] + residue_order = list(zip(res_chain_ids, res_names, res_ids)) + + if author_naming_scheme is not None and author_naming_scheme.auth_seq_id: + auth_seq_id = _flatten_author_naming_scheme_table( + author_naming_scheme.auth_seq_id, + chain_ids=chain_ids, + res_chain_ids=res_chain_ids, + res_ids=res_ids, + default_if_missing='.', + table_name='auth_seq_id', + ) + else: + auth_seq_id = res_ids.astype(str).astype(object) + + if author_naming_scheme is not None and author_naming_scheme.insertion_code: + insertion_code = _flatten_author_naming_scheme_table( + author_naming_scheme.insertion_code, + chain_ids=chain_ids, + res_chain_ids=res_chain_ids, + res_ids=res_ids, + default_if_missing='?', + table_name='insertion_code', + ) + # Make sure insertion code of None is mapped to '.'. + insertion_code = string_array.remap(insertion_code, {None: '?'}) + else: + insertion_code = np.full( + shape=len(res_ids), fill_value='?', dtype=object) + + res_key_by_res = {res: i for i, res in enumerate(residue_order)} + res_keys = np.arange(len(residue_order), dtype=np.int64) + res_chain_keys = string_array.remap( + res_chain_ids, chain_key_by_chain_id + ).astype(np.int64) + residues = Residues( + chain_key=res_chain_keys, + key=res_keys, + id=res_ids, + name=res_names, + auth_seq_id=auth_seq_id, + insertion_code=insertion_code, + ) + + if atom_key is None: + atom_key = np.arange(num_atoms, dtype=np.int64) + + atom_chain_keys = string_array.remap(chain_id, chain_key_by_chain_id).astype( + np.int64 + ) + + try: + atom_res_keys = [res_key_by_res[r] + for r in zip(chain_id, res_name, res_id)] + except KeyError as e: + missing_chain_id, missing_res_name, missing_res_id = e.args[0] + raise ValueError( + 'Inconsistent res_name, res_id and all_residues. Could not find ' + f'residue with chain_id={missing_chain_id}, ' + f'res_name={missing_res_name}, res_id={missing_res_id} in all_residues.' + ) from e + + atoms = Atoms( + key=atom_key, + chain_key=atom_chain_keys, + res_key=np.array(atom_res_keys, dtype=np.int64), + name=_default(atom_name, ['?'] * num_atoms, object), + element=_default(atom_element, ['?'] * num_atoms, object), + x=_default(atom_x, [0.0] * num_atoms, np.float32), + y=_default(atom_y, [0.0] * num_atoms, np.float32), + z=_default(atom_z, [0.0] * num_atoms, np.float32), + b_factor=_default(atom_b_factor, [0.0] * num_atoms, np.float32), + occupancy=_default(atom_occupancy, [1.0] * num_atoms, np.float32), + ) + return atoms, residues, chains diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/table.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/table.py new file mode 100644 index 000000000..7cad4a27d --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/table.py @@ -0,0 +1,565 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Table module for atom/residue/chain tables in Structure. + +Tables are intended to be lightweight collections of columns, loosely based +on a pandas dataframe, for use in the Structure class. +""" + +import abc +from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, Sequence +import dataclasses +import functools +import graphlib +import typing +from typing_extensions import Any, Protocol, Self, TypeAlias, TypeVar, overload + +from alphafold3.cpp import string_array +import numpy as np + + +TableEntry: TypeAlias = str | int | float | None +FilterPredicate: TypeAlias = ( + TableEntry + | Iterable[Any] # Workaround for b/326384670. Tighten once fixed. + | Callable[[Any], bool] # Workaround for b/326384670. Tighten once fixed. + | Callable[[np.ndarray], bool] +) + + +class RowLookup(Protocol): + + def get_row_by_key( + self, + key: int, + column_name_map: Mapping[str, str] | None = None, + ) -> Mapping[str, Any]: + ... + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class Table: + """Parent class for structure tables. + + A table is a collection of columns of equal length, where one column is the + key. The key uniquely identifies each row in the table. + + A table can refer to other tables by including a foreign key column, whose + values are key values from the other table's key column. These column can have + arbitrary names and are treated like any other integer-valued column. + + See the `Database` class in this module for utilities for handing sets of + tables that are related via foreign keys. + + NB: This does not correspond to an mmCIF table. + """ + + key: np.ndarray + + def __post_init__(self): + for col_name in self.columns: + if (col_len := self.get_column(col_name).shape[-1]) != self.size: + raise ValueError( + f'All columns should have length {self.size} but got "{col_name}"' + f' with length {col_len}.' + ) + # Make col immutable. + self.get_column(col_name).flags.writeable = False + if self.key.size and self.key.min() < 0: + raise ValueError( + 'Key values must be non-negative. Got negative values:' + f' {set(self.key[self.key < 0])}' + ) + self.key.flags.writeable = False # Make key immutable. + + def __getstate__(self) -> dict[str, Any]: + """Returns members with cached properties removed for pickling.""" + cached_props = { + k + for k, v in self.__class__.__dict__.items() + if isinstance(v, functools.cached_property) + } + return {k: v for k, v in self.__dict__.items() if k not in cached_props} + + @functools.cached_property + def index_by_key(self) -> np.ndarray: + """Mapping from key values to their index in the column arrays. + + i.e.: self.key[index_by_key[k]] == k + """ + if not self.key.size: + return np.array([], dtype=np.int64) + else: + index_by_key = np.zeros(np.max(self.key) + 1, dtype=np.int64) + index_by_key[self.key] = np.arange(self.size) + return index_by_key + + @functools.cached_property + def columns(self) -> tuple[str, ...]: + """The names of the columns in the table, including the key column.""" + return tuple(field.name for field in dataclasses.fields(self)) + + @functools.cached_property + def items(self) -> Mapping[str, np.ndarray]: + """Returns the mapping from column names to column values.""" + return {col: getattr(self, col) for col in self.columns} + + @functools.cached_property + def size(self) -> int: + """The number of rows in the table.""" + return self.key.shape[-1] + + def __len__(self) -> int: + return self.size + + def get_column(self, column_name: str) -> np.ndarray: + """Gets a column by name.""" + # Performance optimisation: use the cached columns, instead of getattr. + return self.items[column_name] + + def apply_array(self, arr: np.ndarray) -> Self: + """Returns a sliced table using a key (!= index) array or a boolean mask.""" + if arr.dtype == bool and np.all(arr): + return self # Shortcut: No-op, so just return. + + return self.copy_and_update(**{ + column_name: self.apply_array_to_column(column_name, arr) + for column_name in self.columns + }) + + def apply_index(self, index_arr: np.ndarray) -> Self: + """Returns a sliced table using an index (!= key) array.""" + if index_arr.dtype == bool: + raise ValueError('The index array must not be a boolean mask.') + + return self.copy_and_update( + **{col: self.get_column(col)[..., index_arr] for col in self.columns} + ) + + def apply_array_to_column( + self, + column_name: str, + arr: np.ndarray, + ) -> np.ndarray: + """Returns a sliced column array using a key array or a boolean mask.""" + if arr.dtype == bool: + return self.get_column(column_name)[..., arr] + else: + return self.get_column(column_name)[..., self.index_by_key[arr]] + + def get_value_by_index(self, column_name: str, index: int) -> Any: + return self.get_column(column_name)[index] + + def get_value_by_key( + self, + column_name: str, + key: int | np.integer, + ) -> TableEntry: + """Gets the value of a column at the row with specified key value.""" + return self.get_value_by_index(column_name, self.index_by_key[key]) + + @overload + def __getitem__(self, key: str) -> np.ndarray: + ... + + @overload + def __getitem__(self, key: np.ndarray) -> 'Table': + ... + + @overload + def __getitem__(self, key: tuple[str, int | np.integer]) -> TableEntry: + ... + + @overload + def __getitem__(self, key: tuple[str, np.ndarray]) -> np.ndarray: + ... + + def __getitem__(self, key): + match key: + case str(): + return self.get_column(key) + case np.ndarray() as key_arr_or_mask: + return self.apply_array(key_arr_or_mask) + case str() as col, int() | np.integer() as key_val: + return self.get_value_by_key(col, key_val) + case str() as col, np.ndarray() as key_arr_or_mask: + return self.apply_array_to_column(col, key_arr_or_mask) + case _: + if isinstance(key, tuple): + err_msg = f'{key}, type: tuple({[type(v) for v in key]})' + else: + err_msg = f'{key}, type: {type(key)}' + raise KeyError(err_msg) + + def get_row_by_key( + self, + key: int, + column_name_map: Mapping[str, str] | None = None, + ) -> dict[str, Any]: + """Gets the row with specified key value.""" + return self.get_row_by_index( + self.index_by_key[key], column_name_map=column_name_map + ) + + def get_row_by_index( + self, + index: int, + column_name_map: Mapping[str, str] | None = None, + ) -> dict[str, Any]: + """Gets the row at the specified index.""" + if column_name_map is not None: + return { + renamed_col: self.get_value_by_index(col, index) + for renamed_col, col in column_name_map.items() + } + else: + return {col: self.get_value_by_index(col, index) for col in self.columns} + + def iterrows( + self, + *, + row_keys: np.ndarray | None = None, + column_name_map: Mapping[str, str] | None = None, + **table_by_foreign_key_col: RowLookup, + ) -> Iterator[Mapping[str, Any]]: + """Yields rows from the table. + + Args: + row_keys: An optional array of keys of rows to yield. If None, all rows + will be yielded. + column_name_map: An optional mapping from desired keys in the row dicts to + the names of the columns they correspond to. + **table_by_foreign_key_col: An optional mapping from column names in this + table, which are expected to be columns of foreign keys, to the table + that the foreign keys point into. If provided, then the yielded rows + will include data from the foreign tables at the appropriate key. + """ + if row_keys is not None: + row_indices = self.index_by_key[row_keys] + else: + row_indices = range(self.size) + for i in row_indices: + row = self.get_row_by_index(i, column_name_map=column_name_map) + for key_col, table in table_by_foreign_key_col.items(): + foreign_key = self[key_col][i] + foreign_row = table.get_row_by_key(foreign_key) + row.update(foreign_row) + yield row + + def with_column_names( + self, column_name_map: Mapping[str, str] + ) -> 'RenamedTableView': + """Returns a view of this table with mapped column names.""" + return RenamedTableView(self, column_name_map=column_name_map) + + def make_filter_mask( + self, + mask: np.ndarray | None = None, + *, + apply_per_element: bool = False, + **predicate_by_col: FilterPredicate, + ) -> np.ndarray | None: + """Returns a boolean array of rows to keep, or None if all can be kept. + + Args: + mask: See `Table.filter`. + apply_per_element: See `Table.filter`. + **predicate_by_col: See `Table.filter`. + + Returns: + Either a boolean NumPy array of length `(self.size,)` denoting which rows + should be kept according to the input mask and predicates, or None. None + implies there is no filtering required, and is used where possible + instead of an all-True array to save time and space. + """ + if mask is None: + if not predicate_by_col: + return None + else: + mask = np.ones((self.size,), dtype=bool) + else: + if mask.shape != (self.size,): + raise ValueError( + f'mask must have shape ({self.size},). Got: {mask.shape}.' + ) + if mask.dtype != bool: + raise ValueError( + f'mask must have dtype bool. Got: {mask.dtype}.') + + for col, predicate in predicate_by_col.items(): + if self[col].ndim > 1: + raise ValueError( + f'Cannot filter by column {col} with more than 1 dimension.' + ) + + callable_predicates = [] + if not callable(predicate): + if isinstance(predicate, Iterable) and not isinstance(predicate, str): + target_vals = predicate + else: + target_vals = [predicate] + for target_val in target_vals: + callable_predicates.append( + lambda x, target=target_val: x == target) + else: + callable_predicates.append(predicate) + + field_mask = np.zeros_like(mask) + for callable_predicate in callable_predicates: + if not apply_per_element: + callable_predicate = typing.cast( + Callable[[np.ndarray], bool], callable_predicate + ) + predicate_result = callable_predicate(self.get_column(col)) + else: + predicate_result = np.array( + [callable_predicate(elem) + for elem in self.get_column(col)] + ) + np.logical_or(field_mask, predicate_result, out=field_mask) + np.logical_and(mask, field_mask, out=mask) # Update in-place. + return mask + + def filter( + self, + mask: np.ndarray | None = None, + *, + apply_per_element: bool = False, + invert: bool = False, + **predicate_by_col: FilterPredicate, + ) -> Self: + """Filters the table using mask and/or predicates and returns a new table. + + Predicates can be either: + 1. A constant value, e.g. `'CA'`. In this case then only rows that match + this value for the given column are retained. + 2. A (non-string) iterable e.g. `('A', 'B')`. In this + case then rows are retained if they match any of the provided values for + the given column. + 3. A boolean function e.g. `lambda b_fac: b_fac < 100.0`. + In this case then only rows that evaluate to `True` are retained. By + default this function's parameter is expected to be an array, unless + `apply_per_element=True`. + + Args: + mask: An optional boolean NumPy array with length equal to the table size. + If provided then this will be combined with the other predicates so that + a row is included if it is masked-in *and* matches all the predicates. + apply_per_element: Whether apply predicates to each element in the column + individually, or to pass the whole column array to the predicate. + invert: If True then the returned table will contain exactly those rows + that would be removed if this was `False`. + **predicate_by_col: A mapping from column name to a predicate. Filtered + columns must be 1D arrays. If multiple columns are provided as keyword + arguments then each predicate is applied and the results are combined + using a boolean AND operation, so an atom is only retained if it passes + all predicates. + + Returns: + A new table with the desired rows retained (or filtered out if + `invert=True`). + + Raises: + ValueError: If mask is provided and is not a bool array with shape + `(num_atoms,)`. + """ + filter_mask = self.make_filter_mask( + mask, apply_per_element=apply_per_element, **predicate_by_col + ) + if filter_mask is None: + # No mask or predicate was specified, so we can return early. + if not invert: + return self + else: + return self[np.array((), dtype=np.int64)] + else: + return self[~filter_mask if invert else filter_mask] + + def _validate_keys_are_column_names(self, keys: Collection[str]) -> None: + """Raises an error if any of the keys are not column names.""" + if mismatches := set(keys) - set(self.columns): + raise ValueError(f'Invalid column names: {sorted(mismatches)}.') + + def copy_and_update(self, **new_column_by_column_name: np.ndarray) -> Self: + """Returns a copy of this table with the specified changes applied. + + Args: + **new_column_by_column_name: New values for the specified columns. + + Raises: + ValueError: If a specified column name is not a column in this table. + """ + self._validate_keys_are_column_names(new_column_by_column_name) + return dataclasses.replace(self, **new_column_by_column_name) + + def copy_and_remap( + self, **mapping_by_col: Mapping[TableEntry, TableEntry] + ) -> Self: + """Returns a copy of the table with the specified columns remapped. + + Args: + **mapping_by_col: Each kwarg key should be the name of one of this table's + columns, and each value should be a mapping. The values in the column + will be looked up in the mapping and replaced with the result if one is + found. + + Raises: + ValueError: If a specified column name is not a column in this table. + """ + self._validate_keys_are_column_names(mapping_by_col) + if not self.size: + return self + remapped_cols = {} + for column_name, mapping in mapping_by_col.items(): + col_arr = self.get_column(column_name) + if col_arr.dtype == object: + remapped = string_array.remap(col_arr, mapping) + else: + remapped = np.vectorize(lambda x: mapping.get(x, x))( + col_arr) # pylint: disable=cell-var-from-loop + remapped_cols[column_name] = remapped + return self.copy_and_update(**remapped_cols) + + +class RenamedTableView: + """View of a table with renamed column names.""" + + def __init__(self, table: Table, column_name_map: Mapping[str, str]): + self._table = table + self._column_name_map = column_name_map + + def get_row_by_key( + self, + key: int, + column_name_map: Mapping[str, str] | None = None, + ) -> Mapping[str, Any]: + del column_name_map + return self._table.get_row_by_key( + key, column_name_map=self._column_name_map + ) + + +_DatabaseT = TypeVar('_DatabaseT', bound='Database') + + +class Database(abc.ABC): + """Relational database base class.""" + + @property + @abc.abstractmethod + def tables(self) -> Collection[str]: + """The names of the tables in this database.""" + + @abc.abstractmethod + def get_table(self, table_name: str) -> Table: + """Gets the table with the given name.""" + + @property + @abc.abstractmethod + def foreign_keys(self) -> Mapping[str, Collection[tuple[str, str]]]: + """Describes the relationship between keys in the database. + + Returns: + A map from table names to pairs of `(column_name, foreign_table_name)` + where `column_name` is a column containing foreign keys in the table named + by the key, and the `foreign_table_name` is the name of the table that + those foreign keys refer to. + """ + + @abc.abstractmethod + def copy_and_update( + self: _DatabaseT, + **new_field_by_field_name: ..., + ) -> _DatabaseT: + """Returns a copy of this database with the specified changes applied.""" + + +def table_dependency_order(db: Database) -> Iterable[str]: + """Yields the names of the tables in the database in dependency order. + + This order guarantees that a table appears after all other tables that + it refers to using foreign keys. Specifically A < B implies that A contains + no column that refers to B.key as a foreign key. + + Args: + db: The database that defines the table names and foreign keys. + """ + connections: dict[str, set[str]] = {} + for table_name in db.tables: + connection_set = set() + for _, foreign_table in db.foreign_keys.get(table_name, ()): + connection_set.add(foreign_table) + connections[table_name] = connection_set + yield from graphlib.TopologicalSorter(connections).static_order() + + +def concat_databases(dbs: Sequence[_DatabaseT]) -> _DatabaseT: + """Concatenates the tables across a sequence of databases. + + Args: + dbs: A non-empty sequence of database instances of the same type. + + Returns: + A new database containing the concatenated tables from the input databases. + + Raises: + ValueError: If `dbs` is empty or `dbs` contains different Database + types. + """ + if not dbs: + raise ValueError('Need at least one value to concatenate.') + distinct_db_types = {type(db) for db in dbs} + if len(distinct_db_types) > 1: + raise ValueError( + f'All `dbs` must be of the same type, got: {distinct_db_types}' + ) + + first_db, *other_dbs = dbs + concatted_tables: dict[str, Table] = {} + key_offsets: dict[str, list[int]] = {} + for table_name in table_dependency_order(first_db): + first_table = first_db.get_table(table_name) + columns: dict[str, list[np.ndarray]] = { + column_name: [first_table.get_column(column_name)] + for column_name in first_table.columns + } + key_offsets[table_name] = [ + first_table.key.max() + 1 if first_table.size else 0 + ] + + for prev_index, db in enumerate(other_dbs): + table = db.get_table(table_name) + for col_name in table.columns: + columns[col_name].append(table.get_column(col_name)) + key_offset = key_offsets[table_name][prev_index] + offset_key = table.key + key_offset + columns['key'][-1] = offset_key + if table.size: + key_offsets[table_name].append(offset_key.max() + 1) + else: + key_offsets[table_name].append( + key_offsets[table_name][prev_index]) + for fkey_col_name, foreign_table_name in first_db.foreign_keys.get( + table_name, [] + ): + fkey_columns = columns[fkey_col_name] + fkey_columns[-1] = ( + fkey_columns[-1] + + key_offsets[foreign_table_name][prev_index] + ) + + concatted_columns = { + column_name: np.concatenate(values, axis=-1) + for column_name, values in columns.items() + } + concatted_tables[table_name] = (type(first_table))(**concatted_columns) + return first_db.copy_and_update(**concatted_tables) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/test_utils.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/test_utils.py new file mode 100644 index 000000000..8cc6ec498 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/test_utils.py @@ -0,0 +1,358 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# ============================================================================ + +"""Utilities for structure module testing.""" + +import dataclasses + +from absl.testing import parameterized +from alphafold3 import structure +from alphafold3.common.testing import data +import numpy as np + +import os +import contextlib +import datetime +import difflib +import functools +import hashlib +import shutil +import pathlib +from typing import Any +from absl.testing import absltest +import mindspore as ms +from alphafold3.common.testing import data as testing_data +from alphafold3.common import resources +from alphafold3.data import pipeline +from alphafold3.model.atom_layout import atom_layout + +_JACKHMMER_BINARY_PATH = shutil.which('jackhmmer') +_NHMMER_BINARY_PATH = shutil.which('nhmmer') +_HMMALIGN_BINARY_PATH = shutil.which('hmmalign') +_HMMSEARCH_BINARY_PATH = shutil.which('hmmsearch') +_HMMBUILD_BINARY_PATH = shutil.which('hmmbuild') + +@contextlib.contextmanager +def _output(name: str): + with open(result_path := f'{absltest.TEST_TMPDIR.value}/{name}', "wb") as f: + yield result_path, f + + +@functools.singledispatch +def _hash_data(x: Any, /) -> str: + if x is None: + return '<>' + return _hash_data(json.dumps(x).encode('utf-8')) + + +@_hash_data.register +def _(x: bytes, /) -> str: + return hashlib.sha256(x).hexdigest() + + +@_hash_data.register +def _(x: ms.Tensor) -> str: + return _hash_data(x.asnumpy()) + + +@_hash_data.register +def _(x: np.ndarray) -> str: + if x.dtype == object: + return ';'.join(map(_hash_data, x.ravel().tolist())) + return _hash_data(x.tobytes()) + + +@_hash_data.register +def _(_: structure.Structure) -> str: + return '<>' + + +@_hash_data.register +def _(_: atom_layout.AtomLayout) -> str: + return '<>' + + +def _generate_diff(actual: str, expected: str) -> str: + return '\n'.join( + difflib.unified_diff( + expected.split('\n'), + actual.split('\n'), + fromfile='expected', + tofile='actual', + lineterm='', + ) + ) + + +def tree_map(func, dict_tree): + if isinstance(dict_tree, dict): + return {k: tree_map(func, v) for k, v in dict_tree.items()} + else: + if func == "asnumpy": + return dict_tree.asnumpy() + elif func == "float32": + return dict_tree.astype(ms.float32) + elif func == "bfloat16": + return dict_tree.astype(ms.bfloat16) + else: + return func(dict_tree) + +class StructureTestCase(parameterized.TestCase): + """Testing utilities for working with structure.Structure.""" + + def set_path(self, use_full_database=False): + if use_full_database: + small_bfd_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/bfd-first_non_consensus_sequences.fasta' + ).path() + mgnify_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/mgy_clusters_2022_05.fa' + ).path() + uniprot_cluster_annot_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/uniprot_all_2021_04.fa' + ).path() + uniref90_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/uniref90_2022_05.fa' + ).path() + ntrna_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta' + ).path() + rfam_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta' + ).path() + rna_central_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/rnacentral_active_seq_id_90_cov_80_linclust.fasta' + ).path() + pdb_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/mmcif_files' + ).path() + seqres_database_path = testing_data.Data( + '/data/zmmVol2/AF3/public_databases/pdb_seqres_2022_09_28.fasta' + ).path() + else: + small_bfd_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/bfd-first_non_consensus_sequences__subsampled_1000.fasta' + ).path() + mgnify_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/mgy_clusters__subsampled_1000.fa' + ).path() + uniprot_cluster_annot_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/uniprot_all__subsampled_1000.fasta' + ).path() + uniref90_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/uniref90__subsampled_1000.fasta' + ).path() + ntrna_database_path = testing_data.Data( + resources.ROOT + / ('test_data/miniature_databases/' + 'nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq__subsampled_1000.fasta') + ).path() + rfam_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/rfam_14_4_clustered_rep_seq__subsampled_1000.fasta' + ).path() + rna_central_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/rnacentral_active_seq_id_90_cov_80_linclust__subsampled_1000.fasta' + ).path() + pdb_database_path = testing_data.Data( + resources.ROOT / 'test_data/miniature_databases/pdb_mmcif' + ).path() + seqres_database_path = testing_data.Data( + resources.ROOT + / 'test_data/miniature_databases/pdb_seqres_2022_09_28__subsampled_1000.fasta' + ).path() + + self._data_pipeline_config = pipeline.DataPipelineConfig( + jackhmmer_binary_path=_JACKHMMER_BINARY_PATH, + nhmmer_binary_path=_NHMMER_BINARY_PATH, + hmmalign_binary_path=_HMMALIGN_BINARY_PATH, + hmmsearch_binary_path=_HMMSEARCH_BINARY_PATH, + hmmbuild_binary_path=_HMMBUILD_BINARY_PATH, + small_bfd_database_path=small_bfd_database_path, + mgnify_database_path=mgnify_database_path, + uniprot_cluster_annot_database_path=uniprot_cluster_annot_database_path, + uniref90_database_path=uniref90_database_path, + ntrna_database_path=ntrna_database_path, + rfam_database_path=rfam_database_path, + rna_central_database_path=rna_central_database_path, + pdb_database_path=pdb_database_path, + seqres_database_path=seqres_database_path, + max_template_date=datetime.date(2021, 9, 30), + ) + self.data_path = "/data/zmmVol2/AF3/run_test/src/alphafold3/test_data" + + def compare_golden(self, result_path: str, golden_path) -> None: + filename = os.path.split(result_path)[1] + golden_path = pathlib.Path(golden_path) + with open(golden_path, 'r') as golden_file: + golden_text = golden_file.read() + with open(result_path, 'r') as result_file: + result_text = result_file.read() + + diff = _generate_diff(result_text, golden_text) + + self.assertEqual(diff, "", f"Result differs from golden:\n{diff}") + + def assertAuthorNamingSchemeEqual(self, ans1, ans2): # pylint: disable=invalid-name + """Walks naming scheme, making sure all elements are equal.""" + if ans1 is None or ans2 is None: + self.assertIsNone(ans1) + self.assertIsNone(ans2) + return + flat_ans1 = dict(tree.flatten_with_path(dataclasses.asdict(ans1))) + flat_ans2 = dict(tree.flatten_with_path(dataclasses.asdict(ans2))) + for k, v in flat_ans1.items(): + self.assertEqual(v, flat_ans2[k], msg=str(k)) + for k, v in flat_ans2.items(): + self.assertEqual(v, flat_ans1[k], msg=str(k)) + + def assertAllResiduesEqual(self, all_res1, all_res2): # pylint: disable=invalid-name + """Walks all residues, making sure alll elements are equal.""" + if all_res1 is None or all_res2 is None: + self.assertIsNone(all_res1) + self.assertIsNone(all_res2) + return + self.assertSameElements(all_res1.keys(), all_res2.keys()) + for chain_id, chain_res in all_res1.items(): + self.assertSequenceEqual( + chain_res, all_res2[chain_id], msg=chain_id) + + def assertBioassemblyDataEqual(self, data1, data2): # pylint: disable=invalid-name + if data1 is None or data2 is None: + self.assertIsNone(data1) + self.assertIsNone(data2) + return + self.assertDictEqual(data1.to_mmcif_dict(), data2.to_mmcif_dict()) + + def assertChemicalComponentsDataEqual( # pylint: disable=invalid-name + self, + data1, + data2, + allow_chem_comp_data_extension, + ): + """Checks whether two ChemicalComponentData objects are considered equal.""" + if data1 is None or data2 is None: + self.assertIsNone(data1) + self.assertIsNone(data2) + return + if (not allow_chem_comp_data_extension) or ( + data1.chem_comp.keys() ^ data2.chem_comp.keys() + ): + self.assertDictEqual(data1.chem_comp, data2.chem_comp) + else: + mismatching_values = [] + for component_id in data1.chem_comp: + found = data1.chem_comp[component_id] + expected = data2.chem_comp[component_id] + if not found.extends(expected): + mismatching_values.append((component_id, expected, found)) + + if mismatching_values: + mismatch_err_msgs = '\n'.join( + f'{component_id}: {expected} or its extension expected,' + f' but {found} found.' + for component_id, expected, found in mismatching_values + ) + self.fail( + f'Mismatching values for `_chem_comp` table: {mismatch_err_msgs}', + ) + + def assertBondsEqual(self, bonds1, bonds2, atom_key1, atom_key2): # pylint: disable=invalid-name + """Checks whether two Bonds objects are considered equal.""" + # An empty bonds table is functionally equivalent to an empty bonds table. + # NB: this can only ever be None in structure v1. + if bonds1 is None or not bonds1.size or bonds2 is None or not bonds2.size: + self.assertTrue(bonds1 is None or not bonds1.size, + msg=f'{bonds1=}') + self.assertTrue(bonds2 is None or not bonds2.size, + msg=f'{bonds2=}') + return + + ptnr1_indices1, ptnr2_indices1 = bonds1.get_atom_indices(atom_key1) + ptnr1_indices2, ptnr2_indices2 = bonds2.get_atom_indices(atom_key2) + np.testing.assert_array_equal(ptnr1_indices1, ptnr1_indices2) + np.testing.assert_array_equal(ptnr2_indices1, ptnr2_indices2) + np.testing.assert_array_equal(bonds1.type, bonds2.type) + np.testing.assert_array_equal(bonds1.role, bonds2.role) + + def assertStructuresEqual( # pylint: disable=invalid-name + self, + struc1, + struc2, + *, + ignore_fields=None, + allow_chem_comp_data_extension=False, + atol=0, + ): + """Checks whether two Structure objects could be considered equal. + + Args: + struc1: First Structure object. + struc2: Second Structure object. + ignore_fields: Fields not taken into account during comparison. + allow_chem_comp_data_extension: Whether to allow data of `_chem_comp` + table to differ if `struc2` is missing some fields, but `struc1` has + specific values for them. + atol: Absolute tolerance for floating point comparisons (in + np.testing.assert_allclose). + """ + for field in sorted(structure.GLOBAL_FIELDS): + if ignore_fields and field in ignore_fields: + continue + if field == 'author_naming_scheme': + self.assertAuthorNamingSchemeEqual( + struc1[field], struc2[field]) + elif field == 'all_residues': + self.assertAllResiduesEqual(struc1[field], struc2[field]) + elif field == 'bioassembly_data': + self.assertBioassemblyDataEqual(struc1[field], struc2[field]) + elif field == 'chemical_components_data': + self.assertChemicalComponentsDataEqual( + struc1[field], struc2[field], allow_chem_comp_data_extension + ) + elif field == 'bonds': + self.assertBondsEqual( + struc1.bonds, struc2.bonds, struc1.atom_key, struc2.atom_key + ) + else: + self.assertEqual(struc1[field], struc2[field], msg=field) + + # The chain order within a structure is arbitrary so in order to + # directly compare arrays we first align struc1 to struc2 and check that + # the number of atoms doesn't change. + num_atoms = struc1.num_atoms + self.assertEqual(struc2.num_atoms, num_atoms) + struc1 = struc1.order_and_drop_atoms_to_match(struc2) + self.assertEqual(struc1.num_atoms, num_atoms) + + for field in sorted(structure.ARRAY_FIELDS): + if field == 'atom_key': + # atom_key has no external meaning, so it doesn't matter whether it + # differs between two structures. + continue + if ignore_fields and field in ignore_fields: + continue + self.assertEqual(struc1[field] is None, + struc2[field] is None, msg=field) + + if np.issubdtype(struc1[field].dtype, np.inexact): + np.testing.assert_allclose( + struc1[field], struc2[field], err_msg=field, atol=atol + ) + else: + np.testing.assert_array_equal( + struc1[field], struc2[field], err_msg=field + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention.py new file mode 100644 index 000000000..4d3977508 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention.py @@ -0,0 +1,77 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from typing import Literal, TypeAlias +import typing +import alphafold3.utils.attention.attention_base as base +import alphafold3.utils.attention.ms_attention as ms_attention + +Implementation: TypeAlias = Literal["ms"] + + +def dot_product_attention(query, key, value, *, bias, mask, implementation, + logits_dtype=None, precision=None): + """Performs scaled dot-product attention. + + Scaled dot-product attention from "Attention is all you need" + https://arxiv.org/abs/1706.03762. + + Computes self- or cross-attention. The following is computed: + softmax(qk_scale * query @ key^T + bias) @ value. + + Supports both multi-head and multi-query attention + (https://arxiv.org/abs/1911.02150). + + Arguments: + query: Query array of shape `[batch, seq_len_q, num_heads, head_dim]`. + key: Key array of shape `[batch, seq_len_kv, num_heads, head_dim]`. + `num_heads` can be 1 for multi-query attention. + value: Value array of shape `[batch, seq_len_kv, num_heads, head_dim]`. + `num_heads` can be 1 for multi-query attention. + bias: Optional bias array, broadcastable to shape `[batch, num_heads, + seq_len_q, seq_len_kv]`. + mask: Optional boolean mask, broadcastable to `[batch, num_heads, seq_len_q, + seq_len_kv]`. Attention weights are masked out if the corresponding mask + value is `False`. + implementation: if `None` (default), an implementation is automatically + chosen. 'ms' will use standard MS and work on any platform. + logits_dtype: Data type for attention logits (`query @ key^T`). If `None` is + passed (the default), the accumulator type from the `query @ key^T` dot + product will be used, which is FP32 for BF16/FP16/FP32 inputs. Note that + this default increases the memory usage for BF16/FP16 inputs when using + `implementation='ms'`. + precision: The precision for the dot products. Either a single or a tuple + of `DEFAULT` precision. + + Returns: + An array with the same shape as `query`. + """ + + if implementation is not None: + named_args = typing.get_args(Implementation) + if implementation not in named_args: + raise ValueError( + f"Unsupported named implementation. Must be one of {named_args}." + ) + + logits_dtype = base.AUTO if logits_dtype is None else logits_dtype + precision = "DEFAULT" if precision is None else precision + + args = (query, key, value) + kwargs = dict( + precision=precision, + logits_dtype=logits_dtype, + bias=bias, + mask=mask, + ) + + return ms_attention.MsDotProductAttention()(*args, **kwargs) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_base.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_base.py new file mode 100644 index 000000000..2bd41f540 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_base.py @@ -0,0 +1,269 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +import abc +import enum +import math +import dataclasses +import functools +from dataclasses import dataclass, KW_ONLY +from typing import Any +import numpy as np +import mindspore as ms +from mindspore import ops, Tensor +from alphafold3.utils.common import precision as precision_lib + + +@dataclasses.dataclass(frozen=True) +class Mask: + """An attention mask. + + `k_start` (inclusive) and `k_end` (exclusive) define range of enabled + k-sequence values for each row of logits. + + For example, a local attention mask could be defined as follows: + ``` + seq_len_q = seq_len_k = 4 + window_size = 2 + k_start = Tensor(np.maximum(0, np.arange(seq_len_q) + 1 - window_size)) + mask = Mask(k_start=k_start, is_causal=True) + assert mask.as_array(seq_len_q, seq_len_k) == Tensor(np.array( + [[1, 0, 0, 0], + [1, 1, 0, 0], + [0, 1, 1, 0], + [0, 0, 1, 1]], dtype=bool)) + ``` + """ + bool_mask: ms.Tensor | None = None + _: dataclasses.KW_ONLY + q_start: ms.Tensor | None = None + q_end: ms.Tensor | None = None + k_start: ms.Tensor | None = None + k_end: ms.Tensor | None = None + is_causal: bool = False + + def tree_flatten(self): + return ( + self.bool_mask, + self.q_start, + self.q_end, + self.k_start, + self.k_end, + ), (self.is_causal,) + + @classmethod + def tree_unflatten(cls, aux, children): + (is_causal,) = aux + bool_mask, q_start, q_end, k_start, k_end = children + return cls( + bool_mask, + q_start=q_start, + q_end=q_end, + k_start=k_start, + k_end=k_end, + is_causal=is_causal, + ) + + def as_array(self, q_len_or_indices, k_len_or_indices): + """Returns the mask as a boolean array.""" + q_indices = ops.arange(q_len_or_indices) if isinstance( + q_len_or_indices, int) else q_len_or_indices + q_indices = q_indices[..., None] + + k_indices = ops.arange(k_len_or_indices) if isinstance( + k_len_or_indices, int) else k_len_or_indices + k_indices = k_indices[..., None, :] + + mask = [] + if self.bool_mask is not None: + mask.append(self.bool_mask) + + if self.q_start is not None: + mask.append(q_indices >= self.q_start[..., None, :]) + + if self.q_end is not None: + mask.append(q_indices < self.q_end[..., None, :]) + + if self.k_start is not None: + mask.append(k_indices >= self.k_start[..., None]) + + if self.k_end is not None: + mask.append(k_indices < self.k_end[..., None]) + + if self.is_causal: + mask.append(q_indices >= k_indices) + + logical_and = functools.partial(functools.reduce, ops.logical_and) + + if mask: + return logical_and(mask) + else: + return None + + def take(self, *attrs): + """Returns a mask with attrs removed and the removed attrs.""" + default_mask = type(self)() + replacements = {attr: getattr(default_mask, attr) for attr in attrs} + values = (getattr(self, attr) for attr in attrs) + return dataclasses.replace(self, **replacements), *values + + def __and__(self, other): + """Returns the intersection of two masks.""" + if not isinstance(other, Mask): + other = Mask(other) + + def combine(op): + return lambda a, b: b if a is None else a if b is None else op(a, b) + + return Mask( + bool_mask=combine(ops.logical_and)( + self.bool_mask, other.bool_mask), + q_end=combine(ops.minimum)(self.q_end, other.q_end), + k_start=combine(ops.maximum)(self.k_start, other.k_start), + k_end=combine(ops.minimum)(self.k_end, other.k_end), + is_causal=self.is_causal or other.is_causal, + ) + + +CAUSAL_MASK = Mask(is_causal=True) + + +@enum.unique +class SoftmaxResidualMode(enum.Enum): + """The mode of storing softmax residuals for the backwards pass. + + The stable softmax calculation performs two reductions calculating: + - the maximum input value (`x_max`), + - the sum of exponentiated values (`denom`). + + We can store these values as residuals to avoid the need to recompute them + in the backwards pass. + + It is also possible to combine the two residuals into a single residual, + `res = x_max + log(denom)`, as `exp(x - res) === exp(x - x_max - log(denom)) + === exp(x - x_max) / denom`. Combining the residuals reduces the memory usage + of the residuals, but will reduce the accuracy of the backwards pass if + `abs(x_max) >> log(denom)`. + """ + + SEPARATE = "separate" + COMBINED = "combined" + + def conform(self, aux): + match self, aux: + case None, _: + return None + case SoftmaxResidualMode.SEPARATE, (_, _): + return aux + case SoftmaxResidualMode.SEPARATE, _: # pytype: disable=redundant-match # b/300135240 + raise ValueError("`aux` has been combined.") + case SoftmaxResidualMode.COMBINED, (x_max, denom): + return x_max + ops.log(denom) + case SoftmaxResidualMode.COMBINED, _: # pytype: disable=redundant-match # b/300135240 + return aux + + +class DotProductAttention(abc.ABC): + """Dot product attention function.""" + + def __call__(self, query, key, value, *, precision, logits_dtype, bias, mask, q_indices=None, k_indices=None): + """Performs scaled dot-product attention. + + Scaled dot-product attention from "Attention is all you need" + https://arxiv.org/abs/1706.03762. + + Computes self- or cross-attention. The following is computed: + softmax(qk_scale * query @ key^T + bias) @ value. + + Supports both multi-head and multi-query attention + (https://arxiv.org/abs/1911.02150). + + Arguments: + query: Query array of shape `[batch, seq_len_q, num_heads_q, head_dim]`. + It must be a multiple of num_heads_kv. + Here's an example of how q/kv heads are interleaved: + For 8 key/value heads and 4 query heads: + - key/value heads [0, 1] see query head 0 + - key/value heads [2, 3] see query head 1 + - key/value heads [4, 5] see query head 2 + key: Key array of shape `[batch, seq_len_kv, num_heads_kv, head_dim]`. It + must be divisible by num_heads_q. + value: Value array of shape `[batch, seq_len_kv, num_heads_kv, head_dim]`. + precision: The precision for the dot products. Either a tuple `( + query_key_dot_precision, weights_value_dot_precision)` or a single + precision applied to both dot products. + logits_dtype: Data type for attention logits (`query @ key^T`). If `AUTO` + is passed (the default), the accumulator type from the `query @ key^T` + dot product will be used. + bias: Optional bias array, broadcastable to shape `[batch, num_heads, + seq_len_q, seq_len_kv]`. + mask: Optional boolean mask, broadcastable to `[batch, num_heads, + seq_len_q, seq_len_kv]`. Attention weights are masked out if the + corresponding mask value is `False`. + q_indices: Optional indices for each token in query sequence. + k_indices: Optional indices for each token in key/value sequence. + + Returns: + An array with the same shape as `query`. + """ + return self.fwd( + query, + key, + value, + precision=precision, + logits_dtype=logits_dtype, + bias=bias, + mask=mask, + q_indices=q_indices, + k_indices=k_indices, + ) + + def fwd(self, query, key, value, *, precision, logits_dtype, bias, mask, q_indices, k_indices): + """Performs attention.""" + if not isinstance(precision, tuple): + precision = (precision, precision) + + q_k_dot_precision, weights_v_dot_precision = precision + + if not isinstance(q_k_dot_precision, precision_lib.DotPrecision): + q_k_dot_precision = precision_lib.get_equivalent_dot_precision( + query.dtype, key.dtype, q_k_dot_precision + ) + + if not isinstance(weights_v_dot_precision, precision_lib.DotPrecision): + weights_v_dot_precision = precision_lib.get_equivalent_dot_precision( + value.dtype, value.dtype, weights_v_dot_precision + ) + + if not isinstance(mask, Mask): + mask = Mask(mask) + + return self._fwd( + Tensor(query), + Tensor(key), + Tensor(value), + q_k_dot_precision=q_k_dot_precision, + logits_dtype=logits_dtype, + logits_scale=1 / math.sqrt(query.shape[-1]), + bias=bias, + mask=mask, + weights_v_dot_precision=weights_v_dot_precision, + q_indices=q_indices, + k_indices=k_indices, + ) + + @abc.abstractmethod + def _fwd(self, q, k, v, *, q_k_dot_precision, logits_dtype, logits_scale, bias, mask, + weights_v_dot_precision, q_indices, k_indices): + """Performs attention.""" + ... diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_call_arg_specs.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_call_arg_specs.py new file mode 100644 index 000000000..6e8db1bde --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_call_arg_specs.py @@ -0,0 +1,61 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Attention call argument specifications. + +Attention argument specifications used by users of the library. +They are the most important test cases, and also cases for optimize +performance of via autotuning. +""" + +from typing import Any + + +def _make_argspec( + *, + q_shape, + dtype, + k_shape=None, + v_shape=None, + bias_shape=None, + mask_shape=None, + **kwargs, +) -> dict[str, Any]: + """Make argspec from shapes and kwargs.""" + if k_shape is None: + k_shape = q_shape + if v_shape is None: + v_shape = k_shape + + return dict( + query=q_shape, + key=k_shape, + value=v_shape, + bias=bias_shape, + mask=mask_shape, + dtype=dtype, + **kwargs, + ) + + +# A subset of the full set of argument specifications. Useful for tap-tests and +# microbenchmarks. +CALL_ARG_SPECS = dict( + vanilla_f32=_make_argspec(q_shape=(8, 1024, 4, 128), dtype='float32'), + vanilla_bf16=_make_argspec(q_shape=(8, 1024, 4, 128), dtype='bfloat16'), + alphafold=_make_argspec( + q_shape=(384, 384, 4, 32), + bias_shape=(1, 4, 384, 384), + mask_shape=(384, 1, 1, 384), + dtype='bfloat16', + ), +) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py new file mode 100644 index 000000000..835d08644 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py @@ -0,0 +1,96 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +import dataclasses +import mindspore as ms +from mindspore import ops +import alphafold3.utils.attention.attention_base as base + + +def _softmax(x): + """Computes softmax.""" + dtype = ms.float32 + x_max, _ = ops.max(x.astype(dtype), axis=-1, keepdims=True) + unnormalized = ops.exp(x - x_max) + denom = ops.sum(unnormalized, dim=-1, keepdim=True) + return (unnormalized / denom).astype(x.dtype) + + +def cal_logits(q, k, use_bf16=False): + # ...qhd,...khd->...hqk + dtype = q.dtype + if use_bf16: + q = q.astype(ms.bfloat16) + k = k.astype(ms.bfloat16) + q_trans = ops.transpose(q, (0, 2, 1, 3)) # ...qhd -> ...hqd + k_trans = ops.transpose(k, (0, 2, 3, 1)) # ...khd -> ...hdk + logits = ops.matmul(q_trans, k_trans) + if use_bf16: + logits = logits.astype(dtype) + return logits + + +def cal_out(weights, v, use_bf16=False): + # ...hqk,...khd->...qhd + if use_bf16: + weights = weights.astype(ms.bfloat16) + v = v.astype(ms.bfloat16) + v_trans = ops.transpose(v, (0, 2, 1, 3)) # ...khd -> ...hkd + out_temp = ops.matmul(weights, v_trans) # ...hqk,...hkd->...hqd + out = ops.transpose(out_temp, (0, 2, 1, 3)) + return out + + +def _attend( + q, k, v, *, q_k_dot_precision, logits_dtype, logits_scale, + bias, mask, weights_v_dot_precision, q_indices, k_indices, +): + logits = cal_logits(q, k) + + logits *= logits_scale + + if bias is not None: + logits += bias + + if mask is not None: + q_len_or_indices = q.shape[-3] if q_indices is None else q_indices + k_len_or_indices = k.shape[-3] if k_indices is None else k_indices + mask = mask.as_array(q_len_or_indices, k_len_or_indices) + + if mask is not None: # TBD in ms + mask_value = -3.4028235e+37 # a small value close to min of bfloat16 + logits = ops.where(mask.bool(), logits, mask_value) + + weights = _softmax(logits) + + out = cal_out(weights, v) + + return out + + +@dataclasses.dataclass(frozen=True) +class MsDotProductAttention(base.DotProductAttention): + """MS dot product attention function.""" + + _: dataclasses.KW_ONLY + + def _fwd( + self, q, k, v, *, q_k_dot_precision, logits_dtype, logits_scale, + bias, mask, weights_v_dot_precision, q_indices, k_indices, + ): + + return _attend( + q, k, v, bias=bias, mask=mask, q_indices=q_indices, k_indices=k_indices, + q_k_dot_precision=q_k_dot_precision, logits_dtype=logits_dtype, logits_scale=logits_scale, + weights_v_dot_precision=weights_v_dot_precision, + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/common/precision.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/common/precision.py new file mode 100644 index 000000000..b4b299dcd --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/common/precision.py @@ -0,0 +1,91 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Precision classes and utilities.""" + +import enum +import mindspore as ms + + +@enum.unique +class DotPrecision(enum.Enum): + """Precision for `dot` operation. + + Naming scheme: {OPERAND_DTYPE}_{ACCUMULATOR_DTYPE}[_{NUM_PASSES}x] + """ + + BF16_F32 = "bf16_f32" + + # NPU only precisions. + F32_F32 = "f32_f32" # Full f32 precision (doesn't use TensorCores). + F16_F16 = "f16_f16" + F16_F32 = "f16_f32" + + @property + def operand_dtype(self) -> ms.dtype: + match self: + case DotPrecision.BF16_F32: + return ms.bfloat16 + case DotPrecision.F16_F16 | DotPrecision.F16_F32: + return ms.float16 + case _: + return ms.float32 + + @property + def accumulator_dtype(self) -> ms.dtype: + return ms.float16 if (self == DotPrecision.F16_F16) else ms.float32 + + +_MS_NPU_PRECISION_MAP = { + (ms.float16, "DEFAULT"): DotPrecision.F16_F32, + (ms.bfloat16, "DEFAULT"): DotPrecision.BF16_F32, + (ms.float32, "DEFAULT"): DotPrecision.F32_F32, + (ms.float32, "HIGH"): DotPrecision.F32_F32, + (ms.float32, "HIGHEST"): DotPrecision.F32_F32, +} + +_MS_CPU_PRECISION_MAP = { + (ms.float16, "DEFAULT"): DotPrecision.F16_F32, + (ms.bfloat16, "DEFAULT"): DotPrecision.F32_F32, + (ms.float32, "DEFAULT"): DotPrecision.F32_F32, + (ms.float32, "HIGH"): DotPrecision.F32_F32, + (ms.float32, "HIGHEST"): DotPrecision.F32_F32, +} + + +def _create_ms_precision_map(): + precision_map = {} + for (dtype, ms_precision), dot_precision in _MS_NPU_PRECISION_MAP.items(): + precision_map[("ascend", dtype, ms_precision)] = dot_precision + for (dtype, ms_precision), dot_precision in _MS_CPU_PRECISION_MAP.items(): + precision_map[("cpu", dtype, ms_precision)] = dot_precision + return precision_map + + +_MS_PRECISION_MAP = _create_ms_precision_map() + + +def get_equivalent_dot_precision( + a_dtype: ms.dtype, b_dtype: ms.dtype, ms_precision: str +) -> DotPrecision: + """Returns `DotPrecision` replicating default behaviour.""" + if a_dtype != b_dtype: + raise ValueError("Cannot infer precision if operand types differ.") + + backend = ms.context.get_context("device_target").lower() + if (ms_precision != "DEFAULT") and (a_dtype != ms.float32): + raise ValueError( + "`Precision` values other than `DEFAULT` only have an effect if" + " the operand type is `float32`." + ) + return _MS_PRECISION_MAP[(backend, a_dtype, ms_precision)] diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit.py new file mode 100644 index 000000000..5e7f3718e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit.py @@ -0,0 +1,66 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Public API for gated linear unit functions.""" + +import typing +from typing import Literal, TypeAlias +from alphafold3.utils.gated_linear_unit import gated_linear_unit_base + +Implementation: TypeAlias = Literal['ms'] + + +def gated_linear_unit(x, weight, *, activation, precision, implementation=None): + """Applies a gated linear unit (https://arxiv.org/abs/1612.08083). + + Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`. + + This is SwiGLU when `activation=swish`, GEGLU when + `activation=gelu`, REGLU when `activation=relu`, and GLU when + `activation=sigmoid` (https://arxiv.org/abs/2002.05202). + + Args: + x: the input array. + weight: the combined weight array. + activation: optional activation function. + precision: specifies the matrix multiplication precision. Either `None` + (default), which means the default precision for the backend, or an + enum of "DEFAULT/HIGH/...". + implementation: if `None` (default), an implementation is automatically + chosen. 'ms' will use standard MS and work on any platform. + + Raises: + ValueError: if the arguments are invalid. + + Returns: + The output array. + """ + + if x.dtype != weight.dtype: + raise ValueError( + f'Input and weight must have the same dtype. {x.dtype} !=' + f' {weight.dtype}' + ) + + if implementation is not None: + named_args = typing.get_args(Implementation) + if implementation not in named_args: + raise ValueError( + f'Unsupported named implementation. Must be one of {named_args}.' + ) + + return gated_linear_unit_base.gated_linear_unit_ms( + x=x, + weight=weight, + activation=activation, + precision=precision, + ) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py new file mode 100644 index 000000000..afa6406f9 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py @@ -0,0 +1,84 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Common types for gated linear unit kernels.""" +import abc +import mindspore as ms +from mindspore import mint + + +class GatedLinearUnit(abc.ABC): + """Gated linear unit.""" + + def __call__(self, x, weight, *, activation, precision, **kwargs): + """Applies a gated linear unit (https://arxiv.org/abs/1612.08083). + + Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`. + + This is SwiGLU when `activation=swish`, GEGLU when + `activation=gelu`, REGLU when `activation=relu`, and GLU when + `activation=sigmoid` (https://arxiv.org/abs/2002.05202). + + Args: + x: the input array. + weight: the combined weight array. + activation: optional activation function. + precision: specifies the matrix multiplication precision. Either `None` + (default), which means the default precision for the backend, or an + enum of "DEFAULT/HIGH/...". + + Returns: + The output array. + """ + + return self._fwd( + x, weight, activation=activation, precision=precision, **kwargs + ) + + @abc.abstractmethod + def _fwd(self, x, weight, *, activation, precision): + """Gated linear unit.""" + ... + + +def gated_linear_unit_ms(x, weight, *, activation, precision=None): + """Applies a gated linear unit (https://arxiv.org/abs/1612.08083). + + Computes `activation(x @ weight[:, 0]) * x @ weight[:, 1]`. + + This is SwiGLU when `activation=swish`, GEGLU when + `activation=gelu`, REGLU when `activation=relu`, and GLU when + `activation=sigmoid` (https://arxiv.org/abs/2002.05202). + + Args: + x: the input array. + weight: the combined weight array. + activation: optional activation function. + precision: specifies the matrix multiplication precision. Either `None` + (default), which means the default precision for the backend, or an + enum of "DEFAULT/HIGH/...". + + Returns: + The output array. + """ + + weight_reshaped = mint.reshape( + weight, (-1, weight.shape[-2] * weight.shape[-1])) + # y = ops.dot(x.astype('float32'), weight_reshaped.astype('float32')) + y1 = mint.matmul(x, weight_reshaped) + y = y1.astype(ms.float32) + a, b = y.split(y.shape[-1] // 2, axis=-1) + out = mint.mul(a, b) if activation is None else mint.mul(activation(a), b) + out = out.astype(x.dtype) + + return out diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py new file mode 100644 index 000000000..910ccfe9f --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +from alphafold3.utils.geometry import rigid_matrix_vector +from alphafold3.utils.geometry import rotation_matrix +from alphafold3.utils.geometry import struct_of_array +from alphafold3.utils.geometry import vector + +Rot3Array = rotation_matrix.Rot3Array +Rigid3Array = rigid_matrix_vector.Rigid3Array + +StructOfArray = struct_of_array.StructOfArray + +Vec3Array = vector.Vec3Array +square_euclidean_distance = vector.square_euclidean_distance +euclidean_distance = vector.euclidean_distance +dihedral_angle = vector.dihedral_angle +dot = vector.dot +cross = vector.cross diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rigid_matrix_vector.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rigid_matrix_vector.py new file mode 100644 index 000000000..6faf4e062 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rigid_matrix_vector.py @@ -0,0 +1,194 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Rigid3Array Transformations represented by a Matrix and a Vector.""" + +from typing import Any, Final, TypeAlias +import mindspore as ms +import mindspore.numpy as mnp +from mindspore import Tensor +from mindspore.ops import operations as P + +from alphafold3.utils.geometry import rotation_matrix, struct_of_array, utils, vector + +Float: TypeAlias = float | Tensor + +VERSION: Final[str] = '0.1' + + +def _compute_covariance_matrix( + row_values: vector.Vec3Array, + col_values: vector.Vec3Array, + weights: Tensor, + epsilon=1e-6, +) -> Tensor: + """Compute covariance matrix.""" + weights = mnp.asarray(weights) + + weights = mnp.broadcast_to(weights, row_values.shape) + + normalized_weights = weights / \ + (mnp.sum(weights, axis=-1, keepdims=True) + epsilon) + + def weighted_average(x): + return mnp.sum(normalized_weights * x, axis=-1) + + out = [ + mnp.stack( + ( + weighted_average(row_values.x * col_values.x), + weighted_average(row_values.x * col_values.y), + weighted_average(row_values.x * col_values.z), + ), + axis=-1, + ) + ] + + out.append( + mnp.stack( + ( + weighted_average(row_values.y * col_values.x), + weighted_average(row_values.y * col_values.y), + weighted_average(row_values.y * col_values.z), + ), + axis=-1, + ) + ) + + out.append( + mnp.stack( + ( + weighted_average(row_values.z * col_values.x), + weighted_average(row_values.z * col_values.y), + weighted_average(row_values.z * col_values.z), + ), + axis=-1, + ) + ) + + return mnp.stack(out, axis=-2) + + +@struct_of_array.StructOfArray(same_dtype=True) +class Rigid3Array: + """Rigid Transformation, i.e. element of special euclidean group.""" + + rotation: rotation_matrix.Rot3Array + translation: vector.Vec3Array + + def __matmul__(self, other: 'Rigid3Array') -> 'Rigid3Array': + new_rotation = self.rotation @ other.rotation + new_translation = self.apply_to_point(other.translation) + return Rigid3Array(new_rotation, new_translation) + + def inverse(self) -> 'Rigid3Array': + """Return Rigid3Array corresponding to inverse transform.""" + inv_rotation = self.rotation.inverse() + inv_translation = inv_rotation.apply_to_point(-self.translation) + return Rigid3Array(inv_rotation, inv_translation) + + def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: + """Apply Rigid3Array transform to point.""" + return self.rotation.apply_to_point(point) + self.translation + + def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: + """Apply inverse Rigid3Array transform to point.""" + new_point = point - self.translation + return self.rotation.apply_inverse_to_point(new_point) + + def compose_rotation(self, other_rotation: rotation_matrix.Rot3Array) -> 'Rigid3Array': + rot = self.rotation @ other_rotation + trans = P.BroadcastTo(rot.shape)(self.translation) + return Rigid3Array(rot, trans) + + @classmethod + def identity(cls, shape: Any, dtype: ms.dtype = ms.float32) -> 'Rigid3Array': + """Return identity Rigid3Array of given shape.""" + + return cls( + rotation_matrix.Rot3Array.identity(shape, dtype=dtype), + vector.Vec3Array.zeros(shape, dtype=dtype), + ) + + def scale_translation(self, factor: Float) -> 'Rigid3Array': + """Scale translation in Rigid3Array by 'factor'.""" + return Rigid3Array(self.rotation, self.translation * factor) + + def to_array(self): + rot_array = self.rotation.to_array() + vec_array = self.translation.to_array() + return mnp.concatenate([rot_array, vec_array[..., None]], axis=-1) + + @classmethod + def from_array(cls, array): + rot = rotation_matrix.Rot3Array.from_array(array[..., :3]) + vec = vector.Vec3Array.from_array(array[..., -1]) + return cls(rot, vec) + + @classmethod + def from_array4x4(cls, array: Tensor) -> 'Rigid3Array': + """Construct Rigid3Array from homogeneous 4x4 array.""" + if array.shape[-2:] != (4, 4): + raise ValueError(f'array.shape({array.shape}) must be [..., 4, 4]') + rotation = rotation_matrix.Rot3Array( + *(array[..., 0, 0], array[..., 0, 1], array[..., 0, 2]), + *(array[..., 1, 0], array[..., 1, 1], array[..., 1, 2]), + *(array[..., 2, 0], array[..., 2, 1], array[..., 2, 2]), + ) + translation = vector.Vec3Array( + array[..., 0, 3], array[..., 1, 3], array[..., 2, 3] + ) + return cls(rotation, translation) + + @classmethod + def from_point_alignment( + cls, + points_to: vector.Vec3Array, + points_from: vector.Vec3Array, + weights: Float | None = None, + epsilon: float = 1e-6, + ) -> 'Rigid3Array': + """Constructs Rigid3Array by finding transform aligning points.""" + if weights is None: + weights = 1.0 + + def compute_center(value): + return utils.weighted_mean(value=value, weights=weights, axis=-1) + + points_to_center = P.Map()(compute_center, points_to) + points_from_center = P.Map()(compute_center, points_from) + centered_points_to = points_to - points_to_center[..., None] + centered_points_from = points_from - points_from_center[..., None] + cov_mat = _compute_covariance_matrix( + centered_points_to, + centered_points_from, + weights=weights, + epsilon=epsilon, + ) + rots = rotation_matrix.Rot3Array.from_svd( + mnp.reshape(cov_mat, cov_mat.shape[:-2] + (9,)) + ) + + translations = points_to_center - \ + rots.apply_to_point(points_from_center) + + return cls(rots, translations) + + def __getstate__(self): + return (VERSION, (self.rotation, self.translation)) + + def __setstate__(self, state): + version, (rot, trans) = state + del version + object.__setattr__(self, 'rotation', rot) + object.__setattr__(self, 'translation', trans) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rotation_matrix.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rotation_matrix.py new file mode 100644 index 000000000..b9c91a598 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rotation_matrix.py @@ -0,0 +1,255 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Rot3Array Matrix Class.""" + +import dataclasses +from typing import Any, Final +import numpy as np +import mindspore as ms +import mindspore.numpy as mnp +from mindspore import ops, mint +from mindspore import Tensor +from alphafold3.utils.geometry import struct_of_array, utils, vector + +COMPONENTS: Final[tuple[str, ...]] = ( + *('xx', 'xy', 'xz'), + *('yx', 'yy', 'yz'), + *('zx', 'zy', 'zz'), +) +VERSION: Final[str] = '0.1' + + +def make_matrix_svd_factors() -> Tensor: + """Generates factors for converting 3x3 matrix to symmetric 4x4 matrix.""" + factors = mnp.zeros((16, 9), dtype=ms.float32) + + indices = [(0, [0, 4, 8]), ([1, 4], 5), ([1, 4], 7), ([2, 8], 6), ([2, 8], 2), + ([3, 12], 1), ([3, 12], 3), (5, 0), (5, [4, 8]), + ([6, 9], 1), ([6, 9], 3), ([7, 13], 2), ([7, 13], 6), + (10, 4), (10, [0, 8]), ([11, 14], 5), ([11, 14], 7), (15, 8), (15, [0, 4])] + + values = [[1.0], [1.0, -1.0], [1.0, -1.0], [1.0, -1.0], [1.0, -1.0], + [1.0, -1.0], [1.0, 1.0], [1.0, -1.0], [-1.0, -1.0], + [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0], + [1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [1.0, 1.0]] + + for idx, val in zip(indices, values): + if isinstance(idx[1], list): + for i in idx[1]: + factors[idx[0], i] = val[i % len(val)] + else: + factors[idx[0], idx[1]] = val[0] + + return factors + + +def largest_evec(m): + _, eigvecs = np.linalg.eigh(m.asnumpy()) + + return Tensor(eigvecs[..., -1]) + + +MATRIX_SVD_QUAT_FACTORS = make_matrix_svd_factors() + + +@struct_of_array.StructOfArray(same_dtype=True) +class Rot3Array: + """Rot3Array Matrix in 3 dimensional Space implemented as struct of arrays.""" + + xx: Tensor = dataclasses.field(metadata={'dtype': ms.float32}) + xy: Tensor + xz: Tensor + yx: Tensor + yy: Tensor + yz: Tensor + zx: Tensor + zy: Tensor + zz: Tensor + + __array_ufunc__ = None + + def inverse(self): + """Returns inverse of Rot3Array.""" + return Rot3Array( + *(self.xx, self.yx, self.zx), + *(self.xy, self.yy, self.zy), + *(self.xz, self.yz, self.zz), + ) + + def apply_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: + """Applies Rot3Array to point.""" + x = self.xx * point.x + self.xy * point.y + self.xz * point.z + y = self.yx * point.x + self.yy * point.y + self.yz * point.z + z = self.zx * point.x + self.zy * point.y + self.zz * point.z + return vector.Vec3Array(x, y, z) + + def apply_inverse_to_point(self, point: vector.Vec3Array) -> vector.Vec3Array: + """Applies inverse Rot3Array to point.""" + return self.inverse().apply_to_point(point) + + def __matmul__(self, other): + """Composes two Rot3Arrays.""" + c0 = self.apply_to_point( + vector.Vec3Array(other.xx, other.yx, other.zx)) + c1 = self.apply_to_point( + vector.Vec3Array(other.xy, other.yy, other.zy)) + c2 = self.apply_to_point( + vector.Vec3Array(other.xz, other.yz, other.zz)) + return Rot3Array(c0.x, c1.x, c2.x, c0.y, c1.y, c2.y, c0.z, c1.z, c2.z) + + @classmethod + def identity(cls, shape: Any, dtype: ms.dtype = ms.float32): + """Returns identity of given shape.""" + ones = mint.ones(shape, dtype=dtype) + zeros = mint.zeros(shape, dtype=dtype) + + temp = cls(ones, zeros, zeros, zeros, ones, zeros, zeros, zeros, ones) + return temp + + @classmethod + def from_two_vectors(cls, e0: vector.Vec3Array, e1: vector.Vec3Array): + """Construct Rot3Array from two Vectors. + + Rot3Array is constructed such that in the corresponding frame 'e0' lies on + the positive x-Axis and 'e1' lies in the xy plane with positive sign of y. + + Args: + e0: Vector + e1: Vector + + Returns: + Rot3Array + """ + # Normalize the unit vector for the x-axis, e0. + e0 = e0.normalized() + # Make e1 perpendicular to e0. + c = e1.dot(e0) + e1 = (e1 - e0 * c).normalized() + # Compute e2 as cross product of e0 and e1. + e2 = e0.cross(e1) + return cls(e0.x, e1.x, e2.x, e0.y, e1.y, e2.y, e0.z, e1.z, e2.z) + + @classmethod + def from_array(cls, array: Tensor): + """Construct Rot3Array Matrix from array of shape [..., 3, 3].""" + unstacked = utils.unstack(array, axis=-2) + unstacked = sum([utils.unstack(x, axis=-1) for x in unstacked], []) + return cls(*unstacked) + + def to_array(self) -> Tensor: + """Convert Rot3Array to array of shape [..., 3, 3].""" + return ops.stack( + [ + ops.stack([self.xx, self.xy, self.xz], axis=-1), + ops.stack([self.yx, self.yy, self.yz], axis=-1), + ops.stack([self.zx, self.zy, self.zz], axis=-1), + ], + axis=-2, + ) + + @classmethod + def from_quaternion( + cls, + w: Tensor, + x: Tensor, + y: Tensor, + z: Tensor, + normalize: bool = True, + epsilon: float = 1e-6, + ): + """Construct Rot3Array from components of quaternion.""" + if normalize: + inv_norm = ops.rsqrt(ops.maximum( + epsilon, w**2 + x**2 + y**2 + z**2)) + w *= inv_norm + x *= inv_norm + y *= inv_norm + z *= inv_norm + xx = 1 - 2 * (y**2 + z**2) + xy = 2 * (x * y - w * z) + xz = 2 * (x * z + w * y) + yx = 2 * (x * y + w * z) + yy = 1 - 2 * (x**2 + z**2) + yz = 2 * (y * z - w * x) + zx = 2 * (x * z - w * y) + zy = 2 * (y * z + w * x) + zz = 1 - 2 * (x**2 + y**2) + return cls(xx, xy, xz, yx, yy, yz, zx, zy, zz) + + @classmethod + def from_svd(cls, mat: Tensor, use_quat_formula: bool = True): + """Constructs Rot3Array from arbitrary array of shape [3 * 3] using SVD. + + The case when 'use_quat_formula' is False rephrases the problem of + projecting the matrix to a rotation matrix as a problem of finding the + largest eigenvector of a certain 4x4 matrix. This has the advantage of + having fewer numerical issues. + This approach follows: + https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.65.971&rep=rep1&type=pdf + In the other case we construct it via svd following + https://arxiv.org/pdf/2006.14616.pdf + In that case [∂L/∂M] is large if the two smallest singular values are close + to each other, or if they are close to 0. + + Args: + mat: Array of shape [..., 3 * 3] + use_quat_formula: Whether to construct matrix via 4x4 eigenvalue problem. + + Returns: + Rot3Array of shape [...] + """ + assert mat.shape[-1] == 9 + if use_quat_formula: + symmetric_4by4 = ops.einsum( + 'ji, ...i -> ...j', + MATRIX_SVD_QUAT_FACTORS, + mat, + ) + symmetric_4by4 = ops.reshape( + symmetric_4by4, mat.shape[:-1] + (4, 4)) + largest_eigvec = largest_evec(symmetric_4by4) + return cls.from_quaternion( + *utils.unstack(largest_eigvec, axis=-1) + ).inverse() + + mat = ops.reshape(mat, mat.shape[:-1] + (3, 3)) + u, _, v_t = np.linalg.svd(mat.asnumpy(), full_matrices=False) + u = Tensor(u) + v_t = Tensor(v_t) + det_uv_t = ops.det(ops.matmul(u, v_t)) + ones = ops.ones_like(det_uv_t) + diag_array = ops.stack([ones, ones, det_uv_t], axis=-1) + # This is equivalent to making diag_array into a diagonal array and matrix + # multiplying + diag_times_v_t = diag_array[..., None] * v_t + out = ops.matmul(u, diag_times_v_t) + return cls.from_array(out) + + @classmethod + def random_uniform(cls, key, shape, dtype=ms.float32): + """Samples uniform random Rot3Array according to Haar Measure.""" + stdnormal = ops.StandardNormal(seed=key) + quat_array = stdnormal(shape + (4,)).astype(dtype) + # quat_array = ops.StandardNormal()(shape=(tuple(shape) + (4,)), seed=key) + quats = utils.unstack(quat_array) + return cls.from_quaternion(*quats) + + def __getstate__(self): + return (VERSION, [getattr(self, field) for field in COMPONENTS]) + + def __setstate__(self, state): + version, state = state + del version + for i, field in enumerate(COMPONENTS): + object.__setattr__(self, field, state[i]) diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py new file mode 100644 index 000000000..80e872c17 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py @@ -0,0 +1,280 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Class decorator to represent (nested) struct of arrays.""" + +import dataclasses +import mindspore as ms + +def get_item(instance, key): + sliced = {} + for field in get_array_fields(instance): + num_trailing_dims = field.metadata.get('num_trailing_dims', 0) + this_key = key + if isinstance(key, tuple) and Ellipsis in this_key: + this_key += (slice(None),) * num_trailing_dims + + def apply_slice(x): + if isinstance(x, ms.Tensor): + return x[this_key] + elif isinstance(x, dict): + return {k: apply_slice(v) for k, v in x.items()} + elif isinstance(x, list): + return [apply_slice(item) for item in x] + else: + return x + + sliced[field.name] = apply_slice(getattr(instance, field.name)) + + return dataclasses.replace(instance, **sliced) + + +@property +def get_shape(instance): + """Returns Shape for given instance of dataclass.""" + first_field = dataclasses.fields(instance)[0] + num_trailing_dims = first_field.metadata.get('num_trailing_dims', None) + value = getattr(instance, first_field.name) + if num_trailing_dims: + return value.shape[:-num_trailing_dims] + else: + return value.shape + + +def get_len(instance): + """Returns length for given instance of dataclass.""" + shape = instance.shape + if shape: + return shape[0] + else: + # Match utils.numpy behavior. + raise TypeError('len() of unsized object') + + +@property +def get_dtype(instance): + """Returns Dtype for given instance of dataclass.""" + fields = dataclasses.fields(instance) + sets_dtype = [ + field.name for field in fields if field.metadata.get('sets_dtype', False) + ] + if sets_dtype: + assert len(sets_dtype) == 1, 'at most one field can set dtype' + field_value = getattr(instance, sets_dtype[0]) + elif instance.same_dtype: + field_value = getattr(instance, fields[0].name) + else: + raise AttributeError( + 'Trying to access Dtype on Struct of Array without' + 'either "same_dtype" or field setting dtype' + ) + + if hasattr(field_value, 'dtype'): + return field_value.dtype + else: + raise AttributeError(f'field_value {field_value} does not have dtype') + + +def replace(instance, **kwargs): + return dataclasses.replace(instance, **kwargs) + + +def post_init(instance): + """Validate instance has same shapes & dtypes.""" + array_fields = get_array_fields(instance) + arrays = list(get_array_fields(instance, return_values=True).values()) + first_field = array_fields[0] + try: + dtype = instance.dtype + except AttributeError: + dtype = None + if dtype is not None: + first_shape = instance.shape + for array, field in zip(arrays, array_fields, strict=True): + num_trailing_dims = field.metadata.get('num_trailing_dims', None) + if num_trailing_dims: + array_shape = array.shape + field_shape = array_shape[:-num_trailing_dims] + msg = ( + f'field {field} should have number of trailing dims' + ' {num_trailing_dims}' + ) + assert len(array_shape) == len( + first_shape) + num_trailing_dims, msg + else: + + field_shape = array.shape + + shape_msg = ( + f"Stripped Shape {field_shape} of field {field} doesn't " + f'match shape {first_shape} of field {first_field}' + ) + + assert field_shape == first_shape, shape_msg + + field_dtype = array.dtype + + allowed_metadata_dtypes = field.metadata.get('allowed_dtypes', []) + if allowed_metadata_dtypes: + msg = f'Dtype is {field_dtype} but must be in {allowed_metadata_dtypes}' + assert field_dtype in allowed_metadata_dtypes, msg + + if 'dtype' in field.metadata: + target_dtype = field.metadata['dtype'] + else: + target_dtype = dtype + + msg = f'Dtype is {field_dtype} but must be {target_dtype}' + assert field_dtype == target_dtype, msg + + +def flatten(instance): + """Flatten Struct Of Array instance.""" + array_likes = get_array_fields(instance, return_values=True).values() + flat_array_likes = [] + inner_treedefs = [] + num_arrays = [] + for array_like in array_likes: + flat_array_like, inner_treedef = tree_flatten(array_like) + inner_treedefs.append(inner_treedef) + flat_array_likes += flat_array_like + num_arrays.append(len(flat_array_like)) + metadata = get_metadata_fields(instance, return_values=True) + metadata = type(instance).metadata_cls(**metadata) + return flat_array_likes, (inner_treedefs, metadata, num_arrays) + + +def make_metadata_class(cls): + metadata_fields = get_fields( + cls, lambda x: x.metadata.get('is_metadata', False) + ) + metadata_cls = dataclasses.make_dataclass( + cls_name='Meta' + cls.__name__, + fields=[(field.name, field.type, field) for field in metadata_fields], + frozen=True, + eq=True, + ) + return metadata_cls + + +def get_fields(cls_or_instance, filterfn, return_values=False): + fields = dataclasses.fields(cls_or_instance) + fields = [field for field in fields if filterfn(field)] + if return_values: + return { + field.name: getattr(cls_or_instance, field.name) for field in fields + } + else: + return fields + + +def get_array_fields(cls, return_values=False): + return get_fields( + cls, + lambda x: not x.metadata.get('is_metadata', False), + return_values=return_values, + ) + + +def get_metadata_fields(cls, return_values=False): + return get_fields( + cls, + lambda x: x.metadata.get('is_metadata', False), + return_values=return_values, + ) + + +def tree_flatten(pytree): + """Custom tree flattening function for MindSpore tensors.""" + if isinstance(pytree, ms.Tensor): + return [pytree], None + elif isinstance(pytree, dict): + keys, values = zip(*pytree.items()) + flat_values, treedefs = zip(*(tree_flatten(v) for v in values)) + return sum(flat_values, []), {'keys': keys, 'treedefs': treedefs} + elif isinstance(pytree, list): + flat_items, treedefs = zip(*(tree_flatten(item) for item in pytree)) + return sum(flat_items, []), {'treedefs': treedefs} + else: + return [], None + + +def tree_unflatten(treedef, leaves): + """Custom tree unflattening function for MindSpore tensors.""" + if treedef is None: + return leaves[0] + elif isinstance(treedef, dict): + if 'keys' in treedef: + keys = treedef['keys'] + treedefs = treedef['treedefs'] + items = [tree_unflatten(td, leaves[i:i+1]) + for i, td in enumerate(treedefs)] + return dict(zip(keys, items)) + else: + treedefs = treedef['treedefs'] + start = 0 + items = [] + for td in treedefs: + size = len(tree_flatten(tree_unflatten( + td, leaves[start:start+1]))[0]) + items.append(tree_unflatten(td, leaves[start:start+size])) + start += size + return items + else: + return [] + + +class StructOfArray: + """Class Decorator for Struct Of Arrays.""" + + def __init__(self, same_dtype=True): + self.same_dtype = same_dtype + + def __call__(self, cls): + cls.__array_ufunc__ = None + cls.replace = replace + cls.same_dtype = self.same_dtype + cls.dtype = get_dtype + cls.shape = get_shape + cls.__len__ = get_len + cls.__getitem__ = get_item + cls.__post_init__ = post_init + new_cls = dataclasses.dataclass(cls, frozen=True, eq=False) + # pytree claims to require metadata to be hashable, not sure why, + # But making derived dataclass that can just hold metadata + new_cls.metadata_cls = make_metadata_class(new_cls) + + def unflatten(cls, params): + aux, data = params + inner_treedefs, metadata, num_arrays = aux + array_fields = [field.name for field in get_array_fields(new_cls)] + value_dict = {} + array_start = 0 + for num_array, inner_treedef, array_field in zip( + num_arrays, inner_treedefs, array_fields, strict=True + ): + value_dict[array_field] = tree_unflatten( + inner_treedef, data[array_start: array_start + num_array] + ) + array_start += num_array + metadata_fields = get_metadata_fields(new_cls) + for field in metadata_fields: + value_dict[field.name] = getattr(metadata, field.name) + + return new_cls(**value_dict) + + # Override __flatten__ and __unflatten__ methods + new_cls.__flatten__ = flatten + new_cls.__unflatten__ = unflatten + + return new_cls diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/utils.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/utils.py new file mode 100644 index 000000000..b332b5dea --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/utils.py @@ -0,0 +1,149 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +"""Utils for geometry library.""" + +from collections.abc import Iterable +import numbers + +import mindspore as ms +import mindspore.ops as ops +import mindspore.numpy as mnp + + +def safe_select(condition, true_fn, false_fn): + """Safe version of selection (i.e. `where`). + + This applies the double-where trick. + Like jnp.where, this function will still execute both branches and is + expected to be more lightweight than lax.cond. Other than NaN-semantics, + safe_select(condition, true_fn, false_fn) is equivalent to + + utils.tree.map(lambda x, y: jnp.where(condition, x, y), + true_fn(), + false_fn()), + + Compared to the naive implementation above, safe_select provides the + following guarantee: in either the forward or backward pass, a NaN produced + *during the execution of true_fn()* will not propagate to the rest of the + computation and similarly for false_fn. It is very important to note that + while true_fn and false_fn will typically close over other tensors (i.e. they + use values computed prior to the safe_select function), there is no NaN-safety + for the backward pass of closed over values. It is important than any NaN's + are produced within the branch functions and not before them. For example, + + safe_select(x < eps, lambda: 0., lambda: jnp.sqrt(x)) + + will not produce NaN on the backward pass even if x == 0. since sqrt happens + within the false_fn, but the very similar + + y = jnp.sqrt(x) + safe_select(x < eps, lambda: 0., lambda: y) + + will produce a NaN on the backward pass if x == 0 because the sqrt happens + prior to the false_fn. + + Args: + condition: Boolean array to use in where + true_fn: Zero-argument function to construct the values used in the True + condition. Tensors that this function closes over will be extracted + automatically to implement the double-where trick to suppress spurious NaN + propagation. + false_fn: False branch equivalent of true_fn + + Returns: + Resulting PyTree equivalent to tree_map line above. + """ + true_result = true_fn() + false_result = false_fn() + + # Apply the double-where trick + true_part = ops.select(condition, true_result, + ops.stop_gradient(true_result)) + false_part = ops.select( + condition, ops.stop_gradient(false_result), false_result) + + return ops.select(condition, true_part, false_part) + + +def unstack(value: ms.Tensor, axis: int = -1) -> list[ms.Tensor]: + if len(value.shape) == 3: + if axis == -1: + split_tensors = [value[:, :, i] for i in range(value.shape[axis])] + elif axis == -2: + split_tensors = [value[:, i, :] for i in range(value.shape[axis])] + else: + split_tensors = [value[i, :, :] for i in range(value.shape[axis])] + elif len(value.shape) == 2: + if axis == -1: + split_tensors = [value[:, i] for i in range(value.shape[axis])] + else: + split_tensors = [value[i, :] for i in range(value.shape[axis])] + return split_tensors + + +def angdiff(alpha: ms.Tensor, beta: ms.Tensor) -> ms.Tensor: + """Compute absolute difference between two angles.""" + d = alpha - beta + d = (d + mnp.pi) % (2 * mnp.pi) - mnp.pi + return d + + +def safe_arctan2( + x1: ms.Tensor, x2: ms.Tensor, eps: float = 1e-8 +) -> ms.Tensor: + """Safe version of arctan2 that avoids NaN gradients when x1=x2=0.""" + + return safe_select( + ops.abs(x1) + ops.abs(x2) < eps, + lambda: ops.zeros_like(ops.atan2(x1, x2)), + lambda: ops.atan2(x1, x2), + ) + + +def weighted_mean( + *, + weights: ms.Tensor, + value: ms.Tensor, + axis: int | Iterable[int] | None=None, + eps: float = 1e-10, +) -> ms.Tensor: + """Computes weighted mean in a safe way that avoids NaNs. + + This is equivalent to jnp.average for the case eps=0.0, but adds a small + constant to the denominator of the weighted average to avoid NaNs. + 'weights' should be broadcastable to the shape of value. + + Args: + weights: Weights to weight value by. + value: Values to average + axis: Axes to average over. + eps: Epsilon to add to the denominator. + + Returns: + Weighted average. + """ + + weights = ops.cast(weights, value.dtype) + weights = ops.broadcast_to(weights, value.shape) + + weights_shape = weights.shape + + if isinstance(axis, numbers.Integral): + axis = [axis] + elif axis is None: + axis = list(range(len(weights_shape))) + + numerator = ops.reduce_sum(weights * value, axis=tuple(axis)) + denominator = ops.reduce_sum(weights, axis=tuple(axis)) + eps + + return numerator / denominator diff --git a/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py new file mode 100644 index 000000000..76ec2bd73 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py @@ -0,0 +1,255 @@ +# Copyright 2024 DeepMind Technologies Limited +# Copyright (C) 2025 Huawei Technologies Co., Ltd +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md +# +# Modifications by Huawei Technologies Co., Ltd: Adapt to run by MindSpore on Ascend + +"""Vec3Array Class.""" + +import dataclasses +from typing import Final, TypeVar, TypeAlias + +import mindspore as ms +from mindspore import ops, mint +from alphafold3.utils.geometry import struct_of_array + +Self = TypeVar('Self', bound='Vec3Array') +Float = TypeAlias = float | ms.Tensor +VERSION: Final[str] = '0.1' + + +def tree_map(func, *trees): + """ + Recursively applies a function to each leaf of the input trees. + + Args: + func: A function to apply to each leaf. + *trees: One or more tree structures (nested lists/tuples/dicts). + + Returns: + A new tree with the same structure where `func` has been applied to each leaf. + """ + if isinstance(trees[0], Vec3Array): + return Vec3Array( + x=tree_map(func, *(t.x for t in trees)), + y=tree_map(func, *(t.y for t in trees)), + z=tree_map(func, *(t.z for t in trees)) + ) + if isinstance(trees[0], dict): + return {key: tree_map(func, *(t[key] for t in trees)) for key in trees[0]} + if isinstance(trees[0], (list, tuple)): + return type(trees[0])(tree_map(func, *args) for args in zip(*trees)) + return func(*trees) + + +@struct_of_array.StructOfArray(same_dtype=True) +class Vec3Array: + """Vec3Array in 3 dimensional Space implemented as struct of arrays. + This is done in order to improve performance and precision. + """ + + x: ms.Tensor = dataclasses.field(metadata={'dtype': ms.float32}) + y: ms.Tensor + z: ms.Tensor + + def __post_init__(self): + if hasattr(self.x, 'dtype'): + if not self.x.dtype == self.y.dtype == self.z.dtype: + raise ValueError( + f'Type mismatch: {self.x.dtype}, {self.y.dtype}, {self.z.dtype}' + ) + if not self.x.shape == self.y.shape == self.z.shape: + raise ValueError( + f'Shape mismatch: {self.x.shape}, {self.y.shape}, {self.z.shape}' + ) + + @property + def shape(self): + """Return the shape of the Vec3Array.""" + return self.x.shape + + def __add__(self, other: Self) -> Self: + return tree_map(ops.add, self, other) + + def __sub__(self, other: Self) -> Self: + return tree_map(ops.sub, self, other) + + def __mul__(self, other: Float | ms.Tensor) -> Self: + if isinstance(other, float): + return tree_map(lambda x: ops.mul(x, other), self) + x = ops.mul(self.x, other) + y = ops.mul(self.y, other) + z = ops.mul(self.z, other) + return Vec3Array(x, y, z) + + def __rmul__(self, other: Float | ms.Tensor) -> Self: + if isinstance(other, float): + return self * other + x = ops.mul(self.x, other) + y = ops.mul(self.y, other) + z = ops.mul(self.z, other) + return Vec3Array(x, y, z) + + def __truediv__(self, other: Float) -> Self: + return tree_map(lambda x: ops.div(x, other), self) + + def __neg__(self) -> Self: + return tree_map(lambda x: -x, self) + + def __pos__(self) -> Self: + return tree_map(lambda x: x, self) + + def cross(self, other: Self) -> Self: + """Compute cross product between 'self' and 'other'.""" + new_x = ops.sub(ops.mul(self.y, other.z), ops.mul(self.z, other.y)) + new_y = ops.sub(ops.mul(self.z, other.x), ops.mul(self.x, other.z)) + new_z = ops.sub(ops.mul(self.x, other.y), ops.mul(self.y, other.x)) + return Vec3Array(new_x, new_y, new_z) + + def dot(self, other: Self) -> ms.Tensor: + """Compute dot product between 'self' and 'other'.""" + return ops.add(ops.add(ops.mul(self.x, other.x), ops.mul(self.y, other.y)), ops.mul(self.z, other.z)) + + def norm(self, epsilon: float = 1e-6) -> ms.Tensor: + """Compute Norm of Vec3Array, clipped to epsilon.""" + # To avoid NaN on the backward pass, we must use maximum before the sqrt + norm2 = self.dot(self) + if epsilon: + norm2 = ops.maximum(norm2, epsilon**2) + return ops.sqrt(norm2) + + def norm2(self) -> ms.Tensor: + return self.dot(self) + + def normalized(self, epsilon: float = 1e-6) -> Self: + """Return unit vector with optional clipping.""" + return self / self.norm(epsilon) + + @classmethod + def zeros(cls, shape, dtype=ms.float32): + """Return Vec3Array corresponding to zeros of given shape.""" + return cls( + mint.zeros(shape, dtype=dtype), + mint.zeros(shape, dtype=dtype), + mint.zeros(shape, dtype=dtype), + ) + + def to_array(self) -> ms.Tensor: + return ops.stack([self.x, self.y, self.z], axis=-1) + + @classmethod + def from_array(cls, array): + unstacked = ops.unstack(array, axis=-1) + return cls(unstacked[0], unstacked[1], unstacked[2]) + + def __getstate__(self): + return ( + VERSION, + [self.x.asnumpy(), self.y.asnumpy(), self.z.asnumpy()], + ) + + def __setstate__(self, state): + version, state = state + del version + for i, letter in enumerate('xyz'): + object.__setattr__(self, letter, ms.Tensor(state[i])) + + +def square_euclidean_distance( + vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6 +) -> Float: + """Computes square of euclidean distance between 'vec1' and 'vec2'. + + Args: + vec1: Vec3Array to compute distance to + vec2: Vec3Array to compute distance from, should be broadcast compatible + with 'vec1' + epsilon: distance is clipped from below to be at least epsilon + + Returns: + Array of square euclidean distances; + shape will be result of broadcasting 'vec1' and 'vec2' + """ + difference = vec1 - vec2 + distance = difference.dot(difference) + if epsilon: + distance = ops.maximum(distance, epsilon) + return distance + + +def dot(vector1: Vec3Array, vector2: Vec3Array) -> Float: + return vector1.dot(vector2) + + +def cross(vector1: Vec3Array, vector2: Vec3Array) -> Float: + return vector1.cross(vector2) + + +def norm(vector: Vec3Array, epsilon: float = 1e-6) -> Float: + return vector.norm(epsilon) + + +def normalized(vector: Vec3Array, epsilon: float = 1e-6) -> Vec3Array: + return vector.normalized(epsilon) + + +def euclidean_distance( + vec1: Vec3Array, vec2: Vec3Array, epsilon: float = 1e-6 +) -> Float: + """Computes euclidean distance between 'vec1' and 'vec2'. + + Args: + vec1: Vec3Array to compute euclidean distance to + vec2: Vec3Array to compute euclidean distance from, should be broadcast + compatible with 'vec1' + epsilon: distance is clipped from below to be at least epsilon + + Returns: + Array of euclidean distances; + shape will be result of broadcasting 'vec1' and 'vec2' + """ + distance_sq = square_euclidean_distance(vec1, vec2, epsilon**2) + distance = ops.sqrt(distance_sq) + return distance + + +def dihedral_angle( + a: Vec3Array, b: Vec3Array, c: Vec3Array, d: Vec3Array +) -> Float: + """Computes torsion angle for a quadruple of points. + + For points (a, b, c, d), this is the angle between the planes defined by + points (a, b, c) and (b, c, d). It is also known as the dihedral angle. + + Arguments: + a: A Vec3Array of coordinates. + b: A Vec3Array of coordinates. + c: A Vec3Array of coordinates. + d: A Vec3Array of coordinates. + + Returns: + A tensor of angles in radians: [-pi, pi]. + """ + v1 = a - b + v2 = b - c + v3 = d - c + + c1 = v1.cross(v2) + c2 = v3.cross(v2) + c3 = c2.cross(c1) + + v2_mag = v2.norm() + return ops.atan2(c3.dot(v2), v2_mag * c1.dot(c2)) + + +def random_gaussian_vector(shape, key=None, dtype=ms.float32) -> Vec3Array: + stdnormal = ops.StandardNormal(seed=key) + vec_array = stdnormal(shape + (3,)).astype(dtype) + return Vec3Array.from_array(vec_array) diff --git a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst index ca1f06da2..fb549db32 100644 --- a/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst +++ b/docs/api_python/mindchemistry/cell/mindchemistry.cell.orb.EnergyHead.rst @@ -14,7 +14,7 @@ mindchemistry.cell.orb.EnergyHead - **reference_energy_name** (str,可选) - 用于偏移的参考能量名称,例如 ``"vasp-shifted"``。默认值: ``"mp-traj-d3"``。 - **train_reference** (bool,可选) - 是否将参考能量训练为可学习参数。默认值: ``False``。 - **dropout** (Optional[float],可选) - MLP的dropout率。默认值: ``None``。 - - **node_aggregation** (str,可选) - 节点预测的聚合方法,例如 ``"mean"``或 ``"sum"``。默认值: ``None``。 + - **node_aggregation** (str,可选) - 节点预测的聚合方法,例如 ``"mean"`` 或 ``"sum"``。默认值: ``None``。 输入: - **node_features** (dict) - 节点特征字典,必须包含键"feat",形状为 :math:`(n_{nodes}, latent\_dim)`。 -- Gitee From 0998abf5b582b69792680aa5fca34483515f5878 Mon Sep 17 00:00:00 2001 From: goto Date: Mon, 4 Aug 2025 21:02:00 +0800 Subject: [PATCH 29/30] enhance fourier API and replace related APIs --- MindFlow/applications/cfd/acoustic/cbs/cbs.py | 6 +- MindFlow/applications/cfd/acoustic/cbs/dft.py | 124 ------ .../airfoil/2D_unsteady/src/fno2d.py | 15 +- .../mindflow/cell/neural_operators/ffno.py | 37 +- .../mindflow/cell/neural_operators/ffno_sp.py | 33 +- .../mindflow/cell/neural_operators/fno.py | 2 +- .../mindflow/cell/neural_operators/fno_sp.py | 243 +++++++++++ .../mindflow/cell/neural_operators/kno1d.py | 2 +- .../mindflow/cell/neural_operators/kno2d.py | 2 +- MindFlow/mindflow/core/fourier.py | 409 ++++++++---------- tests/st/mindflow/networks/fno/test_fno.py | 2 +- tests/st/mindflow/operators/test_fourier.py | 76 ++-- 12 files changed, 511 insertions(+), 440 deletions(-) delete mode 100644 MindFlow/applications/cfd/acoustic/cbs/dft.py create mode 100644 MindFlow/mindflow/cell/neural_operators/fno_sp.py diff --git a/MindFlow/applications/cfd/acoustic/cbs/cbs.py b/MindFlow/applications/cfd/acoustic/cbs/cbs.py index 0aba76a0a..7496a8997 100644 --- a/MindFlow/applications/cfd/acoustic/cbs/cbs.py +++ b/MindFlow/applications/cfd/acoustic/cbs/cbs.py @@ -19,7 +19,7 @@ import numpy as np import mindspore as ms from mindspore import Tensor, nn, ops, numpy as mnp, lazy_inline -from .dft import MyDFTn, MyiDFTn +from mindflow import DFTn, IDFTn class CBSBlock(nn.Cell): @@ -32,8 +32,8 @@ class CBSBlock(nn.Cell): shape: tuple of int, only the spatial shape, not including the batch and channel dimensions ''' super().__init__() - self.dft_cell = MyDFTn(shape) - self.idft_cell = MyiDFTn(shape) + self.dft_cell = DFTn(shape) + self.idft_cell = IDFTn(shape) # Scattering potential calculation for real and imaginary parts def op_v(self, ur, ui, vr, vi): diff --git a/MindFlow/applications/cfd/acoustic/cbs/dft.py b/MindFlow/applications/cfd/acoustic/cbs/dft.py deleted file mode 100644 index d93d9a8b1..000000000 --- a/MindFlow/applications/cfd/acoustic/cbs/dft.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2025 Huawei Technologies Co., Ltd -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== -''' provide complex dft based on the real dft API in mindflow.dft ''' -import numpy as np -import mindspore as ms -from mindspore import nn, ops, numpy as mnp, mint -from mindflow.cell.neural_operators.dft import dft1, dft2, dft3 - - -class MyDFTn(nn.Cell): - def __init__(self, shape): - super().__init__() - assert len(shape) in (1, 2, 3), 'only ndim 1, 2, 3 supported' - - n = shape[-1] - ndim = len(shape) - modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 - - self.shape = tuple(shape) - self.dft_cell = { - 1: dft1, - 2: dft2, - 3: dft3, - }[ndim](shape, modes) - - # use mask to assemble slices of Tensors, avoiding dynamic shape - # bug note: for unknown reasons, GRAPH_MODE cannot work with mask Tensors allocated using ops.ones() - mask_x0 = np.ones(n//2 + 1) - mask_xm = np.ones(n//2 + 1) - mask_y0 = np.ones(shape) - mask_z0 = np.ones(shape) - mask_x0[0] = 0 - mask_xm[-1] = 0 - if ndim > 1: - mask_y0[..., 0, :] = 0 - if ndim > 2: - mask_z0[..., 0, :, :] = 0 - - self.mask_x0 = ms.Tensor(mask_x0, dtype=ms.float32, const_arg=True) - self.mask_xm = ms.Tensor(mask_xm, dtype=ms.float32, const_arg=True) - self.mask_y0 = ms.Tensor(mask_y0, dtype=ms.float32, const_arg=True) - self.mask_z0 = ms.Tensor(mask_z0, dtype=ms.float32, const_arg=True) - - # bug note: ops.flip/mint.flip/mint.roll has bug for MS2.4.0 in PYNATIVE_MODE - # mnp.flip has bug after MS2.4.0 in GRAPH_MODE - # ops.roll only supports GPU, mnp.roll is ok but slow - msver = tuple([int(s) for s in ms.__version__.split('.')]) - kwargs1 = (dict(axis=-1), dict(axis=-2), dict(axis=-3)) - kwargs2 = (dict(dims=(-1,)), dict(dims=(-2,)), dict(dims=(-3,))) - - if msver <= (2, 4, 0) and ms.get_context('mode') == ms.PYNATIVE_MODE: - self.fliper = mnp.flip - self.roller = mnp.roll - self.flipkw = kwargs1 - self.rollkw = kwargs1 - else: - self.fliper = mint.flip - self.roller = mint.roll - self.flipkw = kwargs2 - self.rollkw = kwargs2 - - def construct(self, ar, ai): - shape = tuple(self.shape) - n = shape[-1] - ndim = len(shape) - scale = float(np.prod(shape) ** .5) - - assert ai is None or ar.shape == ai.shape - assert ar.shape[-ndim:] == shape - - brr, bri = self.dft_cell((ar, ar * 0)) - - # n-D Fourier transform with last axis being real-transformed, output dimension (..., m, n//2+1) - if ai is None: - return brr * scale, bri * scale - - # n-D complex Fourier transform, output dimension (..., m, n) - # call dft for real & imag parts separately and then assemble - bir, bii = self.dft_cell((ai, ai * 0)) - - br_half1 = ops.pad((brr - bii) * self.mask_xm, [0, n//2 - 1]) - bi_half1 = ops.pad((bri + bir) * self.mask_xm, [0, n//2 - 1]) - - br_half2 = self.roller(self.fliper( - ops.pad((brr + bii) * self.mask_x0, [n//2 - 1, 0]), **self.flipkw[0]), n//2, **self.rollkw[0]) - bi_half2 = self.roller(self.fliper( - ops.pad((bir - bri) * self.mask_x0, [n//2 - 1, 0]), **self.flipkw[0]), n//2, **self.rollkw[0]) - if ndim > 1: - br_half2 = br_half2 * (1 - self.mask_y0) + self.roller(self.fliper( - br_half2 * self.mask_y0, **self.flipkw[1]), 1, **self.rollkw[1]) - bi_half2 = bi_half2 * (1 - self.mask_y0) + self.roller(self.fliper( - bi_half2 * self.mask_y0, **self.flipkw[1]), 1, **self.rollkw[1]) - if ndim > 2: - br_half2 = br_half2 * (1 - self.mask_z0) + self.roller(self.fliper( - br_half2 * self.mask_z0, **self.flipkw[2]), 1, **self.rollkw[2]) - bi_half2 = bi_half2 * (1 - self.mask_z0) + self.roller(self.fliper( - bi_half2 * self.mask_z0, **self.flipkw[2]), 1, **self.rollkw[2]) - - br = br_half1 + br_half2 - bi = bi_half1 + bi_half2 - - return br * scale, bi * scale - -class MyiDFTn(MyDFTn): - def __init__(self, shape): - super().__init__(shape) - - def construct(self, ar, ai): - ndim = len(self.shape) - scale = float(np.prod(ar.shape[-ndim:])) - br, bi = super().construct(ar, -ai) - return br / scale, -bi / scale diff --git a/MindFlow/applications/data_driven/airfoil/2D_unsteady/src/fno2d.py b/MindFlow/applications/data_driven/airfoil/2D_unsteady/src/fno2d.py index 64d0e0551..8cd1d90be 100644 --- a/MindFlow/applications/data_driven/airfoil/2D_unsteady/src/fno2d.py +++ b/MindFlow/applications/data_driven/airfoil/2D_unsteady/src/fno2d.py @@ -23,7 +23,7 @@ from mindspore.common.initializer import Zero from mindflow.utils.check_func import check_param_type from mindflow.core.math import get_grid_2d -from mindflow.cell.neural_operators.dft import dft2, idft2 +from mindflow import RDFTn, IRDFTn class FNO2D(nn.Cell): @@ -177,10 +177,10 @@ class SpectralConv2dDft(nn.Cell): self.w_im1 = Parameter(w_im1, requires_grad=True) self.w_re2 = Parameter(w_re2, requires_grad=True) self.w_im2 = Parameter(w_im2, requires_grad=True) - self.dft2_cell = dft2(shape=(column_resolution, raw_resolution), - modes=(modes1, modes2), compute_dtype=self.compute_dtype) - self.idft2_cell = idft2(shape=(column_resolution, raw_resolution), - modes=(modes1, modes2), compute_dtype=self.compute_dtype) + self.dft2_cell = RDFTn(shape=(column_resolution, raw_resolution), norm='ortho', + modes=(modes1, modes2), compute_dtype=self.compute_dtype) + self.idft2_cell = IRDFTn(shape=(column_resolution, raw_resolution), norm='ortho', + modes=(modes1, modes2), compute_dtype=self.compute_dtype) self.mat = Tensor(shape=(1, out_channels, column_resolution - 2 * modes1, modes2), dtype=self.compute_dtype, init=Zero()) self.concat = ops.Concat(-2) @@ -195,8 +195,7 @@ class SpectralConv2dDft(nn.Cell): def construct(self, x: Tensor): """forward""" x_re = x - x_im = ops.zeros_like(x_re) - x_ft_re, x_ft_im = self.dft2_cell((x_re, x_im)) + x_ft_re, x_ft_im = self.dft2_cell(x_re) out_ft_re1 = \ self.mul2d(x_ft_re[:, :, :self.modes1, :self.modes2], self.w_re1) \ @@ -217,5 +216,5 @@ class SpectralConv2dDft(nn.Cell): out_re = self.concat((out_ft_re1, mat, out_ft_re2)) out_im = self.concat((out_ft_im1, mat, out_ft_im2)) - x, _ = self.idft2_cell((out_re, out_im)) + x = self.idft2_cell(out_re, out_im) return x diff --git a/MindFlow/mindflow/cell/neural_operators/ffno.py b/MindFlow/mindflow/cell/neural_operators/ffno.py index 8d8fe6694..be22763c3 100644 --- a/MindFlow/mindflow/cell/neural_operators/ffno.py +++ b/MindFlow/mindflow/cell/neural_operators/ffno.py @@ -339,7 +339,7 @@ class FFNO(nn.Cell): self.dft_compute_dtype = dft_compute_dtype self.ffno_compute_dtype = ffno_compute_dtype self._concat = ops.Concat(axis=-1) - self._positional_embedding, self._input_perm, self._output_perm = self._transpose(len(self.resolutions)) + self._positional_embedding = self._transpose(len(self.resolutions)) self._padding = self._pad(len(self.resolutions)) if self.lifting_channels: self._lifting = nn.SequentialCell([ @@ -401,57 +401,50 @@ class FFNO(nn.Cell): """construct""" batch_size = x.shape[0] grid = mint.repeat_interleave(self._positional_embedding.astype(x.dtype), repeats=batch_size, dim=0) + if self.data_format != "channels_last": - x = ops.transpose(x, input_perm=self._output_perm) + x = ops.movedim(x, 1, -1) + if self.positional_embedding: x = self._concat((x, grid)) x = self._lifting(x) - x = ops.transpose(x, input_perm=self._input_perm) if self.r_padding != 0: - x = ops.Pad(self._padding)(x) - - x = ops.transpose(x, input_perm=self._output_perm) + x = ops.movedim(x, -1, 1) + x = ops.pad(x, self._padding) + x = ops.movedim(x, 1, -1) b = Tensor(0, dtype=mstype.float32) for block in self._ffno_blocks: x, b = block(x) + if self.r_padding != 0: b = self._remove_padding(len(self.resolutions), b) + x = self._projection(b) + if self.data_format != "channels_last": - x = ops.transpose(x, input_perm=self._input_perm) + x = ops.movedim(x, -1, 1) + return x def _transpose(self, n_dim): """transpose tensor""" if n_dim == 1: positional_embedding = Tensor(get_grid_1d(resolution=self.resolutions)) - input_perm = (0, 2, 1) - output_perm = (0, 2, 1) elif n_dim == 2: positional_embedding = Tensor(get_grid_2d(resolution=self.resolutions)) - input_perm = (0, 3, 1, 2) - output_perm = (0, 2, 3, 1) elif n_dim == 3: positional_embedding = Tensor(get_grid_3d(resolution=self.resolutions)) - input_perm = (0, 4, 1, 2, 3) - output_perm = (0, 2, 3, 4, 1) else: raise ValueError(f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {n_dim}") - return positional_embedding, input_perm, output_perm + return positional_embedding def _pad(self, n_dim): """pad the domain if input is non-periodic""" - if n_dim == 1: - pad = ([0, 0], [0, 0], [0, self.r_padding]) - elif n_dim == 2: - pad = ([0, 0], [0, 0], [0, self.r_padding], [0, self.r_padding]) - elif n_dim == 3: - pad = ([0, 0], [0, 0], [0, self.r_padding], [0, self.r_padding], [0, self.r_padding]) - else: + if not n_dim in {1, 2, 3}: raise ValueError(f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {n_dim}") - return pad + return n_dim * [0, self.r_padding] def _remove_padding(self, n_dim, b_input): """remove pad domain""" diff --git a/MindFlow/mindflow/cell/neural_operators/ffno_sp.py b/MindFlow/mindflow/cell/neural_operators/ffno_sp.py index 8ad65613c..b1fa1382f 100644 --- a/MindFlow/mindflow/cell/neural_operators/ffno_sp.py +++ b/MindFlow/mindflow/cell/neural_operators/ffno_sp.py @@ -19,7 +19,7 @@ import mindspore.common.dtype as mstype from mindspore import nn, ops, Tensor, Parameter, ParameterTuple, mint from mindspore.common.initializer import XavierNormal, initializer from ...core.math import get_grid_1d, get_grid_2d, get_grid_3d -from .dft import dft1, idft1 +from ...core.fourier import RDFTn, IRDFTn class FeedForward(nn.Cell): @@ -106,8 +106,8 @@ class SpectralConv(nn.Cell): """" n- shape - 3D: S1 S2 S3 / 2D: M N / 1D: C mode - output length - n//2 +1 dim - 3D: -1 -2 -3 / 2D: -1 -2 / 1D: -1 """ - dft_cell = dft1(shape=(n,), modes=mode, dim=(n_dim,), compute_dtype=self.compute_dtype) - idft_cell = idft1(shape=(n,), modes=mode, dim=(n_dim,), compute_dtype=self.compute_dtype) + dft_cell = RDFTn(shape=n, dim=n_dim, norm='ortho', modes=mode, compute_dtype=self.compute_dtype) + idft_cell = IRDFTn(shape=n, dim=n_dim, norm='ortho', modes=mode, compute_dtype=self.compute_dtype) return dft_cell, idft_cell @@ -244,9 +244,8 @@ class SpectralConv1d(SpectralConv): x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size x_ft_re = x - x_ft_im = ops.zeros_like(x_ft_re) - x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + x_ftx_re, x_ftx_im = self._dft1_x_cell(x_ft_re) x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0]] x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0]] @@ -268,7 +267,7 @@ class SpectralConv1d(SpectralConv): out_ftx_re = ops.zeros_like(x_ftx_re) out_ftx_im = ops.zeros_like(x_ftx_im) - x, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + x = self._idft1_x_cell(out_ftx_re, out_ftx_im) x = ops.transpose(x, input_perm=self._input_perm) return x @@ -298,10 +297,9 @@ class SpectralConv2d(SpectralConv): x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size, grid_size x_ft_re = x - x_ft_im = ops.zeros_like(x_ft_re) # Dimesion Y - x_fty_re, x_fty_im = self._dft1_y_cell((x_ft_re, x_ft_im)) + x_fty_re, x_fty_im = self._dft1_y_cell(x_ft_re) x_fty_re_part = x_fty_re[:, :, :, :self.n_modes[1]] x_fty_im_part = x_fty_im[:, :, :, :self.n_modes[1]] @@ -323,10 +321,10 @@ class SpectralConv2d(SpectralConv): out_fty_re = ops.zeros_like(x_fty_re) out_fty_im = ops.zeros_like(x_fty_im) - xy, _ = self._idft1_y_cell((out_fty_re, out_fty_im)) + xy = self._idft1_y_cell(out_fty_re, out_fty_im) # Dimesion X - x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + x_ftx_re, x_ftx_im = self._dft1_x_cell(x_ft_re) x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0], :] x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0], :] @@ -348,7 +346,7 @@ class SpectralConv2d(SpectralConv): out_ftx_re = ops.zeros_like(x_ftx_re) out_ftx_im = ops.zeros_like(x_ftx_im) - xx, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + xx = self._idft1_x_cell(out_ftx_re, out_ftx_im) # Combining Dimensions x = xx + xy @@ -383,10 +381,9 @@ class SpectralConv3d(SpectralConv): x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size, grid_size, grid_size x_ft_re = x - x_ft_im = ops.zeros_like(x_ft_re) # Dimesion Z - x_ftz_re, x_ftz_im = self._dft1_z_cell((x_ft_re, x_ft_im)) + x_ftz_re, x_ftz_im = self._dft1_z_cell(x_ft_re) x_ftz_re_part = x_ftz_re[:, :, :, :, :self.n_modes[2]] x_ftz_im_part = x_ftz_im[:, :, :, :, :self.n_modes[2]] @@ -408,10 +405,10 @@ class SpectralConv3d(SpectralConv): out_ftz_re = ops.zeros_like(x_ftz_re) out_ftz_im = ops.zeros_like(x_ftz_im) - xz, _ = self._idft1_z_cell((out_ftz_re, out_ftz_im)) + xz = self._idft1_z_cell(out_ftz_re, out_ftz_im) # Dimesion Y - x_fty_re, x_fty_im = self._dft1_y_cell((x_ft_re, x_ft_im)) + x_fty_re, x_fty_im = self._dft1_y_cell(x_ft_re) x_fty_re_part = x_fty_re[:, :, :, :self.n_modes[1], :] x_fty_im_part = x_fty_im[:, :, :, :self.n_modes[1], :] @@ -433,10 +430,10 @@ class SpectralConv3d(SpectralConv): out_fty_re = ops.zeros_like(x_fty_re) out_fty_im = ops.zeros_like(x_fty_im) - xy, _ = self._idft1_y_cell((out_fty_re, out_fty_im)) + xy = self._idft1_y_cell(out_fty_re, out_fty_im) # Dimesion X - x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + x_ftx_re, x_ftx_im = self._dft1_x_cell(x_ft_re) x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0], :, :] x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0], :, :] @@ -458,7 +455,7 @@ class SpectralConv3d(SpectralConv): out_ftx_re = ops.zeros_like(x_ftx_re) out_ftx_im = ops.zeros_like(x_ftx_im) - xx, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + xx = self._idft1_x_cell(out_ftx_re, out_ftx_im) # Combining Dimensions x = xx + xy + xz diff --git a/MindFlow/mindflow/cell/neural_operators/fno.py b/MindFlow/mindflow/cell/neural_operators/fno.py index 8eda5b682..4c4c644ac 100644 --- a/MindFlow/mindflow/cell/neural_operators/fno.py +++ b/MindFlow/mindflow/cell/neural_operators/fno.py @@ -19,7 +19,7 @@ from mindspore import nn, ops, Tensor, mint import mindspore.common.dtype as mstype -from .dft import SpectralConv1dDft, SpectralConv2dDft, SpectralConv3dDft +from .fno_sp import SpectralConv1dDft, SpectralConv2dDft, SpectralConv3dDft from ..activation import get_activation from ...core.math import get_grid_1d, get_grid_2d, get_grid_3d from ...utils.check_func import check_param_type diff --git a/MindFlow/mindflow/cell/neural_operators/fno_sp.py b/MindFlow/mindflow/cell/neural_operators/fno_sp.py new file mode 100644 index 000000000..bb0233350 --- /dev/null +++ b/MindFlow/mindflow/cell/neural_operators/fno_sp.py @@ -0,0 +1,243 @@ +'''' +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +''' +import numpy as np + +import mindspore.common.dtype as mstype +from mindspore import nn, ops, Tensor, Parameter, mint +from mindspore.common.initializer import Zero +from mindspore.ops import operations as P + +from ...core.fourier import RDFTn, IRDFTn + + +class SpectralConvDft(nn.Cell): + """Base Class for Fourier Layer, including DFT, linear transform, and Inverse DFT""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, compute_dtype=mstype.float32): + super().__init__() + self.in_channels = in_channels + self.out_channels = out_channels + if isinstance(n_modes, int): + n_modes = [n_modes] + self.n_modes = n_modes + if isinstance(resolutions, int): + resolutions = [resolutions] + self.resolutions = resolutions + if len(self.n_modes) != len(self.resolutions): + raise ValueError( + "The dimension of n_modes should be equal to that of resolutions, \ + but got dimension of n_modes {} and dimension of resolutions {}".format(len(self.n_modes), + len(self.resolutions))) + self.compute_dtype = compute_dtype + + def construct(self, x: Tensor): + raise NotImplementedError() + + def _einsum(self, inputs, weights): + weights = weights.expand_dims(0) + inputs = inputs.expand_dims(2) + out = inputs * weights + return out.sum(1) + + +class SpectralConv1dDft(SpectralConvDft): + """1D Fourier Layer. It does DFT, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions) + self._scale = (1. / (self.in_channels * self.out_channels)) + w_re = Tensor(self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0]), + dtype=mstype.float32) + w_im = Tensor(self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0]), + dtype=mstype.float32) + self._w_re = Parameter(w_re, requires_grad=True) + self._w_im = Parameter(w_im, requires_grad=True) + self._dft1_cell = RDFTn( + shape=(self.resolutions[0],), norm='ortho', modes=self.n_modes[0], compute_dtype=self.compute_dtype) + self._idft1_cell = IRDFTn( + shape=(self.resolutions[0],), norm='ortho', modes=self.n_modes[0], compute_dtype=self.compute_dtype) + + def construct(self, x: Tensor): + x_re = x + x_ft_re, x_ft_im = self._dft1_cell(x_re) + w_re = P.Cast()(self._w_re, self.compute_dtype) + w_im = P.Cast()(self._w_im, self.compute_dtype) + out_ft_re = self._einsum(x_ft_re[:, :, :self.n_modes[0]], w_re) - self._einsum(x_ft_im[:, :, :self.n_modes[0]], + w_im) + out_ft_im = self._einsum(x_ft_re[:, :, :self.n_modes[0]], w_im) + self._einsum(x_ft_im[:, :, :self.n_modes[0]], + w_re) + + x = self._idft1_cell(out_ft_re, out_ft_im) + + return x + + +class SpectralConv2dDft(SpectralConvDft): + """2D Fourier Layer. It does DFT, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions) + self._scale = (1. / (self.in_channels * self.out_channels)) + w_re1 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1]), + dtype=self.compute_dtype) + w_im1 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1]), + dtype=self.compute_dtype) + w_re2 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1]), + dtype=self.compute_dtype) + w_im2 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1]), + dtype=self.compute_dtype) + + self._w_re1 = Parameter(w_re1, requires_grad=True) + self._w_im1 = Parameter(w_im1, requires_grad=True) + self._w_re2 = Parameter(w_re2, requires_grad=True) + self._w_im2 = Parameter(w_im2, requires_grad=True) + + self._dft2_cell = RDFTn(shape=(self.resolutions[0], self.resolutions[1]), norm='ortho', + modes=(self.n_modes[0], self.n_modes[1]), compute_dtype=self.compute_dtype) + self._idft2_cell = IRDFTn(shape=(self.resolutions[0], self.resolutions[1]), norm='ortho', + modes=(self.n_modes[0], self.n_modes[1]), compute_dtype=self.compute_dtype) + self._mat = Tensor(shape=(1, self.out_channels, self.resolutions[1] - 2 * self.n_modes[0], self.n_modes[1]), + dtype=self.compute_dtype, init=Zero()) + self._concat = ops.Concat(-2) + + def construct(self, x: Tensor): + x_re = x + x_ft_re, x_ft_im = self._dft2_cell(x_re) + + out_ft_re1 = self._einsum(x_ft_re[:, :, :self.n_modes[0], :self.n_modes[1]], self._w_re1) - self._einsum( + x_ft_im[:, :, :self.n_modes[0], :self.n_modes[1]], self._w_im1) + out_ft_im1 = self._einsum(x_ft_re[:, :, :self.n_modes[0], :self.n_modes[1]], self._w_im1) + self._einsum( + x_ft_im[:, :, :self.n_modes[0], :self.n_modes[1]], self._w_re1) + + out_ft_re2 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, :self.n_modes[1]], self._w_re2) - self._einsum( + x_ft_im[:, :, -self.n_modes[0]:, :self.n_modes[1]], self._w_im2) + out_ft_im2 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, :self.n_modes[1]], self._w_im2) + self._einsum( + x_ft_im[:, :, -self.n_modes[0]:, :self.n_modes[1]], self._w_re2) + + batch_size = x.shape[0] + mat = mint.repeat_interleave(self._mat, batch_size, 0) + out_re = self._concat((out_ft_re1, mat, out_ft_re2)) + out_im = self._concat((out_ft_im1, mat, out_ft_im2)) + + x = self._idft2_cell(out_re, out_im) + + return x + + +class SpectralConv3dDft(SpectralConvDft): + """3D Fourier layer. It does DFT, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions) + self._scale = (1 / (self.in_channels * self.out_channels)) + + w_re1 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_im1 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_re2 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_im2 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_re3 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_im3 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_re4 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + w_im4 = Tensor( + self._scale * np.random.rand(self.in_channels, self.out_channels, self.n_modes[0], self.n_modes[1], + self.n_modes[2]), dtype=self.compute_dtype) + + self._w_re1 = Parameter(w_re1, requires_grad=True) + self._w_im1 = Parameter(w_im1, requires_grad=True) + self._w_re2 = Parameter(w_re2, requires_grad=True) + self._w_im2 = Parameter(w_im2, requires_grad=True) + self._w_re3 = Parameter(w_re3, requires_grad=True) + self._w_im3 = Parameter(w_im3, requires_grad=True) + self._w_re4 = Parameter(w_re4, requires_grad=True) + self._w_im4 = Parameter(w_im4, requires_grad=True) + + self._dft3_cell = RDFTn(shape=(self.resolutions[0], self.resolutions[1], self.resolutions[2]), norm='ortho', + modes=(self.n_modes[0], self.n_modes[1], self.n_modes[2]), + compute_dtype=self.compute_dtype) + self._idft3_cell = IRDFTn(shape=(self.resolutions[0], self.resolutions[1], self.resolutions[2]), norm='ortho', + modes=(self.n_modes[0], self.n_modes[1], self.n_modes[2]), + compute_dtype=self.compute_dtype) + self._mat_x = Tensor( + shape=(1, self.out_channels, self.resolutions[0] - 2 * self.n_modes[0], self.n_modes[1], self.n_modes[2]), + dtype=self.compute_dtype, init=Zero()) + self._mat_y = Tensor( + shape=(1, self.out_channels, self.resolutions[0], self.resolutions[1] - 2 * self.n_modes[1], + self.n_modes[2]), + dtype=self.compute_dtype, init=Zero()) + self._concat = ops.Concat(-2) + + def construct(self, x: Tensor): + x_re = x + x_ft_re, x_ft_im = self._dft3_cell(x_re) + + out_ft_re1 = self._einsum(x_ft_re[:, :, :self.n_modes[0], :self.n_modes[1], :self.n_modes[2]], + self._w_re1) - self._einsum(x_ft_im[:, :, :self.n_modes[0], :self.n_modes[1], + :self.n_modes[2]], self._w_im1) + out_ft_im1 = self._einsum(x_ft_re[:, :, :self.n_modes[0], :self.n_modes[1], :self.n_modes[2]], + self._w_im1) + self._einsum(x_ft_im[:, :, :self.n_modes[0], :self.n_modes[1], + :self.n_modes[2]], self._w_re1) + out_ft_re2 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, :self.n_modes[1], :self.n_modes[2]], + self._w_re2) - self._einsum(x_ft_im[:, :, -self.n_modes[0]:, :self.n_modes[1], + :self.n_modes[2]], self._w_im2) + out_ft_im2 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, :self.n_modes[1], :self.n_modes[2]], + self._w_im2) + self._einsum(x_ft_im[:, :, -self.n_modes[0]:, :self.n_modes[1], + :self.n_modes[2]], self._w_re2) + out_ft_re3 = self._einsum(x_ft_re[:, :, :self.n_modes[0], -self.n_modes[1]:, :self.n_modes[2]], + self._w_re3) - self._einsum(x_ft_im[:, :, :self.n_modes[0], -self.n_modes[1]:, + :self.n_modes[2]], self._w_im3) + out_ft_im3 = self._einsum(x_ft_re[:, :, :self.n_modes[0], -self.n_modes[1]:, :self.n_modes[2]], + self._w_im3) + self._einsum(x_ft_im[:, :, :self.n_modes[0], -self.n_modes[1]:, + :self.n_modes[2]], self._w_re3) + out_ft_re4 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, -self.n_modes[1]:, :self.n_modes[2]], + self._w_re4) - self._einsum(x_ft_im[:, :, -self.n_modes[0]:, -self.n_modes[1]:, + :self.n_modes[2]], self._w_im4) + out_ft_im4 = self._einsum(x_ft_re[:, :, -self.n_modes[0]:, -self.n_modes[1]:, :self.n_modes[2]], + self._w_im4) + self._einsum(x_ft_im[:, :, -self.n_modes[0]:, -self.n_modes[1]:, + :self.n_modes[2]], self._w_re4) + + batch_size = x.shape[0] + mat_x = mint.repeat_interleave(self._mat_x, batch_size, 0) + mat_y = mint.repeat_interleave(self._mat_y, batch_size, 0) + + out_re1 = ops.concat((out_ft_re1, mat_x, out_ft_re2), -3) + out_im1 = ops.concat((out_ft_im1, mat_x, out_ft_im2), -3) + + out_re2 = ops.concat((out_ft_re3, mat_x, out_ft_re4), -3) + out_im2 = ops.concat((out_ft_im3, mat_x, out_ft_im4), -3) + out_re = ops.concat((out_re1, mat_y, out_re2), -2) + out_im = ops.concat((out_im1, mat_y, out_im2), -2) + x = self._idft3_cell(out_re, out_im) + + return x diff --git a/MindFlow/mindflow/cell/neural_operators/kno1d.py b/MindFlow/mindflow/cell/neural_operators/kno1d.py index b4c11bb5d..81820de72 100644 --- a/MindFlow/mindflow/cell/neural_operators/kno1d.py +++ b/MindFlow/mindflow/cell/neural_operators/kno1d.py @@ -16,7 +16,7 @@ import mindspore.common.dtype as mstype from mindspore import ops, nn, Tensor -from .dft import SpectralConv1dDft +from .fno_sp import SpectralConv1dDft from ...utils.check_func import check_param_type diff --git a/MindFlow/mindflow/cell/neural_operators/kno2d.py b/MindFlow/mindflow/cell/neural_operators/kno2d.py index 07036e972..79f9ae98a 100644 --- a/MindFlow/mindflow/cell/neural_operators/kno2d.py +++ b/MindFlow/mindflow/cell/neural_operators/kno2d.py @@ -16,7 +16,7 @@ import mindspore.common.dtype as mstype from mindspore import ops, nn, Tensor -from .dft import SpectralConv2dDft +from .fno_sp import SpectralConv2dDft from ...utils.check_func import check_param_type diff --git a/MindFlow/mindflow/core/fourier.py b/MindFlow/mindflow/core/fourier.py index 17c5c6467..64f980668 100644 --- a/MindFlow/mindflow/core/fourier.py +++ b/MindFlow/mindflow/core/fourier.py @@ -14,14 +14,14 @@ # ============================================================================== ''' provide complex dft based on the real dft API in mindflow.dft ''' import numpy as np -from scipy.linalg import dft +import scipy import mindspore as ms import mindspore.common.dtype as mstype from mindspore import nn, ops, Tensor, mint from mindspore.common.initializer import Zero from mindspore.ops import operations as P -from ..utils.check_func import check_param_no_greater, check_param_value, check_param_type, check_param_even +from ..utils.check_func import check_param_no_greater, check_param_value class MyRoll(nn.Cell): @@ -76,88 +76,97 @@ class MyFlip(nn.Cell): return x +def convert_shape(shape): + ''' convert shape to suitable format ''' + if isinstance(shape, int): + n = shape + elif len(shape) == 1: + n, = shape + else: + raise TypeError("Only support 1D dct/dst, but got shape {}".format(shape)) + return n + + def convert_params(shape, modes, dim): ''' convert input arguments to suitable format ''' + shape = tuple(np.atleast_1d(shape).astype(int).tolist()) + ndim = len(shape) + if dim is None: - ndim = len(shape) dim = tuple([n - ndim for n in range(ndim)]) else: dim = tuple(np.atleast_1d(dim).astype(int).tolist()) - shape = tuple(np.atleast_1d(shape).astype(int).tolist()) - modes = tuple(np.atleast_1d(modes).astype(int).tolist()) + if modes is None or isinstance(modes, int): + modes = tuple([modes] * ndim) + else: + modes = tuple(np.atleast_1d(modes).astype(int).tolist()) return shape, modes, dim def check_params(shape, modes, dim): ''' check lawfulness of input arguments ''' - check_param_type(dim, "dim", data_type=tuple) - check_param_type(shape, "shape", data_type=tuple) - check_param_type(modes, "modes", data_type=tuple) check_param_no_greater(len(dim), "dim length", 3) check_param_value(len(shape), "shape length", len(dim)) check_param_value(len(modes), "modes length", len(dim)) - check_param_even(shape, "shape") - for i, (m, n) in enumerate(zip(modes, shape)): - check_param_no_greater(m, f'mode{i+1}', n // 2 + (i == len(dim) - 1)) + if np.any(modes): + for i, (m, n) in enumerate(zip(modes, shape)): + # if for last axis mode need to be n//2+1, mode should be set to None + check_param_no_greater(m, f'mode{i+1}', n // 2) class _DFT1d(nn.Cell): '''One dimensional Discrete Fourier Transformation''' - def __init__(self, n, modes, last_index, idx=0, inv=False, compute_dtype=mstype.float32): + def __init__(self, n, mode, last_index, idx=0, scale='sqrtn', inv=False, compute_dtype=mstype.float32): super().__init__() self.n = n - self.dft_mat = dft(n, scale="sqrtn") - self.modes = modes + self.dft_mat = scipy.linalg.dft(n, scale=scale) self.last_index = last_index self.inv = inv + self.odd = bool(n % 2) self.idx = idx + self.mode_upper = mode if mode else n // 2 + (self.last_index or self.odd) + self.mode_lower = mode if mode else n - self.mode_upper self.compute_dtype = compute_dtype - self.dft_mode_mat_upper = self.dft_mat[:, :modes] - self.a_re_upper = Tensor( - self.dft_mode_mat_upper.real, dtype=compute_dtype) - self.a_im_upper = Tensor( - self.dft_mode_mat_upper.imag, dtype=compute_dtype) - - self.dft_mode_mat_lower = self.dft_mat[:, -modes:] - self.a_re_lower = Tensor( - self.dft_mode_mat_lower.real, dtype=compute_dtype) - self.a_im_lower = Tensor( - self.dft_mode_mat_lower.imag, dtype=compute_dtype) + # generate DFT matrix for positive and negative frequencies + dft_mat_mode = self.dft_mat[:, :self.mode_upper] + self.a_re_upper = Tensor(dft_mat_mode.real, dtype=compute_dtype) + self.a_im_upper = Tensor(dft_mat_mode.imag, dtype=compute_dtype) + + dft_mat_mode = self.dft_mat[:, -self.mode_lower:] + self.a_re_lower = Tensor(dft_mat_mode.real, dtype=compute_dtype) + self.a_im_lower = Tensor(dft_mat_mode.imag, dtype=compute_dtype) + + # the zero matrix to fill the un-transformed modes + m = self.n - (self.mode_upper + self.mode_lower) + if m > 0: + self.mat = Tensor(shape=m, dtype=compute_dtype, init=Zero()) + self.concat = ops.Concat(axis=-1) + self.cast = P.Cast() if self.inv: self.a_re_upper = self.a_re_upper.T self.a_im_upper = -self.a_im_upper.T + self.a_re_lower = self.a_re_lower.T + self.a_im_lower = -self.a_im_lower.T + + # last axis is real-transformed, so the inverse is conjugate of the positive frequencies if last_index: - if modes == n // 2 + 1: - self.dft_mat_res = self.dft_mat[:, -modes + 2:] - else: - self.dft_mat_res = self.dft_mat[:, -modes + 1:] - - mat = Tensor(np.zeros(n,), dtype=compute_dtype).reshape(n, 1) - self.a_re_res = MyFlip()(Tensor(self.dft_mat_res.real, dtype=compute_dtype), dims=-1) - self.a_im_res = MyFlip()(Tensor(self.dft_mat_res.imag, dtype=compute_dtype), dims=-1) - if modes == n // 2 + 1: - self.a_re_res = self.concat((mat, self.a_re_res, mat)) - self.a_im_res = self.concat((mat, self.a_im_res, mat)) - else: - self.a_re_res = self.concat((mat, self.a_re_res)) - self.a_im_res = self.concat((mat, self.a_im_res)) - - self.a_re_res = self.a_re_res.T - self.a_im_res = -self.a_im_res.T - else: - self.a_re_res = self.a_re_lower.T - self.a_im_res = -self.a_im_lower.T - - if (self.n - 2 * self.modes) > 0: - self.mat = Tensor(shape=(self.n - 2 * self.modes), - dtype=compute_dtype, init=Zero()) + mode_res = min(self.mode_lower, self.mode_upper - 1) + dft_mat_res = self.dft_mat[:, -mode_res:] + a_re_res = MyFlip()(Tensor(dft_mat_res.real, dtype=compute_dtype), dims=-1) + a_im_res = MyFlip()(Tensor(dft_mat_res.imag, dtype=compute_dtype), dims=-1) + + a_re_res = ops.pad(a_re_res, (1, self.mode_upper - mode_res - 1)) + a_im_res = ops.pad(a_im_res, (1, self.mode_upper - mode_res - 1)) + + self.a_re_upper += a_re_res.T + self.a_im_upper += a_im_res.T def swap_axes(self, x_re, x_im): return x_re.swapaxes(-1, self.idx), x_im.swapaxes(-1, self.idx) @@ -168,10 +177,9 @@ class _DFT1d(nn.Cell): return y_re, y_im def zero_mat(self, dims): - length = len(dims) mat = self.mat - for i in range(length - 1, -1, -1): - mat = mint.repeat_interleave(mat.expand_dims(0), dims[i], 0) + for n in dims[::-1]: + mat = mint.repeat_interleave(mat.expand_dims(0), n, 0) return mat def compute_forward(self, x_re, x_im): @@ -185,7 +193,7 @@ class _DFT1d(nn.Cell): y_re2, y_im2 = self.complex_matmul( x_re=x_re, x_im=x_im, a_re=self.a_re_lower, a_im=self.a_im_lower) - if self.n == self.modes * 2: + if self.n == self.mode_upper + self.mode_lower: y_re = self.concat((y_re, y_re2)) y_im = self.concat((y_im, y_im2)) else: @@ -197,26 +205,23 @@ class _DFT1d(nn.Cell): def compute_inverse(self, x_re, x_im): ''' Inverse transform for irdft ''' - y_re, y_im = self.complex_matmul(x_re=x_re[..., :self.modes], - x_im=x_im[..., :self.modes], + y_re, y_im = self.complex_matmul(x_re=x_re[..., :self.mode_upper], + x_im=x_im[..., :self.mode_upper], a_re=self.a_re_upper, a_im=self.a_im_upper) if self.last_index: - y_re_res, y_im_res = self.complex_matmul(x_re=x_re, - x_im=x_im, - a_re=self.a_re_res, - a_im=-self.a_im_res) - else: - y_re_res, y_im_res = self.complex_matmul(x_re=x_re[..., -self.modes:], - x_im=x_im[..., -self.modes:], - a_re=self.a_re_res, - a_im=self.a_im_res) + return y_re, y_im + + y_re_res, y_im_res = self.complex_matmul(x_re=x_re[..., -self.mode_lower:], + x_im=x_im[..., -self.mode_lower:], + a_re=self.a_re_lower, + a_im=self.a_im_lower) return y_re + y_re_res, y_im + y_im_res def construct(self, x): ''' perform 1d rdft/irdft with matmul operations ''' x_re, x_im = x - x_re, x_im = P.Cast()(x_re, self.compute_dtype), P.Cast()(x_im, self.compute_dtype) + x_re, x_im = self.cast(x_re, self.compute_dtype), self.cast(x_im, self.compute_dtype) x_re, x_im = self.swap_axes(x_re, x_im) if self.inv: y_re, y_im = self.compute_inverse(x_re, x_im) @@ -227,52 +232,51 @@ class _DFT1d(nn.Cell): class _DFTn(nn.Cell): - r""" - N dimensional Discrete Fourier Transformation - - Args: - shape (tuple): Dimension of the input 'x'. - modes (tuple): The length of the output transform axis. The `modes` must be no greater than half of the - dimension of input 'x'. - dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. - inv (bool): Whether to compute inverse transformation. Default: False. - compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. - - Inputs: - - **x** (Tensor, Tensor): The input data. It's 3-D tuple of Tensor. It's a complex, - including x real and imaginary. Tensor of shape :math:`(*, *)`. - - Returns: - Complex tensor with the same shape of input x. - - Raises: - TypeError: If `shape` is not a tuple. - ValueError: If the length of `shape` is greater than 3. - """ - def __init__(self, shape, modes, dim=None, inv=False, compute_dtype=mstype.float32): + ''' Base class for n-D DFT transform ''' + def __init__(self, shape, dim=None, norm='backward', modes=None, compute_dtype=mstype.float32): super().__init__() - if dim is None: - dim = range(len(shape)) - self.dft1_seq = nn.SequentialCell() - last_index = [False for _ in range(len(shape))] - last_index[-1] = True - for dim_id, idx in enumerate(dim): - self.dft1_seq.append( - _DFT1d(n=shape[dim_id], modes=modes[dim_id], last_index=last_index[dim_id], idx=idx, inv=inv, - compute_dtype=compute_dtype)) - - def construct(self, x): - return self.dft1_seq(x) - + shape, modes, dim = convert_params(shape, modes, dim) + check_params(shape, modes, dim) -class RDFTn(nn.Cell): + ndim = len(shape) + inv, scale, r2c_flags = self.set_options(ndim, norm) + self.dft1_seq = nn.SequentialCell() + for n, m, r, d in zip(shape, modes, r2c_flags, dim): + self.dft1_seq.append(_DFT1d( + n=n, mode=m, last_index=r, idx=d, scale=scale, inv=inv, compute_dtype=compute_dtype)) + + def set_options(self, ndim, norm): + ''' + Choose the dimensions, normalization, and transformation mode (forward/backward). + Derivative APIs overwrite the options to achieve their specific goals. + ''' + inv = False + scale = { + 'backward': None, + 'forward': 'n', + 'ortho': 'sqrtn', + }[norm] + r2c_flags = np.zeros(ndim, dtype=bool).tolist() + r2c_flags[-1] = True + return inv, scale, r2c_flags + + def construct(self, *args, **kwargs): + raise NotImplementedError + + +class RDFTn(_DFTn): r""" 1/2/3D discrete real Fourier transformation on real number. The results should be same as `scipy.fft.rfftn() `_ . Args: shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. + norm (str): Normalization mode, should be one of 'forward', 'backward', 'ortho'. Default: 'backward', + same as torch.fft.rfftn + modes (tuple, int, None): The length of the output transform axis. The `modes` must be no greater than half of the + dimension of input 'x'. compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. Inputs: @@ -296,37 +300,25 @@ class RDFTn(nn.Cell): >>> print(br.shape) (2, 32, 257) """ - def __init__(self, shape, compute_dtype=mstype.float32): - super().__init__() - - n = shape[-1] - ndim = len(shape) - modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 - - shape, modes, dim = convert_params(shape, modes, dim=None) - check_params(shape, modes, dim) - - self.n = n - self.ndim = ndim - self.shape = shape - self.scale = float(np.prod(shape) ** .5) - - self.dft_cell = _DFTn(shape, modes, dim, inv=False, compute_dtype=compute_dtype) - def construct(self, ar): ''' perform n-dimensional rDFT on real tensor ''' # n-D Fourier transform with last axis being real-transformed, output dimension (..., m, n//2+1) - br, bi = self.dft_cell((ar, ar * 0)) # the last ndim dimensions of ar must accord with shape - return br * self.scale, bi * self.scale + # the last ndim dimensions of ar must accord with shape + return self.dft1_seq((ar, ar * 0)) -class IRDFTn(nn.Cell): +class IRDFTn(_DFTn): r""" 1/2/3D discrete inverse real Fourier transformation on complex number. The results should be same as `scipy.fft.irfftn() `_ . Args: shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. + norm (str): Normalization mode, should be one of 'forward', 'backward', 'ortho'. Default: 'backward', + same as torch.fft.irfftn + modes (tuple, int, None): The length of the output transform axis. The `modes` must be no greater than half of the + dimension of input 'x'. compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. Inputs: @@ -351,36 +343,34 @@ class IRDFTn(nn.Cell): >>> print(br.shape) (2, 32, 512) """ - def __init__(self, shape, compute_dtype=mstype.float32): - super().__init__() - - n = shape[-1] - ndim = len(shape) - modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 - - shape, modes, dim = convert_params(shape, modes, dim=None) - check_params(shape, modes, dim) - - self.n = n - self.ndim = ndim - self.shape = shape - self.scale = float(np.prod(self.shape) ** .5) - - self.idft_cell = _DFTn(shape, modes, dim, inv=True, compute_dtype=compute_dtype) + def set_options(self, ndim, norm): + inv = True + scale = { + 'forward': None, + 'backward': 'n', + 'ortho': 'sqrtn', + }[norm] + r2c_flags = np.zeros(ndim, dtype=bool).tolist() + r2c_flags[-1] = True + return inv, scale, r2c_flags def construct(self, ar, ai): ''' perform n-dimensional irDFT on complex tensor and output real tensor ''' - br, _ = self.idft_cell((ar, ai)) - return br / self.scale + return self.dft1_seq((ar, ai))[0] -class DFTn(nn.Cell): +class DFTn(_DFTn): r""" 1/2/3D discrete Fourier transformation on complex number. The results should be same as `scipy.fft.fftn() `_ . Args: shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. + norm (str): Normalization mode, should be one of 'forward', 'backward', 'ortho'. Default: 'backward', + same as torch.fft.irfftn + modes (tuple, int, None): The length of the output transform axis. The `modes` must be no greater than half of the + dimension of input 'x'. compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. Inputs: @@ -404,89 +394,34 @@ class DFTn(nn.Cell): >>> print(br.shape) (2, 32, 512) """ - def __init__(self, shape, compute_dtype=mstype.float32): - super().__init__() - - n = shape[-1] - ndim = len(shape) - modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 - - shape, modes, dim = convert_params(shape, modes, dim=None) - check_params(shape, modes, dim) - - self.n = n - self.ndim = ndim - self.shape = shape - self.scale = float(np.prod(shape) ** .5) - - self.dft_cell = RDFTn(shape, compute_dtype) - - # use mask to assemble slices of Tensors, avoiding dynamic shape - mask_x0 = np.ones(self.n//2 + 1) - mask_xm = np.ones(self.n//2 + 1) - mask_y0 = np.ones(self.shape) - mask_z0 = np.ones(self.shape) - mask_x0[0] = 0 - mask_xm[-1] = 0 - if self.ndim > 1: - mask_y0[..., 0, :] = 0 - if self.ndim > 2: - mask_z0[..., 0, :, :] = 0 - - self.mask_x0 = Tensor(mask_x0, dtype=compute_dtype, const_arg=True) - self.mask_xm = Tensor(mask_xm, dtype=compute_dtype, const_arg=True) - self.mask_y0 = Tensor(mask_y0, dtype=compute_dtype, const_arg=True) - self.mask_z0 = Tensor(mask_z0, dtype=compute_dtype, const_arg=True) - - self.fliper = MyFlip() - self.roller = MyRoll() + def set_options(self, ndim, norm): + inv = False + scale = { + 'forward': 'n', + 'backward': None, + 'ortho': 'sqrtn', + }[norm] + r2c_flags = np.zeros(ndim, dtype=bool).tolist() + return inv, scale, r2c_flags def construct(self, ar, ai): ''' perform n-dimensional DFT on complex tensor ''' # n-D complex Fourier transform, output dimension (..., m, n) - # call dft for real & imag parts separately and then assemble - brr, bri = self.dft_cell(ar) # ar and ai must have same shape - bir, bii = self.dft_cell(ai) # the last ndim dimensions of ai must accord with shape - - n = self.n - - br_half1 = ops.pad((brr - bii) * self.mask_xm, [0, n//2 - 1]) - bi_half1 = ops.pad((bri + bir) * self.mask_xm, [0, n//2 - 1]) - - br_half2 = ops.pad((brr + bii) * self.mask_x0, [n//2 - 1, 0]) - bi_half2 = ops.pad((bir - bri) * self.mask_x0, [n//2 - 1, 0]) - br_half2 = self.roller(self.fliper(br_half2, dims=-1), n//2, dims=-1) - bi_half2 = self.roller(self.fliper(bi_half2, dims=-1), n//2, dims=-1) - - if self.ndim > 1: - br_half2_1 = br_half2 * (1 - self.mask_y0) - bi_half2_1 = bi_half2 * (1 - self.mask_y0) - br_half2_2 = br_half2 * self.mask_y0 - bi_half2_2 = bi_half2 * self.mask_y0 - br_half2 = br_half2_1 + self.roller(self.fliper(br_half2_2, dims=-2), 1, dims=-2) - bi_half2 = bi_half2_1 + self.roller(self.fliper(bi_half2_2, dims=-2), 1, dims=-2) - - if self.ndim > 2: - br_half2_1 = br_half2 * (1 - self.mask_z0) - bi_half2_1 = bi_half2 * (1 - self.mask_z0) - br_half2_2 = br_half2 * self.mask_z0 - bi_half2_2 = bi_half2 * self.mask_z0 - br_half2 = br_half2_1 + self.roller(self.fliper(br_half2_2, dims=-3), 1, dims=-3) - bi_half2 = bi_half2_1 + self.roller(self.fliper(bi_half2_2, dims=-3), 1, dims=-3) - - br = br_half1 + br_half2 - bi = bi_half1 + bi_half2 - - return br, bi + return self.dft1_seq((ar, ai)) -class IDFTn(nn.Cell): +class IDFTn(DFTn): r""" 1/2/3D discrete inverse Fourier transformation on complex number. The results should be same as `scipy.fft.ifftn() `_ . Args: shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. + norm (str): Normalization mode, should be one of 'forward', 'backward', 'ortho'. Default: 'backward', + same as torch.fft.irfftn + modes (tuple, int, None): The length of the output transform axis. The `modes` must be no greater than half of the + dimension of input 'x'. compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. Inputs: @@ -510,15 +445,15 @@ class IDFTn(nn.Cell): >>> print(br.shape) (2, 32, 512) """ - def __init__(self, shape, compute_dtype=mstype.float32): - super().__init__() - self.dft_cell = DFTn(shape, compute_dtype) - - def construct(self, ar, ai): - ''' perform n-dimensional iDFT on complex tensor ''' - scale = self.dft_cell.scale**2 - br, bi = self.dft_cell(ar, -ai) - return br / scale, -bi / scale + def set_options(self, ndim, norm): + inv = True + scale = { + 'forward': None, + 'backward': 'n', + 'ortho': 'sqrtn', + }[norm] + r2c_flags = np.zeros(ndim, dtype=bool).tolist() + return inv, scale, r2c_flags class DCT(nn.Cell): @@ -552,9 +487,11 @@ class DCT(nn.Cell): """ def __init__(self, shape, compute_dtype=mstype.float32): super().__init__() - self.dft_cell = DFTn(shape, compute_dtype) - assert self.dft_cell.ndim == 1, 'only support 1D dct' - n, = self.dft_cell.shape + + n = convert_shape(shape) + + self.dft_cell = DFTn(n, compute_dtype=compute_dtype) + w = Tensor(np.arange(n) * np.pi / (2 * n), dtype=compute_dtype) self.cosw = ops.cos(w) self.sinw = ops.sin(w) @@ -603,12 +540,13 @@ class IDCT(nn.Cell): def __init__(self, shape, compute_dtype=mstype.float32): super().__init__() - self.dft_cell = IRDFTn(shape, compute_dtype) - assert self.dft_cell.ndim == 1, 'only support 1D dct' - n, = self.dft_cell.shape - assert n % 2 == 0, 'only support even length' # n has to be even, or IRDFTn would fail + n = convert_shape(shape) + + # assert n % 2 == 0, 'only support even length' # n has to be even, or IRDFTn would fail + + self.dft_cell = IRDFTn(n, compute_dtype=compute_dtype) - w = Tensor(np.arange(0, n // 2 + 1, 1) * np.pi / (2 * n), dtype=compute_dtype) + w = Tensor(np.arange(n // 2 + 1) * np.pi / (2 * n), dtype=compute_dtype) self.cosw = ops.cos(w) self.sinw = ops.sin(w) @@ -628,6 +566,9 @@ class IDCT(nn.Cell): c2 = self.fliper(c[..., (n + 1) // 2:], dims=-1) d1 = ops.pad(c1.reshape(-1)[..., None], (0, 1)).reshape(*c1.shape[:-1], -1) d2 = ops.pad(c2.reshape(-1)[..., None], (1, 0)).reshape(*c2.shape[:-1], -1) + # in case n is odd, d1 and d2 need to be aligned + d1 = d1[..., :n] + d2 = ops.pad(d2, (0, n % 2)) return d1 + d2 @@ -662,8 +603,9 @@ class DST(nn.Cell): """ def __init__(self, shape, compute_dtype=mstype.float32): super().__init__() - self.dft_cell = DCT(shape, compute_dtype) - multiplier = np.ones(shape) + n = convert_shape(shape) + self.dft_cell = DCT(n, compute_dtype=compute_dtype) + multiplier = np.ones(n) multiplier[..., 1::2] *= -1 self.multiplier = Tensor(multiplier, dtype=compute_dtype) @@ -703,8 +645,9 @@ class IDST(nn.Cell): """ def __init__(self, shape, compute_dtype=mstype.float32): super().__init__() - self.dft_cell = IDCT(shape, compute_dtype) - multiplier = np.ones(shape) + n = convert_shape(shape) + self.dft_cell = IDCT(n, compute_dtype=compute_dtype) + multiplier = np.ones(n) multiplier[..., 1::2] *= -1 self.multiplier = Tensor(multiplier, dtype=compute_dtype) diff --git a/tests/st/mindflow/networks/fno/test_fno.py b/tests/st/mindflow/networks/fno/test_fno.py index 10e9ad45c..7fbc0c437 100644 --- a/tests/st/mindflow/networks/fno/test_fno.py +++ b/tests/st/mindflow/networks/fno/test_fno.py @@ -20,7 +20,7 @@ import numpy as np from mindspore import Tensor, context, set_seed, load_param_into_net, load_checkpoint from mindspore import dtype as mstype from mindflow.cell import FNO1D, FNO2D, FNO3D -from mindflow.cell.neural_operators.dft import SpectralConv1dDft, SpectralConv2dDft, SpectralConv3dDft +from mindflow.cell.neural_operators.fno_sp import SpectralConv1dDft, SpectralConv2dDft, SpectralConv3dDft RTOL = 0.001 set_seed(123456) diff --git a/tests/st/mindflow/operators/test_fourier.py b/tests/st/mindflow/operators/test_fourier.py index 44f354cc8..188e24720 100644 --- a/tests/st/mindflow/operators/test_fourier.py +++ b/tests/st/mindflow/operators/test_fourier.py @@ -30,7 +30,7 @@ sys.path.append(PROJECT_ROOT) # pylint: disable=wrong-import-position -from common.cell import FP32_RTOL +from common.cell import FP32_RTOL, FP16_RTOL, FP32_ATOL, FP16_ATOL from common.cell.utils import compare_output # pylint: enable=wrong-import-position @@ -57,7 +57,8 @@ def gen_input(shape=(5, 6, 4, 8), rand_test=True): @pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) @pytest.mark.parametrize('ndim', [1, 2, 3]) -def test_rdft_accuracy(device_target, mode, ndim): +@pytest.mark.parametrize('compute_dtype', [ms.float32, ms.float16]) +def test_rdft_accuracy(device_target, mode, ndim, compute_dtype): """ Feature: Test RDFTn & IRDFTn accuracy Description: Input random tensor, compare the results of RDFTn and IRDFTn with numpy results @@ -68,12 +69,15 @@ def test_rdft_accuracy(device_target, mode, ndim): shape = a.shape b = np.fft.rfftn(a.real, s=a.shape[-ndim:], axes=range(-ndim, 0)) - br, bi = RDFTn(shape[-ndim:])(ar) - cr = IRDFTn(shape[-ndim:])(br, bi) + br, bi = RDFTn(shape[-ndim:], compute_dtype=compute_dtype)(ar) + cr = IRDFTn(shape[-ndim:], compute_dtype=compute_dtype)(br, bi) - assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(b.imag, bi.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + rtol = FP32_RTOL if compute_dtype == ms.float32 else FP16_RTOL * 10 + atol = FP32_ATOL if compute_dtype == ms.float32 else FP16_ATOL * 20 + + assert compare_output(br.numpy(), b.real, rtol, atol) + assert compare_output(bi.numpy(), b.imag, rtol, atol) + assert compare_output(cr.numpy(), a.real, rtol, atol) @pytest.mark.level0 @@ -82,7 +86,8 @@ def test_rdft_accuracy(device_target, mode, ndim): @pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) @pytest.mark.parametrize('ndim', [1, 2, 3]) -def test_dft_accuracy(device_target, mode, ndim): +@pytest.mark.parametrize('compute_dtype', [ms.float32, ms.float16]) +def test_dft_accuracy(device_target, mode, ndim, compute_dtype): """ Feature: Test DFTn & IDFTn accuracy Description: Input random tensor, compare the results of DFTn and IDFTn with numpy results @@ -93,13 +98,16 @@ def test_dft_accuracy(device_target, mode, ndim): shape = a.shape b = np.fft.fftn(a, s=a.shape[-ndim:], axes=range(-ndim, 0)) - br, bi = DFTn(shape[-ndim:])(ar, ai) - cr, ci = IDFTn(shape[-ndim:])(br, bi) + br, bi = DFTn(shape[-ndim:], compute_dtype=compute_dtype)(ar, ai) + cr, ci = IDFTn(shape[-ndim:], compute_dtype=compute_dtype)(br, bi) + + rtol = FP32_RTOL if compute_dtype == ms.float32 else FP16_RTOL * 10 + atol = FP32_ATOL if compute_dtype == ms.float32 else FP16_ATOL * 20 - assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(b.imag, bi.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) - assert compare_output(a.imag, ci.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + assert compare_output(br.numpy(), b.real, rtol, atol) + assert compare_output(bi.numpy(), b.imag, rtol, atol) + assert compare_output(cr.numpy(), a.real, rtol, atol) + assert compare_output(ci.numpy(), a.imag, rtol, atol) @pytest.mark.level0 @@ -107,7 +115,8 @@ def test_dft_accuracy(device_target, mode, ndim): @pytest.mark.env_onecard @pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) -def test_dct_accuracy(device_target, mode): +@pytest.mark.parametrize('compute_dtype', [ms.float32, ms.float16]) +def test_dct_accuracy(device_target, mode, compute_dtype): """ Feature: Test DCT & IDCT accuracy Description: Input random tensor, compare the results of DCT and IDCT with numpy results @@ -118,11 +127,14 @@ def test_dct_accuracy(device_target, mode): shape = a.shape b = dct(a.real) - br = DCT(shape[-1:])(ar) - cr = IDCT(shape[-1:])(br) + br = DCT(shape[-1:], compute_dtype=compute_dtype)(ar) + cr = IDCT(shape[-1:], compute_dtype=compute_dtype)(br) + + rtol = FP32_RTOL if compute_dtype == ms.float32 else FP16_RTOL * 10 + atol = FP32_ATOL if compute_dtype == ms.float32 else FP16_ATOL * 20 - assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + assert compare_output(br.numpy(), b.real, rtol, atol) + assert compare_output(cr.numpy(), a.real, rtol, atol) @pytest.mark.level0 @@ -130,7 +142,8 @@ def test_dct_accuracy(device_target, mode): @pytest.mark.env_onecard @pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) -def test_dst_accuracy(device_target, mode): +@pytest.mark.parametrize('compute_dtype', [ms.float32, ms.float16]) +def test_dst_accuracy(device_target, mode, compute_dtype): """ Feature: Test DST & IDST accuracy Description: Input random tensor, compare the results of DST and IDST with numpy results @@ -141,11 +154,14 @@ def test_dst_accuracy(device_target, mode): shape = a.shape b = dst(a.real) - br = DST(shape[-1:])(ar) - cr = IDST(shape[-1:])(br) + br = DST(shape[-1:], compute_dtype=compute_dtype)(ar) + cr = IDST(shape[-1:], compute_dtype=compute_dtype)(br) - assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) - assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + rtol = FP32_RTOL if compute_dtype == ms.float32 else FP16_RTOL * 10 + atol = FP32_ATOL if compute_dtype == ms.float32 else FP16_ATOL * 20 + + assert compare_output(br.numpy(), b.real, rtol, atol) + assert compare_output(cr.numpy(), a.real, rtol, atol) @pytest.mark.level0 @@ -201,7 +217,8 @@ def test_dft_speed(device_target, mode, ndim): @pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) @pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) @pytest.mark.parametrize('ndim', [1, 2, 3]) -def test_dft_grad(device_target, mode, ndim): +@pytest.mark.parametrize('compute_dtype', [ms.float32, ms.float16]) +def test_dft_grad(device_target, mode, ndim, compute_dtype): """ Feature: Test the correctness of DFTn & IDFTn grad calculation Description: Input random tensor, compare the autograd results with theoretic solutions @@ -211,7 +228,7 @@ def test_dft_grad(device_target, mode, ndim): a, ar, ai = gen_input() shape = a.shape - dft_cell = DFTn(shape[-ndim:]) + dft_cell = DFTn(shape[-ndim:], compute_dtype=compute_dtype) def forward_fn(xr, xi): yr, yi = dft_cell(xr, xi) @@ -224,5 +241,8 @@ def test_dft_grad(device_target, mode, ndim): b = np.fft.fftn(a, s=a.shape[-ndim:], axes=range(-ndim, 0)) g = np.fft.ifftn(b, s=a.shape[-ndim:], axes=range(-ndim, 0)) * 2 * np.prod(a.shape[-ndim:]) - assert compare_output(g.real, g1.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(g)) - assert compare_output(g.imag, g2.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(g)) + rtol = FP32_RTOL if compute_dtype == ms.float32 else FP16_RTOL * 10 + atol = FP32_ATOL if compute_dtype == ms.float32 else FP16_ATOL * 500 # grad func leads to larger error + + assert compare_output(g1.numpy(), g.real, rtol, atol) + assert compare_output(g2.numpy(), g.imag, rtol, atol) -- Gitee From c247ae32dd9a6017abda952be2005196d879128f Mon Sep 17 00:00:00 2001 From: Luo Yuanhanyu Date: Wed, 29 Oct 2025 16:13:26 +0000 Subject: [PATCH 30/30] =?UTF-8?q?!2329=20update=20MindSPONGE/applications/?= =?UTF-8?q?medformer/README.md.=20*=20Update=20Medformer=20*=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../applications/research/medformer/README.md | 42 +++ .../research/medformer/__init__.py | 12 + .../research/medformer/module/MedFormer.py | 110 ++++++ .../research/medformer/requirements.txt | 7 + .../applications/research/medformer/train.py | 324 ++++++++++++++++++ 5 files changed, 495 insertions(+) create mode 100644 MindSPONGE/applications/research/medformer/README.md create mode 100644 MindSPONGE/applications/research/medformer/__init__.py create mode 100644 MindSPONGE/applications/research/medformer/module/MedFormer.py create mode 100644 MindSPONGE/applications/research/medformer/requirements.txt create mode 100644 MindSPONGE/applications/research/medformer/train.py diff --git a/MindSPONGE/applications/research/medformer/README.md b/MindSPONGE/applications/research/medformer/README.md new file mode 100644 index 000000000..ae6dee737 --- /dev/null +++ b/MindSPONGE/applications/research/medformer/README.md @@ -0,0 +1,42 @@ +# MedFormer: Transformer-based Drug Perturbation Prediction + +MedFormer is a drug perturbation prediction framework based on the Transformer architecture, designed to predict the transcriptional responses of small molecule drugs under different cellular states. By integrating drug molecular fingerprints, baseline transcriptional states, and gene embeddings, it achieves high-precision predictions for unseen drugs and cell types, and is scalable to single-cell data. + +This project is based on [MindSPONGE](https://gitee.com/mindspore/mindscience/tree/master/MindSPONGE) and implemented in Python. + +--- + +## 🔧 requirement + +- Python 3.8+ + +- mindspore >= 3.9.0 + +- numpy + +- pandas + +- scikit-learn + +- rdkit + +- tqdm + +--- + +## Quick start + +Raw data link: +https://zenodo.org/records/14230870 + +Essential data link: +https://pan.baidu.com/s/1AKJT6gvSf05PgYit6SPbYQ?pwd=f5iy + +Run: +`python train.py --split_key drug_split_0 --ablation False --device_id 0` + +`split_key` indicates which fold of the k-fold cross-validation should be used as the training set. + +`ablation` indicates whether an ablation experiment is to be conducted. + +`device_id` represents the ID of the computing card being used. It can be filled in according to the actual situation. By default, the idle computing card among all available ones will be selected automatically. \ No newline at end of file diff --git a/MindSPONGE/applications/research/medformer/__init__.py b/MindSPONGE/applications/research/medformer/__init__.py new file mode 100644 index 000000000..d00e2d8a6 --- /dev/null +++ b/MindSPONGE/applications/research/medformer/__init__.py @@ -0,0 +1,12 @@ +# Copyright 2025 Yuanhanyu Luo & Linchang Zhu +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/MindSPONGE/applications/research/medformer/module/MedFormer.py b/MindSPONGE/applications/research/medformer/module/MedFormer.py new file mode 100644 index 000000000..5dc8026ce --- /dev/null +++ b/MindSPONGE/applications/research/medformer/module/MedFormer.py @@ -0,0 +1,110 @@ +# Copyright 2025 Yuanhanyu Luo & Linchang Zhu + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module defines the MedFormer model for gene expression prediction. +""" + +import mindspore as ms +from mindspore import nn +from mindspore import Parameter +from mindspore import ops + +class GenePertFormer(ms.nn.Cell): + """ + GenePertFormer model for gene expression prediction, combining gene, drug, and cell features. + """ + def __init__(self, gene_vocab_size=23185, drug_dim=1024, cell_dim=82, + hidden_dim=256, n_layers=4, n_heads=1, dropout=0.1, + cell_input_dim=978, use_cell_expr=False): + super().__init__() + self.hidden_dim = hidden_dim + + # Embeddings + self.gene_embedding = nn.Embedding(gene_vocab_size, hidden_dim) + self.expr_embedding = nn.Dense(1, hidden_dim) + self.drug_embedding = nn.Dense(drug_dim, hidden_dim) + + self.use_cell_expr = use_cell_expr + if use_cell_expr: + self.cell_embedding = nn.SequentialCell([ + nn.Dense(cell_input_dim, 512), + nn.ReLU(), + nn.Dropout(0.1), + nn.Dense(512, hidden_dim) + ]) + else: + self.cell_embedding = nn.Dense(cell_dim, hidden_dim) + + # CLS Token & positional embedding + self.cls_token = Parameter(ops.StandardNormal()((1, 1, hidden_dim)), name='cls_token') + # Line too long fixed by splitting the long line + self.pos_embedding = \ + Parameter(ops.StandardNormal()((1, gene_vocab_size + 3, hidden_dim)), name='pos_embedding') + + # Transformer Encoder + encoder_layer = nn.TransformerEncoderLayer(d_model=hidden_dim, nhead=n_heads, + dim_feedforward=4 * hidden_dim, dropout=dropout, + batch_first=True) + self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=n_layers) + + # Prediction Heads + self.to_gene_pred = nn.Dense(hidden_dim, 1) + self.cls_head = nn.Dense(hidden_dim, hidden_dim) + self.recon_head = nn.SequentialCell([ + nn.Dense(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Dense(hidden_dim, cell_input_dim) + ]) + + def construct(self, gene_ids, gene_expr, drug_fp, cell_feat, mask=None): + """ + Forward pass for the GenePertFormer model. + + Args: + gene_ids (Tensor): Tensor of gene IDs. + gene_expr (Tensor): Tensor of gene expressions. + drug_fp (Tensor): Tensor of drug fingerprints. + cell_feat (Tensor): Tensor of cell features. + mask (Tensor, optional): Mask for the transformer encoder. Defaults to None. + + Returns: + Tuple[Tensor, Tensor, Tensor]: Predicted gene expression, CLS token output, and reconstructed cell features. + """ + batch_size, _ = gene_ids.shape # Renamed B to batch_size, G to _ (unused) + + id_embed = self.gene_embedding(gene_ids) # [batch_size, G, H] + expr_embed = self.expr_embedding(gene_expr) # [batch_size, G, H] + gene_embed = id_embed + expr_embed # [batch_size, G, H] + + drug_token = self.drug_embedding(drug_fp).expand_dims(1) # [batch_size, 1, H] + + if self.use_cell_expr: + cell_raw = ops.Squeeze(-1)(cell_feat) # [batch_size, G] + cell_embed = self.cell_embedding(cell_raw) # [batch_size, H] + else: + cell_embed = self.cell_embedding(cell_feat) # [batch_size, H] + cell_token = cell_embed.expand_dims(1) # [batch_size, 1, H] + + cls = ops.BroadcastTo((batch_size, 1, self.hidden_dim))(self.cls_token) + tokens = ops.Concat(axis=1)((cls, drug_token, cell_token, gene_embed)) + tokens = tokens + self.pos_embedding[:, :tokens.shape[1], :] + + x = self.encoder(tokens, src_key_padding_mask=mask) + + pred_gene = self.to_gene_pred(x[:, 3:, :]).squeeze(-1) # [batch_size, G] + cls_out = self.cls_head(x[:, 0, :]) + recon = self.recon_head(cell_embed) + + return pred_gene, cls_out, recon diff --git a/MindSPONGE/applications/research/medformer/requirements.txt b/MindSPONGE/applications/research/medformer/requirements.txt new file mode 100644 index 000000000..9f9d297eb --- /dev/null +++ b/MindSPONGE/applications/research/medformer/requirements.txt @@ -0,0 +1,7 @@ +python +mindspore==3.9.0 +numpy +pandas +scikit-learn +rdkit +tqdm diff --git a/MindSPONGE/applications/research/medformer/train.py b/MindSPONGE/applications/research/medformer/train.py new file mode 100644 index 000000000..4198ed922 --- /dev/null +++ b/MindSPONGE/applications/research/medformer/train.py @@ -0,0 +1,324 @@ +# Copyright 2025 Yuanhanyu Luo & Linchang Zhu + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements the training and evaluation pipeline for the GenePertFormer model. +It handles data loading, preprocessing, model definition, training loop, and result visualization. +""" +import argparse +import os +import json +import logging +from datetime import datetime + +import numpy as np +import scanpy as sc +import matplotlib.pyplot as plt +import seaborn as sns + +import mindspore as ms +from mindspore import nn, context +import mindspore.dataset as ds + +# Grouped scipy imports +from scipy import sparse +from scipy.stats import pearsonr +from sklearn.metrics import r2_score + +import wandb + +from rdkit import Chem +from rdkit.Chem import AllChem + +from module.MedFormer import GenePertFormer + + +## Execution Mode +context.set_context(mode=context.GRAPH_MODE, device_target="Ascend") + +# Configure logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + + +# utils +def parse_args(): + """ + Parses command line arguments for the perturbation model. + + Returns: + argparse.Namespace: An object containing the parsed arguments. + """ + parser = argparse.ArgumentParser(description="MindSpore version of perturbation model") + parser.add_argument("--split_key", default="drug_split_0", type=str) + parser.add_argument("--ablation", default=None, type=str) + return parser.parse_args() + + +def shuffle_adata(adata_obj): + """ + Shuffles the AnnData object in place. + + Args: + adata_obj (anndata.AnnData): The AnnData object to be shuffled. + + Returns: + anndata.AnnData: The shuffled AnnData object. + """ + if sparse.issparse(adata_obj.X): + adata_obj.X = adata_obj.X.A + perm = np.random.permutation(adata_obj.shape[0]) + return adata_obj[perm, :] + + +def train_valid_test_split(adata_obj, split_key): + """ + Splits the AnnData object into training, validation, and test sets, + including control samples in all sets. + + Args: + adata_obj (anndata.AnnData): The AnnData object containing the data. + split_key (str): The observation key used for splitting (e.g., "drug_split_0"). + + Returns: + Tuple[anndata.AnnData, anndata.AnnData, anndata.AnnData]: + Train, validation, and test AnnData objects. + """ + shuffled = shuffle_adata(adata_obj) + adata_ctrl0 = adata_obj[adata_obj.obs["control"] == 0] + train_idx = adata_ctrl0.obs[adata_ctrl0.obs[split_key] == "train"].index.tolist() + valid_idx = adata_ctrl0.obs[adata_ctrl0.obs[split_key] == "valid"].index.tolist() + test_idx = adata_ctrl0.obs[adata_ctrl0.obs[split_key] == "test"].index.tolist() + ctrl_idx = adata_obj.obs[adata_obj.obs["control"] == 1].index.tolist() + + def subset(idx_list): + return shuffled[idx_list + ctrl_idx] + return subset(train_idx), subset(valid_idx), subset(test_idx) + + +# ------------ Dataset & DataLoader ------------ +def drug_smiles_encode(drug_smiles_list: list, num_bits=1024): + """ + Encodes a list of drug SMILES strings into Morgan fingerprints. + + Args: + drug_smiles_list (list): A list of SMILES strings. + num_bits (int): The number of bits for the Morgan fingerprint. + + Returns: + numpy.ndarray: A NumPy array of drug fingerprints. + + Raises: + ValueError: If an invalid SMILES string is encountered. + """ + arr = np.zeros((len(drug_smiles_list), num_bits), dtype=np.float32) + for i, smiles in enumerate(drug_smiles_list): + mol = Chem.MolFromSmiles(smiles) + if mol is None: + raise ValueError("Invalid SMILES") # Changed to lazy formatting + bits = AllChem.GetMorganFingerprintAsBitVect(mol, 2, useFeatures=True, nBits=num_bits).ToBitString() + arr[i] = np.array(list(bits), dtype=np.float32) + return arr + + +class GenePertAnnDatasetMS: + """ + A MindSpore Dataset for GenePert data, wrapping an AnnData object. + """ + def __init__(self, adata_obj, gene2id_path, control_key="condition", smiles_key="SMILES", cell_key="cell_id"): + """ + Initializes the GenePertAnnDatasetMS. + + Args: + adata_obj (anndata.AnnData): The AnnData object containing the data. + gene2id_path (str): Path to the JSON file mapping gene names to IDs. + control_key (str): Observation key for control samples. + smiles_key (str): Observation key for drug SMILES strings. + cell_key (str): Observation key for cell IDs. + """ + self.adata = adata_obj + self.control_key = control_key + self.smiles_key = smiles_key + self.cell_key = cell_key + self.drug_dim = 1024 + + with open(gene2id_path, "r", encoding='utf-8') as f: + self.gene2id = json.load(f) + self.idx = np.where(self.adata.obs[self.control_key] != "control")[0] + self.gene_order = self.adata.var_names.tolist() + cells = self.adata.obs[self.cell_key].unique().tolist() + self.cell2id = {cid: i for i, cid in enumerate(sorted(cells))} + + def __len__(self): + """Returns the number of samples in the dataset.""" + return len(self.idx) + + def __getitem__(self, idx): + """ + Retrieves a single sample from the dataset. + + Args: + idx (int): Index of the sample to retrieve. + + Returns: + Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]: + gene_ids, treated gene expression, drug fingerprint, cell one-hot encoding, control gene expression. + """ + si = self.idx[idx] + sample = self.adata[si] + treat = sample.X.toarray().flatten() + ctrl_id = sample.obs["paired_control_index"].values[0] + ctrl_i = self.adata.obs_names.get_loc(ctrl_id) + ctrl = self.adata[ctrl_i].X.toarray().flatten() + gene_ids = np.array([self.gene2id[g] for g in self.gene_order], dtype=np.int32) + treat = treat.astype(np.float32).reshape(-1, 1) + ctrl = ctrl.astype(np.float32) + smiles = sample.obs[self.smiles_key].values[0] + drug_fp = drug_smiles_encode([smiles], self.drug_dim)[0] + cid = sample.obs[self.cell_key].values[0] + cell_onehot = np.eye(len(self.cell2id), dtype=np.float32)[self.cell2id[cid]] + return gene_ids, treat, drug_fp, cell_onehot, ctrl + + +def build_ms_dataset(adata_obj, gene2id_path, batch_size=64, shuffle_data=True): + """ + Builds a MindSpore GeneratorDataset from an AnnData object. + + Args: + adata_obj (anndata.AnnData): The AnnData object to build the dataset from. + gene2id_path (str): Path to the JSON file mapping gene names to IDs. + batch_size (int): Batch size for the dataset. + shuffle_data (bool): Whether to shuffle the dataset. + + Returns: + mindspore.dataset.engine.datasets.GeneratorDataset: The built MindSpore dataset. + """ + ds_src = GenePertAnnDatasetMS(adata_obj, gene2id_path) + ms_ds = ds.GeneratorDataset(ds_src, + ["gene_ids", "gene_expr", "drug_fp", "cell_feat", "control_expr"], + shuffle=shuffle_data) + ms_ds = ms_ds.batch(batch_size, drop_remainder=True) + return ms_ds + + +# ------------ training process ------------ +args = parse_args() +original_adata = sc.read_h5ad("./Lincs_L1000.h5ad") +train_data, valid_data, test_data = train_valid_test_split(original_adata, args.split_key) + +timestamp = datetime.now().strftime("%Y%m%d_%H%M") +save_dir = f"./MSmodel_{args.split_key}_{timestamp}" +os.makedirs(save_dir, exist_ok=True) + +train_ds = build_ms_dataset(train_data, "./data/gene2id.json", batch_size=64, shuffle_data=True) +valid_ds = build_ms_dataset(valid_data, "./data/gene2id.json", batch_size=64, shuffle_data=False) +test_ds = build_ms_dataset(test_data, "./data/gene2id.json", batch_size=64, shuffle_data=False) + +# define model +model = GenePertFormer(drug_dim=1024, cell_dim=82, hidden_dim=256, use_cell_expr=True, cell_input_dim=978) + +# loss +loss_fn = nn.MSELoss() +optimizer = nn.Adam(model.trainable_params(), learning_rate=0.0005) + +net = nn.WithLossCell(model, loss_fn) +train_net = nn.TrainOneStepCell(net, optimizer) +train_net.set_train() + +# wandb init +wandb.init(project="GenePertFormerMS", name=f"ms_{args.split_key}_{datetime.now().strftime('%Y%m%d_%H%M')}") + +best_val = 1e9 +patience, counter = 5, 0 +train_losses, val_losses = [], [] + +for ep in range(100): + total, count = 0.0, 0 + for b in train_ds.create_tuple_iterator(): + _, loss = train_net(*b) + total += loss.asnumpy() + count += 1 + avg_train = total / count + train_losses.append(avg_train) + + # validation + model.set_train(False) + total, count = 0.0, 0 + for b in valid_ds.create_tuple_iterator(): + out = model(*b[:-1]) + v_loss = loss_fn(out[0], b[1]) + total += v_loss.asnumpy() + count += 1 + avg_val = total / count + val_losses.append(avg_val) + model.set_train(True) + + wandb.log({"epoch": ep, "train_loss": avg_train, "val_loss": avg_val}) + if avg_val < best_val: + best_val = avg_val + counter = 0 + ms.save_checkpoint(model, os.path.join(save_dir, "best.ckpt")) + else: + counter += 1 + if counter >= patience: + break + +# === results === +pred_list, true_list = [], [] + +for batch in test_ds.create_tuple_iterator(): + pred_batch, _, _ = model(*batch[:-1]) + true_batch = batch[1].asnumpy() + pred_list.append(pred_batch.asnumpy()) + true_list.append(true_batch) + +pred_array = np.vstack(pred_list) +true_array = np.vstack(true_list) + +r2 = np.nanmean([r2_score(t, p) for t, p in zip(true_array, pred_array)]) +pcc = np.nanmean([pearsonr(t, p)[0] for t, p in zip(true_array, pred_array)]) +logger.info("Test R²: %.4f, Pearson: %.4f", r2, pcc) # Changed to lazy formatting + +np.savez(os.path.join(save_dir, f"{args.split_key}_test_result_ms.npz"), + pred=pred_array, + true=true_array, + r2=r2, pcc=pcc) + +wandb.log({"test_r2": r2, "test_pearson": pcc}) + +# === visualization === +flat_true = true_array.flatten() +flat_pred = pred_array.flatten() +mask = ~np.isnan(flat_true) & ~np.isnan(flat_pred) +flat_true, flat_pred = flat_true[mask], flat_pred[mask] + +sns.set_theme(style="ticks") +fig, ax = plt.subplots(figsize=(6, 6)) +ax.scatter(flat_true, flat_pred, alpha=0.4, s=3, color='steelblue') +ax.plot([flat_true.min(), flat_true.max()], [flat_true.min(), flat_true.max()], + 'r--', linewidth=1.2) +ax.set_xlabel("True Expression", fontsize=12) +ax.set_ylabel("Predicted Expression", fontsize=12) +ax.set_title(f"True vs Predicted (MS)\nR² = {r2:.3f}, PCC = {pcc:.3f}", fontsize=13) +sns.despine() +fig.tight_layout() + +fig_path = os.path.join(save_dir, f"{args.split_key}_true_vs_pred_ms.png") +fig.savefig(fig_path, dpi=300) +plt.close(fig) +wandb.log({"true_vs_pred_scatter_ms": wandb.Image(fig_path)}) -- Gitee

    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 ds.GeneratorDataset: + """Builds the dataloader from a config file. + + Args: + dataset_path: Dataset path. + num_workers: The number of workers for each dataset. + batch_size: The batch_size config for each dataset. + augmentation: If rotation augmentation is used. + target_config: The target config. + shuffle: If the dataset should be shuffled. + parallel_mode: The parallel mode to use, e.g., "DATA_PARALLEL" or "NONE". + + Returns: + The Dataloader. + """ + log_loading = f"Loading datasets: {dataset_path} with {num_workers} workers. " + dataset = AseSqliteDataset( + dataset_path, target_config=target_config, augmentation=augmentation, **kwargs + ) + + log_loading += f"Total dataset size: {len(dataset)} samples" + logging.info(log_loading) + + dataset = BufferData(dataset, shuffle=shuffle) + if parallel_mode == "DATA_PARALLEL": + rank_id = get_rank() + rank_size = get_group_size() + dataloader = [ + [dataset[j] for j in range(i, min(i + batch_size, len(dataset)))] \ + for i in range(0, len(dataset), batch_size) + ] + dataloader = [ + base.batch_graphs( + data[rank_id*len(data)//rank_size : (rank_id+1)*len(data)//rank_size] + ) for data in dataloader + ] + else: + dataloader = [ + base.batch_graphs( + [dataset[j] for j in range(i, min(i + batch_size, len(dataset)))] + ) for i in range(0, len(dataset), batch_size) + ] + + return dataloader + + +def run(args, parallel_mode="NONE"): + """Training Loop. + + Args: + config (DictConfig): Config for training loop. + parallel_mode (str): The parallel mode to use, e.g., "DATA_PARALLEL" or "NONE". + """ + utils.seed_everything(args.random_seed) + + # Load dataset + train_loader = build_loader( + dataset_path=args.train_data_path, + num_workers=args.num_workers, + batch_size=args.batch_size, + target_config={"graph": ["energy", "stress"], "node": ["forces"]}, + augmentation=True, + ) + val_loader = build_loader( + dataset_path=args.val_data_path, + num_workers=args.num_workers, + batch_size=1000, + target_config={"graph": ["energy", "stress"], "node": ["forces"]}, + augmentation=False, + shuffle=False, + ) + num_steps = len(train_loader) + + # Instantiate model + pretrained_weights_path = os.path.join(args.checkpoint_path, "orb-mptraj-only-v2.ckpt") + model = pretrained.orb_mptraj_only_v2(pretrained_weights_path) + loss_fn = OrbLoss(model) + model_params = sum(p.size for p in model.trainable_params() if p.requires_grad) + logging.info("Model has %d trainable parameters.", model_params) + + total_steps = args.max_epochs * num_steps + optimizer, lr_scheduler = utils.get_optim(args.lr, total_steps, model) + + # Fine-tuning loop + start_epoch = 0 + train_time = timeit.default_timer() + for epoch in range(start_epoch, args.max_epochs): + train_metrics, val_metrics = finetune( + model=model, + loss_fn=loss_fn, + optimizer=optimizer, + train_dataloader=train_loader, + val_dataloader=val_loader, + lr_scheduler=lr_scheduler, + clip_grad=args.gradient_clip_val, + parallel_mode=parallel_mode, + ) + print(f'Epoch: {epoch}/{args.max_epochs}, \n train_metrics: {train_metrics}\n val_metrics: {val_metrics}') + + # Save checkpoint from last epoch + if epoch == args.max_epochs - 1: + # create ckpts folder if it does not exist + if not os.path.exists(args.checkpoint_path): + os.makedirs(args.checkpoint_path) + if parallel_mode == "DATA_PARALLEL": + rank_id = get_rank() + rank_size = get_group_size() + ms.save_checkpoint( + model, + os.path.join( + args.checkpoint_path, + f"orb-ft-parallel[{rank_id}-{rank_size}]-checkpoint_epoch{epoch}.ckpt" + ), + ) + else: + ms.save_checkpoint( + model, + os.path.join(args.checkpoint_path, f"orb-ft-checkpoint_epoch{epoch}.ckpt"), + ) + logging.info("Checkpoint saved to %s", args.checkpoint_path) + logging.info("Training time: %.5f seconds", timeit.default_timer() - train_time) + + +def main(): + """Main.""" + parser = argparse.ArgumentParser( + description="Finetune orb model", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "--config", type=str, default="configs/config.yaml", help="Path to config file" + ) + parser.add_argument( + "--device_target", + type=str, + default="Ascend", + help="The target device to run, support 'Ascend'" + ) + parser.add_argument( + "--device_id", default=0, type=int, help="device index to use." + ) + parser.add_argument( + "--parallel_mode", + type=str, + default="NONE", + choices=["DATA_PARALLEL", "NONE"], + help="Parallel mode, support 'DATA_PARALLEL', 'NONE'" + ) + args = parser.parse_args() + + if args.parallel_mode.upper() == "DATA_PARALLEL": + ms.set_context( + mode=context.PYNATIVE_MODE, + device_target=args.device_target, + pynative_synchronize=True, + ) + # Set parallel context + ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True) + init() + ms.set_seed(1) + else: + ms.set_context( + mode=context.PYNATIVE_MODE, + device_target=args.device_target, + device_id=args.device_id, + pynative_synchronize=True, + ) + configs = utils.load_cfg(args.config) + warnings.filterwarnings("ignore") + + run(configs, args.parallel_mode) + + +if __name__ == "__main__": + main() diff --git a/MindChemistry/applications/orb/requirement.txt b/MindChemistry/applications/orb/requirement.txt new file mode 100644 index 000000000..e3f1a95c7 --- /dev/null +++ b/MindChemistry/applications/orb/requirement.txt @@ -0,0 +1,8 @@ +python>=3.10 +cached_path>=1.6.7 +ase>=3.24.0 +numpy>=1.26.4 +scipy>=1.15.1 +dm-tree==0.1.8 +tqdm>=4.66.5 +mindspore==2.5.0 \ No newline at end of file diff --git a/MindChemistry/applications/orb/run.sh b/MindChemistry/applications/orb/run.sh new file mode 100644 index 000000000..e21df131a --- /dev/null +++ b/MindChemistry/applications/orb/run.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +export GLOG_v=3 +echo "==============================================================================================================" +echo "Please run the script as: " +echo "bash run.sh" +echo "==============================================================================================================" + +python finetune.py --device_target Ascend --device_id 7 diff --git a/MindChemistry/applications/orb/run_parallel.sh b/MindChemistry/applications/orb/run_parallel.sh new file mode 100644 index 000000000..e49c6ab71 --- /dev/null +++ b/MindChemistry/applications/orb/run_parallel.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +rm -rf msrun_log +mkdir msrun_log + +export GLOG_v=3 +echo "==============================================================================================================" +echo "Please run the script as: " +echo "bash run_parallel.sh" +echo "==============================================================================================================" + +msrun --worker_num=4 --local_worker_num=4 --master_port=8118 --log_dir=msrun_log --join=True --cluster_time_out=300 finetune.py --config configs/config_parallel.yaml --parallel_mode DATA_PARALLEL \ No newline at end of file diff --git a/MindChemistry/applications/orb/src/__init__.py b/MindChemistry/applications/orb/src/__init__.py new file mode 100644 index 000000000..328a08a65 --- /dev/null +++ b/MindChemistry/applications/orb/src/__init__.py @@ -0,0 +1,16 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""init""" diff --git a/MindChemistry/applications/orb/src/ase_dataset.py b/MindChemistry/applications/orb/src/ase_dataset.py new file mode 100644 index 000000000..f61b95aa3 --- /dev/null +++ b/MindChemistry/applications/orb/src/ase_dataset.py @@ -0,0 +1,239 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""ASE dataset""" + +import os +from typing import Dict, Optional, Tuple, Union + +import ase +import ase.db +import ase.db.row +import ase.stress +import numpy as np +import mindspore as ms +from mindspore import Tensor + +from src import atomic_system, property_definitions +from src.base import AtomGraphs +from src.utils import rand_matrix + + +class AseSqliteDataset: + """AseSqliteDataset. + + A MindSpore Dataset for reading ASE Sqlite serialized Atoms objects. + + Args: + dataset_path: Local path to read. + system_config: A config for controlling how an atomic system is represented. + target_config: A config for regression/classification targets. + augmentation: If random rotation augmentation is used. + + Returns: + An AseSqliteDataset. + """ + + def __init__( + self, + dataset_path: Union[str, os.PathLike], + system_config: Optional[atomic_system.SystemConfig] = None, + target_config: Optional[Dict] = None, + augmentation: Optional[bool] = True, + ): + super().__init__() + self.augmentation = augmentation + self.path = dataset_path + self.db = ase.db.connect(str(self.path), serial=True, type="db") + + self.feature_config = system_config + if target_config is None: + target_config = { + "graph": ["energy", "stress"], + "node": ["forces"], + "edge": [], + } + self.target_config = target_config + + def __getitem__(self, idx) -> AtomGraphs: + """Fetch an item from the db. + + Args: + idx: An index to fetch from the db file and convert to an AtomGraphs. + + Returns: + A AtomGraphs object containing everything the model needs as input, + positions and atom types and other auxiliary information, such as + fine tuning targets, or global graph features. + """ + # Sqlite db is 1 indexed. + row = self.db.get(idx + 1) + atoms = row.toatoms() + node_properties = property_definitions.get_property_from_row( + self.target_config["node"], row + ) + graph_property_dict = {} + for target_property in self.target_config["graph"]: + system_properties = property_definitions.get_property_from_row( + target_property, row + ) + # transform stress to voigt6 representation + if target_property == "stress" and len(system_properties.reshape(-1)) == 9: + system_properties = Tensor( + ase.stress.full_3x3_to_voigt_6_stress(system_properties.reshape(3, 3)), + dtype=ms.float32, + ).reshape(1, -1) + graph_property_dict[target_property] = system_properties + extra_targets = { + "node": {"forces": node_properties}, + "edge": {}, + "graph": graph_property_dict, + } + if self.augmentation: + atoms, extra_targets = random_rotations_with_properties(atoms, extra_targets) + + atom_graph = atomic_system.ase_atoms_to_atom_graphs( + atoms, + system_id=idx, + brute_force_knn=False, + ) + atom_graph = self._add_extra_targets(atom_graph, extra_targets) + + return atom_graph + + def get_atom(self, idx: int) -> ase.Atoms: + """Return the Atoms object for the dataset index.""" + row = self.db.get(idx + 1) + return row.toatoms() + + def get_atom_and_metadata(self, idx: int) -> Tuple[ase.Atoms, Dict]: + """Return the Atoms object plus a dict of metadata for the dataset index.""" + row = self.db.get(idx + 1) + return row.toatoms(), row.data + + def __len__(self) -> int: + """Return the dataset length.""" + return len(self.db) + + def __repr__(self) -> str: + """String representation of class.""" + return f"AseSqliteDataset(path={self.path})" + + def _add_extra_targets( + self, + atom_graph: AtomGraphs, + extra_targets: Dict[str, Dict], + ): + """Add extra features and targets to the AtomGraphs object. + + Args: + atom_graph: AtomGraphs object to add extra features and targets to. + extra_targets: Dictionary of extra targets to add. + """ + node_targets = ( + atom_graph.node_targets if atom_graph.node_targets is not None else {} + ) + node_targets = {**node_targets, **extra_targets["node"]} + + edge_targets = ( + atom_graph.edge_targets if atom_graph.edge_targets is not None else {} + ) + edge_targets = {**edge_targets, **extra_targets["edge"]} + + system_targets = ( + atom_graph.system_targets if atom_graph.system_targets is not None else {} + ) + system_targets = {**system_targets, **extra_targets["graph"]} + + return atom_graph._replace( + node_targets=node_targets if node_targets != {} else None, + edge_targets=edge_targets if edge_targets != {} else None, + system_targets=system_targets if system_targets != {} else None, + ) + + +def random_rotations_with_properties( + atoms: ase.Atoms, properties: dict +) -> Tuple[ase.Atoms, dict]: + """Randomly rotate atoms in ase.Atoms object. + + This exists to handle the case where we also need to rotate properties. + Currently we only ever do this for random rotations, but it could be extended. + + Args: + atoms (ase.Atoms): Atoms object to rotate. + properties (dict): Dictionary of properties to rotate. + """ + rand_rotation = rand_matrix(1)[0] + atoms.positions = atoms.positions @ rand_rotation + if atoms.cell is not None: + atoms.set_cell(atoms.cell.array @ rand_rotation) + + new_node_properties = {} + for key, v in properties["node"].items(): + if tuple(v.shape) == tuple(atoms.positions.shape): + new_node_properties[key] = v @ rand_rotation + else: + new_node_properties[key] = v + properties["node"] = new_node_properties + + if "stress" in properties["graph"]: + # Transformation rule of stress tensor + stress = properties["graph"]["stress"] + full_stress = ase.stress.voigt_6_to_full_3x3_stress(stress) + + if full_stress.shape != (3, 3): + full_stress = full_stress.reshape(3, 3) + + transformed = np.dot(np.dot(rand_rotation, full_stress), rand_rotation.T) + # Back to voigt notation, and shape (1, 6) for consistency with batching + properties["graph"]["stress"] = Tensor( + [ + transformed[0, 0], + transformed[1, 1], + transformed[2, 2], + transformed[1, 2], + transformed[0, 2], + transformed[0, 1], + ], + dtype=ms.float32, + ).unsqueeze(0) + + return atoms, properties + +class BufferData: + """Wrapper for a dataset. Loads all data into memory.""" + + def __init__(self, dataset, shuffle: bool = True): + """BufferData. + Args: + dataset: The dataset to wrap. + shuffle: If True, shuffle the data. + """ + self.data_objects = [dataset[i] for i in range(len(dataset))] + if shuffle: + self.shuffle() + + def __len__(self): + return len(self.data_objects) + + def __getitem__(self, index): + return self.data_objects[index] + + def shuffle(self): + """Shuffle the data.""" + indices = np.arange(len(self.data_objects)) + np.random.shuffle(indices) + self.data_objects = [self.data_objects[i] for i in indices] diff --git a/MindChemistry/applications/orb/src/atomic_system.py b/MindChemistry/applications/orb/src/atomic_system.py new file mode 100644 index 000000000..b9895e9bd --- /dev/null +++ b/MindChemistry/applications/orb/src/atomic_system.py @@ -0,0 +1,222 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""atomic system""" + +from dataclasses import dataclass +from typing import List, Optional + +import ase +from ase import constraints +from ase.calculators.singlepoint import SinglePointCalculator + +import mindspore as ms +from mindspore import Tensor, mint + +from src import featurization_utilities +from src.base import AtomGraphs + + +@dataclass +class SystemConfig: + """Config controlling how to featurize a system of atoms. + + Args: + radius: radius for edge construction + max_num_neighbors: maximum number of neighbours each node can send messages to. + use_timestep_0: (unused - purely for compatibility with internal models) + """ + + radius: float + max_num_neighbors: int + use_timestep_0: bool = True + + +def atom_graphs_to_ase_atoms( + graphs: AtomGraphs, + energy: Optional[Tensor] = None, + forces: Optional[Tensor] = None, + stress: Optional[Tensor] = None, +) -> List[ase.Atoms]: + """Converts a list of graphs to a list of ase.Atoms.""" + if "atomic_numbers_embedding" in graphs.node_features: + atomic_numbers = mint.argmax( + graphs.node_features["atomic_numbers_embedding"], dim=-1 + ) + else: + atomic_numbers = graphs.node_features["atomic_numbers"] + atomic_numbers_split = mint.split(atomic_numbers, graphs.n_node.tolist()) + positions_split = mint.split(graphs.positions, graphs.n_node.tolist()) + assert graphs.tags is not None and graphs.system_features is not None + tags = mint.split(graphs.tags, graphs.n_node.tolist()) + + calculations = {} + if energy is not None: + energy_list = mint.unbind(energy.detach()) + assert len(energy_list) == len(atomic_numbers_split) + calculations["energy"] = energy_list + if forces is not None: + forces_list = mint.split(forces.detach(), graphs.n_node.tolist()) + assert len(forces_list) == len(atomic_numbers_split) + calculations["forces"] = forces_list + if stress is not None: + stress_list = mint.unbind(stress.detach()) + assert len(stress_list) == len(atomic_numbers_split) + calculations["stress"] = stress_list + + atoms_list = [] + for index, (n, p, c, t) in enumerate( + zip(atomic_numbers_split, positions_split, graphs.cell, tags) + ): + atoms = ase.Atoms( + numbers=n.detach(), + positions=p.detach(), + cell=c.detach(), + tags=t.detach(), + pbc=mint.any(c != 0), + ) + if calculations != {}: + spc = SinglePointCalculator( + atoms=atoms, + **{ + key: ( + val[index].item() + if val[index].nelement() == 1 + else val[index].numpy() + ) + for key, val in calculations.items() + }, + ) + atoms.calc = spc + atoms_list.append(atoms) + + return atoms_list + + +def ase_atoms_to_atom_graphs( + atoms: ase.Atoms, + system_config: SystemConfig = SystemConfig( + radius=10.0, max_num_neighbors=20, use_timestep_0=True + ), + system_id: Optional[int] = None, + brute_force_knn: Optional[bool] = None, +) -> AtomGraphs: + """Generate AtomGraphs from an ase.Atoms object. + + Args: + atoms: ase.Atoms object + system_config: SystemConfig object + system_id: Optional system_id + brute_force_knn: whether to use a 'brute force' knn approach with torch.cdist for kdtree construction. + Defaults to None, in which case brute_force is used if we a GPU is available (2-6x faster), + but not on CPU (1.5x faster - 4x slower). For very large systems, brute_force may OOM on GPU, + so it is recommended to set to False in that case. + device: device to put the tensors on. + + Returns: + AtomGraphs object + """ + atomic_numbers = ms.from_numpy(atoms.numbers).long() + atom_type_embedding = mint.nn.functional.one_hot( + atomic_numbers, num_classes=118 + ).type(ms.float32) + + node_feats = { + "atomic_numbers": atomic_numbers.to(ms.int64), + "atomic_numbers_embedding": atom_type_embedding.to(ms.float32), + "positions": ms.from_numpy(atoms.positions).to(ms.float32), + } + system_feats = {"cell": Tensor(atoms.cell.array[None, ...]).to(ms.float32)} + edge_feats, senders, receivers = _get_edge_feats( + node_feats["positions"], + system_feats["cell"][0], + system_config.radius, + system_config.max_num_neighbors, + brute_force=brute_force_knn, + ) + + num_atoms = len(node_feats["positions"]) + atom_graph = AtomGraphs( + senders=senders, + receivers=receivers, + n_node=Tensor([num_atoms]), + n_edge=Tensor([len(senders)]), + node_features=node_feats, + edge_features=edge_feats, + system_features=system_feats, + system_id=Tensor([system_id]) if system_id is not None else system_id, + fix_atoms=ase_fix_atoms_to_tensor(atoms), + tags=_get_ase_tags(atoms), + radius=system_config.radius, + max_num_neighbors=system_config.max_num_neighbors, + ) + return atom_graph + + +def _get_edge_feats( + positions: Tensor, + cell: Tensor, + radius: float, + max_num_neighbours: int, + brute_force: Optional[bool] = None, +): + """Get edge features. + + Args: + positions: (n_nodes, 3) positions tensor + cell: 3x3 tensor unit cell for a system + radius: radius for edge construction + max_num_neighbours: maximum number of neighbours each node can send messages to. + n_kdtree_workers: number of workers to use for kdtree construction. + brute_force: whether to use brute force for kdtree construction. + """ + # Construct a graph from a 3x3 supercell (as opposed to an infinite supercell). + ( + edge_index, + edge_vectors, + ) = featurization_utilities.compute_pbc_radius_graph( + positions=positions, + periodic_boundaries=cell, + radius=radius, + max_number_neighbors=max_num_neighbours, + brute_force=brute_force, + ) + edge_feats = { + "vectors": edge_vectors.to(ms.float32), + "r": edge_vectors.norm(dim=-1), + } + senders, receivers = edge_index[0], edge_index[1] + return edge_feats, senders, receivers + + +def _get_ase_tags(atoms: ase.Atoms) -> Tensor: + """Get tags from ase.Atoms object.""" + tags = atoms.get_tags() + if tags is not None: + tags = Tensor(tags) + else: + tags = mint.zeros(len(atoms)) + return tags + + +def ase_fix_atoms_to_tensor(atoms: ase.Atoms) -> Optional[Tensor]: + """Get fixed atoms from ase.Atoms object.""" + fixed_atoms = None + if atoms.constraints is not None and atoms.constraints: + constraint = atoms.constraints[0] + if isinstance(constraint, constraints.FixAtoms): + fixed_atoms = mint.zeros((len(atoms)), dtype=ms.bool_) + fixed_atoms[constraint.index] = True + return fixed_atoms diff --git a/MindChemistry/applications/orb/src/base.py b/MindChemistry/applications/orb/src/base.py new file mode 100644 index 000000000..046e5950f --- /dev/null +++ b/MindChemistry/applications/orb/src/base.py @@ -0,0 +1,486 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Base Model class.""" + +from collections import defaultdict +from copy import deepcopy +from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Sequence, Union +import tree + +import mindspore as ms +from mindspore import ops, Tensor, mint + +from src import featurization_utilities + +Metric = Union[Tensor, int, float] +TensorDict = Mapping[str, Optional[Tensor]] + + +class ModelOutput(NamedTuple): + """A model's output.""" + + loss: Tensor + log: Mapping[str, Metric] + + +class AtomGraphs(NamedTuple): + """A class representing the input to a model for a graph. + + Args: + senders (torch.Tensor): The integer source nodes for each edge. + receivers (torch.Tensor): The integer destination nodes for each edge. + n_node (torch.Tensor): A (batch_size, ) shaped tensor containing the number of nodes per graph. + n_edge (torch.Tensor): A (batch_size, ) shaped tensor containing the number of edges per graph. + node_features (Dict[str, torch.Tensor]): A dictionary containing node feature tensors. + It will always contain "atomic_numbers" and "positions" keys, representing the + atomic numbers of each node, and the 3d cartesian positions of them respectively. + edge_features (Dict[str, torch.Tensor]): A dictionary containing edge feature tensors. + system_features (Optional[TensorDict]): An optional dictionary containing system-level features. + node_targets (Optional[Dict[torch.Tensor]]): An optional dict of tensors containing targets + for individual nodes. This tensor is commonly expected to have shape (num_nodes, *). + edge_target (Optional[torch.Tensor]): An optional tensor containing targets for individual edges. + This tensor is commonly expected to have (num_edges, *). + system_targets (Optional[Dict[torch.Tensor]]): An optional dict of tensors containing targets for the + entire system. system_id (Optional[torch.Tensor]): An optional tensor containing the ID of the system. + fix_atoms (Optional[torch.Tensor]): An optional tensor containing information on fixed atoms in the system. + """ + + senders: Tensor + receivers: Tensor + n_node: Tensor + n_edge: Tensor + node_features: Dict[str, Tensor] + edge_features: Dict[str, Tensor] + system_features: Dict[str, Tensor] + node_targets: Optional[Dict[str, Tensor]] = None + edge_targets: Optional[Dict[str, Tensor]] = None + system_targets: Optional[Dict[str, Tensor]] = None + system_id: Optional[Tensor] = None + fix_atoms: Optional[Tensor] = None + tags: Optional[Tensor] = None + radius: Optional[float] = None + max_num_neighbors: Optional[int] = None + + @property + def positions(self): + """Get positions of atoms.""" + return self.node_features["positions"] + + @positions.setter + def positions(self, val: Tensor): + self.node_features["positions"] = val + + @property + def atomic_numbers(self): + """Get integer atomic numbers.""" + return self.node_features["atomic_numbers"] + + @atomic_numbers.setter + def atomic_numbers(self, val: Tensor): + self.node_features["atomic_numbers"] = val + + @property + def cell(self): + """Get unit cells.""" + assert self.system_features + return self.system_features.get("cell") + + @cell.setter + def cell(self, val: Tensor): + assert self.system_features + self.system_features["cell"] = val + + def clone(self) -> "AtomGraphs": + """Clone the AtomGraphs object. + + Note: this differs from deepcopy() because it preserves gradients. + """ + + def _clone(x): + if isinstance(x, Tensor): + return x.clone() + return x + + return tree.map_structure(_clone, self) + + def to(self, device: Any = None) -> "AtomGraphs": + """Move AtomGraphs child tensors to a device.""" + + print(f"Moving AtomGraphs to device: {device}") + def _to(x): + if hasattr(x, "to"): + return x + return x + + return tree.map_structure(_to, self) + + def tachdetach(self) -> "AtomGraphs": + """Detach all child tensors.""" + + def _detach(x): + if hasattr(x, "detach"): + return x.detach() + return x + + return tree.map_structure(_detach, self) + + def equals(self, graphs: "AtomGraphs") -> bool: + """Check two atomgraphs are equal.""" + + def _is_equal(x, y): + if isinstance(x, Tensor): + return mint.equal(x, y) + return x == y + + flat_results = tree.flatten(tree.map_structure(_is_equal, self, graphs)) + return all(flat_results) + + def allclose(self, graphs: "AtomGraphs", rtol=1e-5, atol=1e-8) -> bool: + """Check all tensors/scalars of two atomgraphs are close.""" + + def _is_close(x, y): + if isinstance(x, Tensor): + return mint.allclose(x, y, rtol=rtol, atol=atol) + if isinstance(x, (float, int)): + return mint.allclose( + Tensor(x), Tensor(y), rtol=rtol, atol=atol + ) + return x == y + + flat_results = tree.flatten(tree.map_structure(_is_close, self, graphs)) + return all(flat_results) + + def to_dict(self): + """Return a dictionary mapping each AtomGraph property to a corresponding tensor/scalar. + + Any nested attributes of the AtomGraphs are unpacked so the + returned dict has keys like "positions" and "atomic_numbers". + + Any None attributes are not included in the dictionary. + + Returns: + dict: A dictionary mapping attribute_name -> tensor/scalar + """ + ret = {} + for key, val in self._asdict().items(): + if val is None: + continue + if isinstance(val, dict): + for k, v in val.items(): + ret[k] = v + else: + ret[key] = val + + return ret + + def to_batch_dict(self) -> Dict[str, Any]: + """Return a single dictionary mapping each AtomGraph property to a corresponding list of tensors/scalars. + + Returns: + dict: A dict mapping attribute_name -> list of length batch_size containing tensors/scalars. + """ + batch_dict = defaultdict(list) + for graph in self.split(self): + for key, value in graph.to_dict().items(): + batch_dict[key].append(value) + return batch_dict + + def split(self, clone=True) -> List["AtomGraphs"]: + """Splits batched AtomGraphs into constituent system AtomGraphs. + + Args: + graphs (AtomGraphs): A batched AtomGraphs object. + clone (bool): Whether to clone the graphs before splitting. + Cloning removes risk of side-effects, but uses more memory. + """ + graphs = self.clone() if clone else self + + batch_nodes = graphs.n_node.tolist() + batch_edges = graphs.n_edge.tolist() + + if not batch_nodes: + raise ValueError("Cannot split empty batch") + if len(batch_nodes) == 1: + return [graphs] + + batch_systems = mint.ones(len(batch_nodes), dtype=ms.int32).tolist() + node_features = _split_features(graphs.node_features, batch_nodes) + node_targets = _split_features(graphs.node_targets, batch_nodes) + edge_features = _split_features(graphs.edge_features, batch_edges) + edge_targets = _split_features(graphs.edge_targets, batch_edges) + system_features = _split_features(graphs.system_features, batch_systems) + system_targets = _split_features(graphs.system_targets, batch_systems) + system_ids = _split_tensors(graphs.system_id, batch_systems) + fix_atoms = _split_tensors(graphs.fix_atoms, batch_nodes) + tags = _split_tensors(graphs.tags, batch_nodes) + batch_nodes = [Tensor([n]) for n in batch_nodes] + batch_edges = [Tensor([e]) for e in batch_edges] + + # calculate the new senders and receivers + senders = list(_split_tensors(graphs.senders, batch_edges)) + receivers = list(_split_tensors(graphs.receivers, batch_edges)) + n_graphs = graphs.n_node.shape[0] + offsets = mint.cumsum(graphs.n_node[:-1], 0) + offsets = mint.cat([Tensor([0]), offsets]) + unbatched_senders = [] + unbatched_recievers = [] + for graph_index in range(n_graphs): + s = senders[graph_index] - offsets[graph_index] + r = receivers[graph_index] - offsets[graph_index] + unbatched_senders.append(s) + unbatched_recievers.append(r) + + return [ + AtomGraphs(*args) + for args in zip( + unbatched_senders, + unbatched_recievers, + batch_nodes, + batch_edges, + node_features, + edge_features, + system_features, + node_targets, + edge_targets, + system_targets, + system_ids, + fix_atoms, + tags, + [graphs.radius for _ in range(len(batch_nodes))], + [graphs.max_num_neighbors for _ in range(len(batch_nodes))], + ) + ] + + +def batch_graphs(graphs: List[AtomGraphs]) -> AtomGraphs: + """Batch graphs together by concatenating their nodes, edges, and features. + + Args: + graphs (List[AtomGraphs]): A list of AtomGraphs to be batched together. + + Returns: + AtomGraphs: A new AtomGraphs object with the concatenated nodes, + edges, and features from the input graphs, along with concatenated target, + system ID, and other information. + """ + # Calculates offsets for sender and receiver arrays, caused by concatenating + # the nodes arrays. + offsets = mint.cumsum( + Tensor([0] + [mint.sum(g.n_node) for g in graphs[:-1]]), 0 + ) + radius = graphs[0].radius + assert {graph.radius for graph in graphs} == {radius} + max_num_neighbours = graphs[0].max_num_neighbors + assert {graph.max_num_neighbors for graph in graphs} == {max_num_neighbours} + + return AtomGraphs( + n_node=mint.concat([g.n_node for g in graphs], dim=0).to(ms.int64), + n_edge=mint.concat([g.n_edge for g in graphs], dim=0).to(ms.int64), + senders=mint.concat( + [g.senders + o for g, o in zip(graphs, offsets)], dim=0 + ).to(ms.int64), + receivers=mint.concat( + [g.receivers + o for g, o in zip(graphs, offsets)], dim=0 + ).to(ms.int64), + node_features=_map_concat([g.node_features for g in graphs]), + edge_features=_map_concat([g.edge_features for g in graphs]), + system_features=_map_concat([g.system_features for g in graphs]), + node_targets=_map_concat([g.node_targets for g in graphs]), + edge_targets=_map_concat([g.edge_targets for g in graphs]), + system_targets=_map_concat([g.system_targets for g in graphs]), + system_id=_concat([g.system_id for g in graphs]), + fix_atoms=_concat([g.fix_atoms for g in graphs]), + tags=_concat([g.tags for g in graphs]), + radius=radius, + max_num_neighbors=max_num_neighbours, + ) + + +def refeaturize_atomgraphs( + atoms: AtomGraphs, + positions: Tensor, + atomic_number_embeddings: Optional[Tensor] = None, + cell: Optional[Tensor] = None, + recompute_neighbors=True, + updates: Optional[Tensor] = None, + fixed_atom_pos: Optional[Tensor] = None, + fixed_atom_type_embedding: Optional[Tensor] = None, + differentiable: bool = False, +) -> AtomGraphs: + """Return a graph updated according to the new positions, and (if given) atomic numbers and unit cells. + + Note: if a unit cell is given, it will *both* be used to do the + pbc-remapping and be set on the returned AtomGraphs + + Args: + atoms (AtomGraphs): The original AtomGraphs object. + positions (torch.Tensor): The new positions of the atoms. + atomic_number_embeddings (Optional[torch.Tensor]): The new atomic number embeddings. + cell (Optional[torch.Tensor]): The new unit cell. + recompute_neighbors (bool): Whether to recompute the neighbor list. + updates (Optional[torch.Tensor]): The updates to the positions. + fixed_atom_pos (Optional[torch.Tensor]): The positions of atoms + which are fixed when diffusing on a fixed trajectory. + fixed_atom_type_embedding (Optional[torch.Tensor]): If using atom type diffusion + with a fixed trajectory, the unormalized vectors of the fixed atoms. Shape (n_atoms, 118). + differentiable (bool): Whether to make the graph inputs require_grad. This includes + the positions and atomic number embeddings, if passed. + exact_pbc_image_neighborhood: bool: If the exact pbc image neighborhood calculation (from torch nl) + which considers boundary crossing for more than cell is used. + + Returns: + AtomGraphs: A refeaturized AtomGraphs object. + """ + if cell is None: + cell = atoms.cell + + if atoms.fix_atoms is not None and fixed_atom_pos is not None: + positions[atoms.fix_atoms] = fixed_atom_pos[atoms.fix_atoms] + + if ( + atoms.fix_atoms is not None + and fixed_atom_type_embedding is not None + and atomic_number_embeddings is not None + ): + atomic_number_embeddings[atoms.fix_atoms] = fixed_atom_type_embedding[ + atoms.fix_atoms + ] + + num_atoms = atoms.n_node + positions = featurization_utilities.batch_map_to_pbc_cell( + positions, cell, num_atoms + ) + + if differentiable: + positions.requires_grad = True + if atomic_number_embeddings is not None: + atomic_number_embeddings.requires_grad = True + + if recompute_neighbors: + assert atoms.radius is not None and atoms.max_num_neighbors is not None + ( + edge_index, + edge_vectors, + batch_num_edges, + ) = featurization_utilities.batch_compute_pbc_radius_graph( + positions=positions, + periodic_boundaries=cell, + radius=atoms.radius, + image_idx=num_atoms, + max_number_neighbors=atoms.max_num_neighbors, + ) + new_senders = edge_index[0] + new_receivers = edge_index[1] + else: + assert updates is not None + new_senders = atoms.senders + new_receivers = atoms.receivers + edge_vectors = recompute_edge_vectors(atoms, updates) + batch_num_edges = atoms.n_edge + + edge_features = { + "vectors": edge_vectors.to(ms.float32), + } + + new_node_features = {} + if atoms.node_features is not None: + new_node_features = deepcopy(atoms.node_features) + new_node_features["positions"] = positions + if atomic_number_embeddings is not None: + new_node_features["atomic_numbers_embedding"] = atomic_number_embeddings + + new_system_features = {} + if atoms.system_features is not None: + new_system_features = deepcopy(atoms.system_features) + new_system_features["cell"] = cell + + new_atoms = AtomGraphs( + senders=new_senders, + receivers=new_receivers, + n_node=atoms.n_node, + n_edge=batch_num_edges, + node_features=new_node_features, + edge_features=edge_features, + system_features=new_system_features, + node_targets=atoms.node_targets, + system_targets=atoms.system_targets, + fix_atoms=atoms.fix_atoms, + tags=atoms.tags, + radius=atoms.radius, + max_num_neighbors=atoms.max_num_neighbors, + ) + + return new_atoms + + +def recompute_edge_vectors(atoms, updates): + """Recomputes edge vectors with per node updates.""" + updates = -updates + senders = atoms.senders + receivers = atoms.receivers + edge_translation = updates[senders] - updates[receivers] + return atoms.edge_features["vectors"] + edge_translation + + +def volume_atomgraphs(atoms: AtomGraphs): + """Returns the volume of the unit cell.""" + cell = atoms.cell + cross = ops.Cross(dim=1) + return (cell[:, 0] * cross(cell[:, 1], cell[:, 2])).sum(-1) + + +def _map_concat(nests): + concat = lambda *args: _concat(args) + return tree.map_structure(concat, *nests) + + +def _concat( + tensors: List[Optional[Tensor]], +) -> Optional[Tensor]: + """Splits tensors based on the intended split sizes.""" + if any([x is None for x in tensors]): + return None + return mint.concat(tensors, dim=0) + + +def _split_tensors( + features: Optional[Tensor], + split_sizes: List[int], +) -> Sequence[Optional[Tensor]]: + """Splits tensors based on the intended split sizes.""" + if features is None: + return [None] * len(split_sizes) + + return mint.split(features, split_sizes) + + +def _split_features( + features: Optional[TensorDict], + split_sizes: List[int], +) -> Sequence[Optional[TensorDict]]: + """Splits features based on the intended split sizes.""" + if features is None: + return [None] * len(split_sizes) + + split_dict = { + k: mint.split(v, split_sizes) if v is not None else [None] * len(split_sizes) + for k, v in features.items() + } + individual_tuples = zip(*[v for v in split_dict.values()]) + individual_dicts: List[Optional[TensorDict]] = list( + map(lambda k: dict(zip(split_dict.keys(), k)), individual_tuples) + ) + return individual_dicts diff --git a/MindChemistry/applications/orb/src/featurization_utilities.py b/MindChemistry/applications/orb/src/featurization_utilities.py new file mode 100644 index 000000000..dd68d4ad8 --- /dev/null +++ b/MindChemistry/applications/orb/src/featurization_utilities.py @@ -0,0 +1,438 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Featurization utilities for molecular models.""" + +from typing import Callable, Optional, Tuple, Union +from pynanoflann import KDTree as NanoKDTree +from scipy.spatial import KDTree as SciKDTree + +import numpy as np +import mindspore as ms +from mindspore import ops, Tensor, mint + +DistanceFeaturizer = Callable[[Tensor], Tensor] + + + +def gaussian_basis_function( + scalars: Tensor, + num_bases: Union[Tensor, int], + radius: Union[Tensor, float], + scale: Union[Tensor, float] = 1.0, +) -> Tensor: + """Gaussian basis function applied to a tensor of scalars. + + Args: + scalars (Tensor): Scalars to compute the gbf on. Shape [num_scalars]. + num_bases (Tensor): The number of bases. An Int. + radius (Tensor): The largest centre of the bases. A Float. + scale (Tensor, optional): The width of the gaussians. Defaults to 1. + + Returns: + Tensor: A tensor of shape [num_scalars, num_bases]. + """ + assert len(scalars.shape) == 1 + gaussian_means = ops.arange( + 0, float(radius), float(radius) / num_bases + ) + return mint.exp( + -(scale**2) * (scalars.unsqueeze(1) - gaussian_means.unsqueeze(0)).abs() ** 2 + ) + + +def featurize_edges( + edge_vectors: Tensor, distance_featurization: DistanceFeaturizer +) -> Tensor: + """Featurizes edge features, provides concatenated unit vector along with featurized distances. + + Args: + edge_vectors (tensor): Edge vectors to featurize. Shape [num_edge, 3] + distance_featurization (DistanceFeaturization): A function that featurizes the distances of the vectors. + + Returns: + tensor: Edge features, shape [num_edge, num_edge_features]. + """ + edge_features = [] + edge_norms = mint.linalg.norm(edge_vectors, dim=1) + featurized_edge_norms = distance_featurization(edge_norms) + unit_vectors = edge_vectors / edge_norms.unsqueeze(1) + unit_vectors = mint.nan_to_num(unit_vectors, nan=0, posinf=0, neginf=0) + edge_features.append(featurized_edge_norms) + edge_features.append(unit_vectors) + return mint.cat(edge_features, dim=-1).to(ms.float32) + + +def compute_edge_vectors( + edge_index: Tensor, positions: Tensor +) -> Tensor: + """Computes edge vectors from positions. + + Args: + edge_index (tensor): The edge index. First position the senders, second + position the receivers. Shape [2, num_edge]. + positions (tensor): Positions of each node. Shape [num_nodes, 3] + + Returns: + tensor: The vectors of each edge. + """ + senders = edge_index[0] + receivers = edge_index[1] + return positions[receivers] - positions[senders] + + +# These are offsets applied to coordinates to create a 3x3x3 +# tiled periodic image of the input structure. +OFFSETS = np.array( + [ + [-1.0, 1.0, -1.0], + [0.0, 1.0, -1.0], + [1.0, 1.0, -1.0], + [-1.0, 0.0, -1.0], + [0.0, 0.0, -1.0], + [1.0, 0.0, -1.0], + [-1.0, -1.0, -1.0], + [0.0, -1.0, -1.0], + [1.0, -1.0, -1.0], + [-1.0, 1.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [-1.0, -1.0, 0.0], + [0.0, -1.0, 0.0], + [1.0, -1.0, 0.0], + [-1.0, 1.0, 1.0], + [0.0, 1.0, 1.0], + [1.0, 1.0, 1.0], + [-1.0, 0.0, 1.0], + [0.0, 0.0, 1.0], + [1.0, 0.0, 1.0], + [-1.0, -1.0, 1.0], + [0.0, -1.0, 1.0], + [1.0, -1.0, 1.0], + ] +) + +NUM_OFFSETS = len(OFFSETS) + + +def _compute_img_positions_torch( + positions: Tensor, periodic_boundaries: Tensor +) -> Tensor: + """Computes the positions of the periodic images of the input structure. + + Consider the following 2D periodic boundary image. + + --- + --- + --- + + | | | | + + --- + --- + --- + + | | x | | + + --- + --- + --- + + | | | | + + --- + --- + --- + + + Each tile in this has an associated translation to translate + 'x'. For example, the top left would by (-1, +1). These are + the 'OFFSETS', but OFFSETS are for a 3x3x3 grid. + + This is complicated by the fact that our periodic + boundaries are not orthogonal to each other, and so we form a new + translation by taking a linear combination of the unit cell axes. + + Args: + positions (Tensor): Positions of the atoms. Shape [num_atoms, 3]. + periodic_boundaries (Tensor): Periodic boundaries of the unit cell. + This can be 2 shapes - [3, 3] or [num_atoms, 3, 3]. If the shape is + [num_atoms, 3, 3], it is assumed that the PBC has been repeat_interleaved + for each atom, i.e this function is agnostic as to whether it is computing + with respect to a batch or not. + Returns: + Tensor: The positions of the periodic images. Shape [num_atoms, 27, 3]. + """ + num_positions = len(positions) + + has_unbatched_pbc = periodic_boundaries.shape == (3, 3) + if has_unbatched_pbc: + periodic_boundaries = periodic_boundaries.unsqueeze(0) + periodic_boundaries = periodic_boundaries.expand((num_positions, 3, 3)) + + assert periodic_boundaries.shape[0] == positions.shape[0] + offsets = Tensor(OFFSETS, dtype=positions.dtype) + offsets = mint.unsqueeze(offsets, 0) + repeated_offsets = offsets.expand((num_positions, NUM_OFFSETS, 3)) + repeated_offsets = mint.unsqueeze(repeated_offsets, 3) + periodic_boundaries = mint.unsqueeze(periodic_boundaries, 1) + translations = repeated_offsets * periodic_boundaries + translations = translations.sum(2) + + # Expand the positions so we can broadcast add the translations per PBC image. + expanded_positions = positions.unsqueeze(1) + translated_positions = expanded_positions + translations + return translated_positions + + +def brute_force_knn( + img_positions: Tensor, positions: Tensor, k: int +) -> Tuple[Tensor, Tensor]: + """Brute force k-nearest neighbors. + + Args: + img_positions (Tensor): The positions of the images. Shape [num_atoms * 27, 3]. + positions (Tensor): The positions of the query atoms. Shape [num_atoms, 3]. + k (int): The number of nearest neighbors to find. + + Returns: + return_types.topk: The indices of the nearest neighbors. Shape [num_atoms, k]. + """ + dist = mint.cdist(positions, img_positions) + return mint.topk(dist, k, largest=False, sorted=True) + + +def compute_pbc_radius_graph( + *, + positions: Tensor, + periodic_boundaries: Tensor, + radius: Union[float, Tensor], + max_number_neighbors: int = 20, + brute_force: Optional[bool] = None, + library: str = "pynanoflann", + n_workers: int = 1, +) -> Tuple[Tensor, Tensor]: + """Computes periodic condition radius graph from positions. + + Args: + positions (Tensor): 3D positions of particles. Shape [num_particles, 3]. + periodic_boundaries (Tensor): A 3x3 matrix where the periodic boundary axes are rows or columns. + radius (Union[float, tensor]): The radius within which to connect atoms. + max_number_neighbors (int, optional): The maximum number of neighbors for each particle. Defaults to 20. + brute_force (bool, optional): Whether to use brute force knn. Defaults to None, in which case brute_force + is used if GPU is available (2-6x faster), but not on CPU (1.5x faster - 4x slower, depending on + system size). + library (str, optional): The KDTree library to use. Currently, either 'scipy' or 'pynanoflann'. + n_workers (int, optional): The number of workers to use for KDTree construction. Defaults to 1. + + Returns: + Tuple[Tensor, Tensor]: A 2-Tuple. First, an edge_index tensor, where the first index are the + sender indices and the second are the receiver indices. Second, the vector displacements between edges. + """ + if brute_force is None: + brute_force = ms.get_context("device_target") == "GPU" + + if mint.any(periodic_boundaries != 0.0): + supercell_positions = _compute_img_positions_torch( + positions=positions, periodic_boundaries=periodic_boundaries + ) + # CRITICALLY IMPORTANT: We need to reshape the supercell_positions to be + # flat, so we can use them for the nearest neighbors. The *way* in which + # they are flattened is important, because we need to be able to map the + # indices returned from the nearest neighbors to the original positions. + # The easiest way to do this is to transpose, so that when we flatten, we + # have: + # [ + # img_0_atom_0, + # img_0_atom_1, + # ..., + # img_0_atom_N, + # img_1_atom_0, + # img_1_atom_1, + # ..., + # img_N_atom_N, + # etc + # ] + # This way, we can take the mod of the indices returned from the nearest + # neighbors to get the original indices. + # Shape (27, num_positions, 3) + supercell_positions = supercell_positions.transpose(0, 1) + supercell_positions = supercell_positions.reshape(-1, 3) + else: + supercell_positions = positions + + num_positions = positions.shape[0] + + if brute_force: + # Brute force + distance_values, nearest_img_neighbors = brute_force_knn( + supercell_positions, + positions, + min(max_number_neighbors + 1, len(supercell_positions)), + ) + + # remove distances greater than radius, and exclude self + within_radius = distance_values[:, 1:] < (radius + 1e-6) + + num_neighbors_per_position = within_radius.sum(-1) + # remove the self node which will be closest + index_array = nearest_img_neighbors[:, 1:] + + senders = mint.repeat_interleave( + mint.arange(num_positions), num_neighbors_per_position + ) + receivers_imgs = index_array[within_radius] + + receivers = receivers_imgs % num_positions + vectors = supercell_positions[receivers_imgs] - positions[senders] + stacked = mint.stack((senders, receivers), dim=0) + return stacked, vectors + + # Build a KDTree from the supercell positions. + # Query that KDTree just for the positions in the central cell. + tree_data = supercell_positions.clone().numpy() + tree_query = positions.clone().numpy() + distance_upper_bound = np.array(radius) + 1e-8 + if library == "scipy": + tree = SciKDTree(tree_data, leafsize=100) + _, nearest_img_neighbors = tree.query( + tree_query, + max_number_neighbors + 1, + distance_upper_bound=distance_upper_bound, + workers=n_workers, + p=2, + ) + # Remove the self-edge that will be closest + index_array = np.array(nearest_img_neighbors)[:, 1:] + # Remove any entry that equals len(supercell_positions), which are negative hits + receivers_imgs = index_array[index_array != len(supercell_positions)] + num_neighbors_per_position = (index_array != len(supercell_positions)).sum( + -1 + ) + elif library == "pynanoflann": + tree = NanoKDTree( + n_neighbors=min(max_number_neighbors + 1, len(supercell_positions)), + radius=radius, + leaf_size=100, + metric="l2", + ) + tree.fit(tree_data) + distance_values, nearest_img_neighbors = tree.kneighbors( + tree_query, n_jobs=n_workers + ) + nearest_img_neighbors = nearest_img_neighbors.astype(np.int32) + + # remove the self node which will be closest + index_array = nearest_img_neighbors[:, 1:] + # remove distances greater than radius + within_radius = distance_values[:, 1:] < (radius + 1e-6) + receivers_imgs = index_array[within_radius] + num_neighbors_per_position = within_radius.sum(-1) + + # We construct our senders and receiver indexes. + senders = np.repeat(np.arange(num_positions), list(num_neighbors_per_position)) + receivers_img_torch = Tensor(receivers_imgs, ms.int32) + # Map back to indexes on the central image. + receivers = receivers_img_torch % num_positions + senders_torch = Tensor(senders, ms.int32) + + # Finally compute the vector displacements between senders and receivers. + vectors = supercell_positions[receivers_img_torch] - positions[senders_torch] + return mint.stack((senders_torch, receivers), dim=0), vectors + + +def batch_map_to_pbc_cell( + positions: Tensor, + periodic_boundary_conditions: Tensor, + num_atoms: Tensor, +) -> Tensor: + """Maps positions to within a periodic boundary cell, for a batched system. + + Args: + positions (Tensor): The positions to be mapped. Shape [num_particles, 3] + periodic_boundary_conditions (Tensor): The periodic boundary conditions. Shape [num_batches, 3, 3] + num_atoms (LongTensor): The number of atoms in each batch. Shape [num_batches] + """ + dtype = positions.dtype + positions = positions.double() + periodic_boundary_conditions = periodic_boundary_conditions.double() + + pbc_nodes = mint.repeat_interleave(periodic_boundary_conditions, num_atoms, dim=0) + + # To use the stable linalg.solve, we need to mask batch elements which don't + # have periodic boundaries. We do this by adding the identity matrix as their PBC, + # because we need the PBCs to be non-singular. + null_pbc = pbc_nodes.abs().sum(dim=[1, 2]) == 0 + identity = mint.eye(3, dtype=ms.bool_) + # Broadcast the identity to the elements of the batch that have a null pbc. + null_pbc_identity_mask = null_pbc.view(-1, 1, 1) & identity.view(1, 3, 3) + pbc_nodes_masked = pbc_nodes + null_pbc_identity_mask.double() + + lattice_coords = ops.matrix_solve(pbc_nodes_masked.transpose(1, 2), positions) + frac_coords = lattice_coords % 1.0 + + cartesian = mint.einsum("bi,bij->bj", frac_coords, pbc_nodes) + return mint.where(null_pbc.unsqueeze(1), positions, cartesian).to(dtype) + + +def batch_compute_pbc_radius_graph( + *, + positions: Tensor, + periodic_boundaries: Tensor, + radius: Union[float, Tensor], + image_idx: Tensor, + max_number_neighbors: int = 20, + brute_force: Optional[bool] = None, + library: str = "scipy", +): + """Computes batched periodic boundary condition radius graph from positions. + + This function is optimised for computation on CPU, and work work on device. GPU implementations + are likely to be significantly slower because of the irregularly sized tensor computations and the + lack of extremely fast GPU knn routines. + + Args: + positions (Tensor): 3D positions of a batch of particles. Shape [num_particles, 3]. + periodic_boundaries (Tensor): A batch where each element 3x3 matrix where the periodic boundary axes + are rows or columns. + radius (Union[float, tensor]): The radius within which to connect atoms. + image_idx (Tensor): A vector where each element indicates the number of particles in each element of + the batch. Of size len(batch). + max_number_neighbors (int, optional): The maximum number of neighbors for each particle. Defaults to 20. + brute_force (bool, optional): Whether to use brute force knn. Defaults to None, in which case brute_force + is used if we are on GPU (2-6x faster), but not on CPU (1.5x faster - 4x slower). + library (str, optional): The KDTree library to use. Currently, either 'scipy' or 'pynanoflann'. + + Returns: + Tuple[Tensor, Tensor]: A 2-Tuple. First, an edge_index tensor, where the first index are the + sender indices and the second are the receiver indices. Second, the vector displacements between edges. + """ + idx = 0 + all_edges = [] + all_vectors = [] + num_edges = [] + + for p, pbc in zip( + ops.tensor_split(positions, mint.cumsum(image_idx, 0)[:-1]), + periodic_boundaries, + ): + edges, vectors = compute_pbc_radius_graph( + positions=p, + periodic_boundaries=pbc, + radius=radius, + max_number_neighbors=max_number_neighbors, + brute_force=brute_force, + library=library, + ) + if idx == 0: + offset = 0 + else: + offset += image_idx[idx - 1] + all_edges.append(edges + offset) + all_vectors.append(vectors) + num_edges.append(len(edges[0])) + idx += 1 + + all_edges = ms.numpy.concatenate(all_edges, 1) + all_vectors = ms.numpy.concatenate(all_vectors, 0) + num_edges = Tensor(num_edges, dtype=ms.int64) + return all_edges, all_vectors, num_edges diff --git a/MindChemistry/applications/orb/src/pretrained.py b/MindChemistry/applications/orb/src/pretrained.py new file mode 100644 index 000000000..cb66db19f --- /dev/null +++ b/MindChemistry/applications/orb/src/pretrained.py @@ -0,0 +1,116 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""pretrained.""" + +import os +from typing import Optional + +from mindspore import nn, load_checkpoint, load_param_into_net + +from mindchemistry.cell import ( + EnergyHead, + GraphHead, + Orb, + NodeHead, + MoleculeGNS, +) + + +def get_gns( + latent_dim: int = 256, + mlp_hidden_dim: int = 512, + num_message_passing_steps: int = 15, + num_edge_in_features: int = 23, + distance_cutoff: bool = True, + attention_gate: str = "sigmoid", +) -> MoleculeGNS: + """Define the base pretrained model architecture.""" + return MoleculeGNS( + num_node_in_features=256, + num_node_out_features=3, + num_edge_in_features=num_edge_in_features, + latent_dim=latent_dim, + interactions="simple_attention", + interaction_params={ + "distance_cutoff": distance_cutoff, + "polynomial_order": 4, + "cutoff_rmax": 6, + "attention_gate": attention_gate, + }, + num_message_passing_steps=num_message_passing_steps, + num_mlp_layers=2, + mlp_hidden_dim=mlp_hidden_dim, + use_embedding=True, + node_feature_names=["feat"], + edge_feature_names=["feat"], + ) + + +def load_model_for_inference(model: nn.Cell, weights_path: str) -> nn.Cell: + """ + Load a pretrained model in inference mode, using GPU if available. + """ + if not os.path.exists(weights_path): + raise FileNotFoundError(f"Checkpoint file {weights_path} not found.") + param_dict = load_checkpoint(weights_path) + load_param_into_net(model, param_dict) + model.set_train(False) + + return model + +def orb_v2( + weights_path: Optional[str] = None, +): + """Load ORB v2.""" + gns = get_gns() + + model = Orb( + graph_head=EnergyHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=1, + node_aggregation="mean", + reference_energy_name="vasp-shifted", + train_reference=True, + predict_atom_avg=True, + ), + node_head=NodeHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=3, + remove_mean=True, + ), + stress_head=GraphHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=6, + compute_stress=True, + ), + model=gns, + ) + model = load_model_for_inference(model, weights_path) + return model + + +def orb_mptraj_only_v2( + weights_path: Optional[str] = None, +): + """Load ORB MPTraj Only v2.""" + + return orb_v2(weights_path,) diff --git a/MindChemistry/applications/orb/src/property_definitions.py b/MindChemistry/applications/orb/src/property_definitions.py new file mode 100644 index 000000000..3951c06c0 --- /dev/null +++ b/MindChemistry/applications/orb/src/property_definitions.py @@ -0,0 +1,239 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Classes that define prediction targets.""" + +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union + +import ase.data +import ase.db +import ase.db.row +import ase.db.sqlite +import numpy as np + +import mindspore as ms +from mindspore import ops, Tensor, mint + +HARTREE_TO_EV = 27.211386245988 + + +def recursive_getattr(obj: object, attr: str) -> Any: + """Recursively access an object property using dot notation.""" + for sub_attr in attr.split("."): + obj = getattr(obj, sub_attr) + + return obj + + +def get_property_from_row( + name: Union[str, List[str]], + row: ase.db.row.AtomsRow, + conversion_factor: float = 1.0, +) -> Tensor: + """Retrieve arbitrary values from ase db data dict.""" + if isinstance(name, str): + names = [name] + else: + names = name + values = [] + for name_ in names: + attribute = recursive_getattr(row, name_) + target = np.array(attribute) + values.append(target) + + property_tensor = ms.from_numpy(np.hstack(values)).to(ms.float32) + + while len(property_tensor.shape) < 2: + property_tensor = property_tensor[None, ...] + + if "stress" in name and property_tensor.shape == (3, 3): + # convert stress tensor to voigt notation + property_tensor = Tensor( + [ + property_tensor[0, 0], + property_tensor[1, 1], + property_tensor[2, 2], + property_tensor[1, 2], + property_tensor[0, 2], + property_tensor[0, 1], + ], + dtype=ms.float32, + ).unsqueeze(0) + return property_tensor * conversion_factor + + +@dataclass +class PropertyDefinition: + """Defines how to extract and transform a quantative property from an ase db. + + Such properties have two primary use-cases: + - as features for the model to use / condition on. + - as target variables for regression tasks. + + Args: + name: The name of the property. + dim: The dimensionality of the property variable. + domain: Whether the variable is real, binary or categorical. If using + this variable as a regression target, then var_type determines + the loss function used e.g. MSE, BCE or cross-entropy loss. + row_to_property_fn: A function defining how a target can be + retrieved from an ase database row. + means: The mean to transform this by in the model. + stds: The std to scale this by in the model. + """ + + name: str + dim: int + domain: Literal["real", "binary", "categorical"] + row_to_property_fn: Optional[Callable] = None + means: Optional[Tensor] = None + stds: Optional[Tensor] = None + + +def energy_row_fn(row: ase.db.row.AtomsRow, dataset: str) -> float: + """Energy data in eV. + + - Some datasets use sums of energy values e.g. PBE + D3. + - For external datasets, we should explicitly register how + to extract the energy property by adding it to `extract_info'. + - Unregistered datasets default to using the `energy` attribute + and a conversion factor of 1, which is always correct for our + internally generated datasets. + """ + extract_info: Dict[str, List[Tuple]] = { + "mp-traj": [("energy", 1)], + "mp-traj-d3": [("energy", 1), ("data.d3.energy", 1)], + "alexandria-d3": [("energy", 1), ("data.d3.energy", 1)], + } + if dataset not in extract_info: + if not hasattr(row, "energy"): + raise ValueError( + f"db row {row.id} doesn't have an energy attribute directly " + ", but also doesn't define a method to extract energy info." + ) + return get_property_from_row("energy", row, 1) + + energy_ = 0.0 + for row_attribute, conversion_factor in extract_info[dataset]: + energy_ += get_property_from_row(row_attribute, row, conversion_factor) + return energy_ + + +def forces_row_fn(row: ase.db.row.AtomsRow, dataset: str): + """Force data in eV / Angstrom. + + - Some datasets use sums of energy values e.g. PBE + D3. + - For external datasets, we should explicitly register how + to extract the energy property by adding it to `extract_info'. + - Unregistered datasets default to using the `energy` attribute + and a conversion factor of 1, which is always correct for our + internally generated datasets. + """ + extract_info: Dict[str, List[Tuple]] = { + "mp-traj": [("forces", 1)], + "mp-traj-d3": [("forces", 1), ("data.d3.forces", 1)], + "alexandria-d3": [("forces", 1), ("data.d3.forces", 1)], + } + if dataset not in extract_info: + if not hasattr(row, "forces"): + raise ValueError( + f"db row {row.id} doesn't have a forces attribute directly, " + "but also doesn't define a method to extract forces info." + ) + return get_property_from_row("forces", row, 1) + + forces_ = 0.0 + for row_attribute, conversion_factor in extract_info[dataset]: + forces_ += get_property_from_row(row_attribute, row, conversion_factor) + return forces_ + + +def stress_row_fn(row: ase.db.row.AtomsRow, dataset: str) -> float: + """Extract stress data.""" + extract_info: Dict[str, List[Tuple]] = { + "mp-traj": [("stress", 1)], + "mp-traj-d3": [("stress", 1), ("data.d3.stress", 1)], + "alexandria-d3": [("stress", 1), ("data.d3.stress", 1)], + } + if dataset not in extract_info: + if not hasattr(row, "stress"): + raise ValueError( + f"db row {row.id} doesn't have an stress attribute directly " + ", but also doesn't define a method to extract stress info." + ) + return get_property_from_row("stress", row, 1) + + stress_ = 0.0 + for row_attribute, conversion_factor in extract_info[dataset]: + stress_ += get_property_from_row(row_attribute, row, conversion_factor) + return stress_ + + +def test_fixture_node_row_fn(row: ase.db.row.AtomsRow): + """Just return random noise.""" + + pos = ms.from_numpy(row.toatoms().positions) + return ops.rand_like(pos).to(ms.float32) + + +def test_fixture_graph_row_fn(): + """Just return random noise.""" + return mint.randn((1, 1)).to(ms.float32) + + +energy = PropertyDefinition( + name="energy", + dim=1, + domain="real", + row_to_property_fn=energy_row_fn, +) + +forces = PropertyDefinition( + name="forces", + dim=3, + domain="real", + row_to_property_fn=forces_row_fn, +) + +stress = PropertyDefinition( + name="stress", + dim=6, + domain="real", + row_to_property_fn=stress_row_fn, +) + +test_fixture = PropertyDefinition( + name="test-fixture", + dim=3, + domain="real", + row_to_property_fn=test_fixture_node_row_fn, +) + +test_graph_fixture = PropertyDefinition( + name="test-graph-fixture", + dim=1, + domain="real", + row_to_property_fn=test_fixture_graph_row_fn, +) + + +PROPERTIES = { + "energy": energy, + "forces": forces, + "stress": stress, + "test-fixture": test_fixture, + "test-graph-fixture": test_graph_fixture, +} diff --git a/MindChemistry/applications/orb/src/segment_ops.py b/MindChemistry/applications/orb/src/segment_ops.py new file mode 100644 index 000000000..d8b671e40 --- /dev/null +++ b/MindChemistry/applications/orb/src/segment_ops.py @@ -0,0 +1,202 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Segment operations.""" + +from typing import Optional +import numpy as np +import mindspore as ms +from mindspore import ops, Tensor, mint + +MSINT = [ms.int64, ms.int32, ms.int16, ms.int8, ms.uint8] + + +def aggregate_nodes(tensor: Tensor, n_node: Tensor, reduction: str = "mean", deterministic: bool = False) -> Tensor: + """Aggregates over a tensor based on graph sizes.""" + count = len(n_node) + if deterministic: + ms.set_seed(1) + segments = ops.arange(count).repeat_interleave(n_node).astype(ms.int32) + if reduction == "sum": + return scatter_sum(tensor, segments, dim=0) + if reduction == "mean": + return scatter_mean(tensor, segments, dim=0) + if reduction == "max": + return scatter_max(tensor, segments, dim=0) + raise ValueError("Invalid reduction argument. Use sum, mean or max.") + + +def segment_sum(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based sum over segments of a tensor.""" + return scatter_sum(data, segment_ids, dim=0, dim_size=num_segments) + + +def segment_max(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based max over segments of a tensor.""" + assert segment_ids is not None, "segment_ids must not be None" + assert num_segments > 0, "num_segments must be greater than 0" + max_op = ops.ArgMaxWithValue(axis=0) + _, max_values = max_op(data) + return max_values + + +def segment_mean(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based mean over segments of a tensor.""" + sum_v = segment_sum(data, segment_ids, num_segments) + count = ops.scatter_add(ops.zeros( + (num_segments,), dtype=ms.int32), segment_ids, ops.ones_like(segment_ids)) + return sum_v / count.astype(sum_v.dtype) + + +def segment_softmax(data: Tensor, segment_ids: Tensor, num_segments: int, weights: Optional[Tensor] = None): + """Computes a softmax over segments of the tensor.""" + data_max = segment_max(data, segment_ids, num_segments) + data = data - data_max[segment_ids] + + unnormalised_probs = ops.exp(data) + if weights is not None: + unnormalised_probs = unnormalised_probs * weights + denominator = segment_sum(unnormalised_probs, segment_ids, num_segments) + + return safe_division(unnormalised_probs, denominator, segment_ids) + + +def safe_division(numerator: Tensor, denominator: Tensor, segment_ids: Tensor): + """Divides logits by denominator, setting 0 where the denominator is zero.""" + result = ops.where(denominator[segment_ids] == + 0, 0, numerator / denominator[segment_ids]) + return result + + +def _broadcast(src: Tensor, other: Tensor, dim: int): + """Broadcasts the source tensor to match the shape of the other tensor along the specified dimension.""" + if dim < 0: + dim = other.ndim + dim + if src.ndim == 1: + for _ in range(0, dim): + src = src.unsqueeze(0) + for _ in range(src.ndim, other.ndim): + src = src.unsqueeze(-1) + src = src.expand_as(other) + return src + + +def scatter_sum( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None, reduce: str = "sum" +) -> Tensor: + """Applies a sum reduction of the orb_models tensor along the specified dimension.""" + assert reduce == "sum" + index = _broadcast(index, src, dim) + if out is None: + size = list(src.shape) + if dim_size is not None: + size[dim] = dim_size + elif index.numel() == 0: + size[dim] = 0 + else: + size[dim] = int(index.max()) + 1 + out = ops.zeros(size, dtype=src.dtype) + return mint.scatter_add(out, dim, index, src) + return mint.scatter_add(out, dim, index, src) + + +def scatter_std( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None, unbiased: bool = True +) -> Tensor: + """Computes the standard deviation of the orb_models tensor along the specified dimension.""" + if out is not None: + dim_size = out.shape[dim] + + if dim < 0: + dim = src.ndim + dim + + count_dim = dim + if index.ndim <= dim: + count_dim = index.ndim - 1 + + ones = ops.ones(index.shape, dtype=src.dtype) + count = scatter_sum(ones, index, count_dim, dim_size=dim_size) + + index = _broadcast(index, src, dim) + tmp = scatter_sum(src, index, dim, dim_size=dim_size) + count = _broadcast(count, tmp, dim).clip(1) + mean = tmp / count + + var = src - mean.gather(dim, index) + var = var * var + out = scatter_sum(var, index, dim, out=out, dim_size=dim_size) + + if unbiased: + count = count - 1 + count = count.clip(1) + out = out / (count + 1e-6) + out = ops.sqrt(out) + return out + + +def scatter_mean( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None +) -> Tensor: + """Computes the mean of the orb_models tensor along the specified dimension.""" + out = scatter_sum(src, index, dim, out=out, dim_size=dim_size) + dim_size = out.shape[dim] + + index_dim = dim + if index_dim < 0: + index_dim = index_dim + src.ndim + if index.ndim <= index_dim: + index_dim = index.ndim - 1 + + ones = ops.ones(index.shape, dtype=src.dtype) + count = scatter_sum(ones, index, index_dim, dim_size=dim_size) + count = count.clip(1) + count = _broadcast(count, out, dim) + out = out / count + return out + + +def scatter_max( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None +) -> Tensor: + """Computes the maximum of the orb_models tensor for each group defined by index along the specified dimension.""" + if out is not None: + raise NotImplementedError( + "The 'out' argument is not supported for scatter_max") + + if src.dtype in MSINT: + init_value = np.iinfo(src.dtype).min + else: + init_value = np.finfo(src.dtype).min + + if dim < 0: + dim = src.ndim + dim + + if dim_size is None: + dim_size = int(index.max()) + 1 + + result = ops.ones( + (dim_size, *src.shape[:dim], *src.shape[dim + 1:]), dtype=src.dtype) + result = init_value * result + broadcasted_index = _broadcast(index, src, dim) + + scatter_result = ops.ZerosLike()(result) + index = ops.expand_dims(broadcasted_index, dim) + scatter_result = scatter_result.scatter_update(index, src) + result = ops.Maximum()(result, scatter_result) + return result diff --git a/MindChemistry/applications/orb/src/trainer.py b/MindChemistry/applications/orb/src/trainer.py new file mode 100644 index 000000000..5b1404f6b --- /dev/null +++ b/MindChemistry/applications/orb/src/trainer.py @@ -0,0 +1,329 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Trainer.""" + +from typing import Dict, Optional, Tuple + +import mindspore as ms +from mindspore import ops, Tensor, mint + +from src import base, segment_ops + +class OrbLoss: + """Loss function for ORB models. + + This class is used to compute the loss for the ORB model. + It can be used to compute the loss for both node and graph predictions. + """ + + def __init__(self, model): + """Initializes the OrbLoss. + + Args: + target: either the name of a PropertyDefinition or a PropertyDefinition itself. + """ + self.model = model + + def loss_node(self, batch, out_batch=None): + """Apply mlp to compute loss and metrics.""" + batch_n_node = batch.n_node + assert batch.node_targets is not None + target = batch.node_targets['forces'].squeeze(-1) + pred = out_batch["node_pred"].squeeze(-1) + # make sure we remove fixed atoms before normalization + pred, target, batch_n_node = _remove_fixed_atoms( + pred, target, batch_n_node, batch.fix_atoms, self.model.training + ) + mae = mint.abs(pred - self.model.node_head.normalizer(target)) + raw_pred = self.model.node_head.normalizer.inverse(pred) + raw_mae = mint.abs(raw_pred - target) + + mae = mae.mean(dim=-1) + mae = segment_ops.aggregate_nodes( + mae, batch_n_node, reduction="mean" + ).mean() + raw_mae = raw_mae.mean(dim=-1) + raw_mae = segment_ops.aggregate_nodes( + raw_mae, batch_n_node, reduction="mean" + ).mean() + + metrics = { + "node_mae": mae.item(), + "node_mae_raw": raw_mae.item(), + "node_cosine_sim": ops.cosine_similarity(raw_pred, target, dim=-1).mean().item(), + "fwt_0.03": forces_within_threshold(raw_pred, target, batch_n_node), + } + return mae, base.ModelOutput(loss=mae, log=metrics) + + def loss_graph(self, batch, out_batch=None): + """Apply mlp to compute loss and metrics. + + Depending on whether the target is real/binary/categorical, we + use an MSE/cross-entropy loss. In the case of cross-entropy, the + preds are logits (not normalised) to take advantage of numerically + stable log-softmax. + """ + assert batch.system_targets is not None + target = batch.system_targets['stress'].squeeze(-1) + if self.model.stress_head.compute_stress: + pred = out_batch["stress_pred"].squeeze(-1) + else: + pred = out_batch["graph_pred"].squeeze(-1) + + normalized_target = self.model.stress_head.normalizer(target) + errors = normalized_target - pred + mae = mint.abs(errors).mean() + + raw_pred = self.model.stress_head.normalizer.inverse(pred) + raw_mae = mint.abs(raw_pred - target).mean() + metrics = {"stress_mae": mae.item(), "stress_mae_raw": raw_mae.item()} + return mae, base.ModelOutput(loss=mae, log=metrics) + + + def loss_energy(self, batch, out_batch=None): + """Apply mlp to compute loss and metrics.""" + assert batch.system_targets is not None + target = batch.system_targets['energy'].squeeze(-1) + pred = out_batch["graph_pred"].squeeze(-1) + + reference = self.model.graph_head.reference(batch.atomic_numbers, batch.n_node).squeeze(-1) + reference_target = target - reference + if self.model.graph_head.atom_avg: + reference_target = reference_target / batch.n_node + + normalized_reference = self.model.graph_head.normalizer(reference_target) + model_loss = normalized_reference - pred + + raw_pred = self.model.graph_head.normalizer.inverse(pred) + if self.model.graph_head.atom_avg: + raw_pred = raw_pred * batch.n_node + raw_mae = mint.abs((raw_pred + reference) - target).mean() + + reference_mae = mint.abs(reference_target).mean() + model_mae = mint.abs(model_loss).mean() + metrics = { + "energy_reference_mae": reference_mae.item(), + "energy_mae": model_mae.item(), + "energy_mae_raw": raw_mae.item(), + } + return model_mae, base.ModelOutput(loss=model_mae, log=metrics) + + def loss(self, batch, label=None): + """Loss function of Orb GraphRegressor.""" + assert label is None, "Orb GraphRegressor does not support labels." + + out = self.model( + batch.edge_features, + batch.node_features, + batch.senders, + batch.receivers, + batch.n_node, + ) + loss = Tensor(0.0, ms.float32) + metrics: Dict = {} + + loss1, graph_out = self.loss_energy(batch, out) + metrics.update(graph_out.log) + loss = loss.type_as(loss1) + loss1 + + loss2, stress_out = self.loss_graph(batch, out) + metrics.update(stress_out.log) + loss = loss.type_as(loss2) + loss2 + + loss3, node_out = self.loss_node(batch, out) + metrics.update(node_out.log) + loss = loss.type_as(loss3) + loss3 + + metrics["loss"] = loss.item() + return loss, metrics + + +def binary_accuracy( + pred: Tensor, target: Tensor, threshold: float = 0.5 +) -> float: + """Calculate binary accuracy between 2 tensors. + + Args: + pred: the prediction tensor. + target: the tensor of target values. + threshold: Binary classification threshold. Default 0.5. + + Returns: + mean accuracy. + """ + return ((pred > threshold) == target).to(ms.float32).mean().item() + + +def categorical_accuracy(pred: Tensor, target: Tensor) -> float: + """Calculate accuracy for K class classification. + + Args: + pred: the tensor of logits for K classes of shape (..., K) + target: tensor of integer target values of shape (...) + + Returns: + mean accuracy. + """ + pred_labels = mint.argmax(pred, dim=-1) + return (pred_labels == target).to(ms.float32).mean().item() + + +def error_within_threshold( + pred: Tensor, target: Tensor, threshold: float = 0.02 +) -> float: + """Calculate MAE between 2 tensors within a threshold. + + Args: + pred: the prediction tensor. + target: the tensor of target values. + threshold: margin threshold. Default 0.02 (derived from OCP metrics). + + Returns: + Mean predictions within threshold. + """ + error = mint.abs(pred - target) + within_threshold = error < threshold + return within_threshold.to(ms.float32).mean().item() + + +def forces_within_threshold( + pred: Tensor, + target: Tensor, + batch_num_nodes: Tensor, + threshold: float = 0.03, +) -> float: + """Calculate MAE between batched graph tensors within a threshold. + + The predictions for a graph are counted as being within the threshold + only if all nodes in the graph have predictions within the threshold. + + Args: + pred: the prediction tensor. + target: the tensor of target values. + batch_num_nodes: A tensor containing the number of nodes per + graph. + threshold: margin threshold. Default 0.03 (derived from OCP metrics). + + Returns: + Mean predictions within threshold. + """ + error = mint.abs(pred - target) + largest_dim_fwt = error.max(-1)[0] < threshold + + count_within_threshold = segment_ops.aggregate_nodes( + largest_dim_fwt.float(), batch_num_nodes, reduction="sum" + ) + # count equals batch_num_nodes if all nodes within threshold + return (count_within_threshold == batch_num_nodes).to(ms.float32).mean().item() + + +def energy_and_forces_within_threshold( + pred_energy: Tensor, + pred_forces: Tensor, + target_energy: Tensor, + target_forces: Tensor, + batch_num_nodes: Tensor, + fixed_atoms: Optional[Tensor] = None, + threshold: Tuple[float, float] = (0.02, 0.03), +) -> float: + """Calculate MAE between batched graph energies and forces within a threshold. + + The predictions for a graph are counted as being within the threshold + only if all nodes in the graph have predictions within the threshold AND + the energies are also within a threshold. A combo of the two above functions. + + Args: + pred_*: the prediction tensors. + target_*: the tensor of target values. + batch_num_nodes: A tensor containing the number of nodes per + graph. + fixed_atoms: A tensor of bools indicating which atoms are fixed. + threshold: margin threshold. Default (0.02, 0.03) (derived from OCP metrics). + Returns: + Mean predictions within threshold. + """ + energy_err = mint.abs(pred_energy - target_energy) + ewt = energy_err < threshold[0] + + forces_err = mint.abs(pred_forces - target_forces) + largest_dim_fwt = forces_err.max(-1).values < threshold[1] + + working_largest_dim_fwt = largest_dim_fwt + + if fixed_atoms is not None: + fixed_per_graph = segment_ops.aggregate_nodes( + fixed_atoms.int(), batch_num_nodes, reduction="sum" + ) + # remove the fixed atoms from the counts + batch_num_nodes = batch_num_nodes - fixed_per_graph + # remove the fixed atoms from the forces + working_largest_dim_fwt = largest_dim_fwt[not fixed_atoms] + + force_count_within_threshold = segment_ops.aggregate_nodes( + working_largest_dim_fwt.int(), batch_num_nodes, reduction="sum" + ) + fwt = force_count_within_threshold == batch_num_nodes + + # count equals batch_num_nodes if all nodes within threshold + return (fwt & ewt).to(ms.float32).mean().item() + + +def _remove_fixed_atoms( + pred_node: Tensor, + node_target: Tensor, + batch_n_node: Tensor, + fix_atoms: Optional[Tensor], + training: bool, +): + """We use inf targets on purpose to designate nodes for removal.""" + assert len(pred_node) == len(node_target) + if fix_atoms is not None and not training: + pred_node = pred_node[~fix_atoms] + node_target = node_target[~fix_atoms] + batch_n_node = segment_ops.aggregate_nodes( + (~fix_atoms).int(), batch_n_node, reduction="sum" + ) + return pred_node, node_target, batch_n_node + + +def bce_loss( + pred: Tensor, target: Tensor, metric_prefix: str = "" +) -> Tuple: + """Binary cross-entropy loss with accuracy metric.""" + loss = mint.nn.BCEWithLogitsLoss()(pred, target.float()) + accuracy = binary_accuracy(pred, target) + return ( + loss, + { + f"{metric_prefix}_accuracy": accuracy, + f"{metric_prefix}_loss": loss.item(), + }, + ) + + +def cross_entropy_loss( + pred: Tensor, target: Tensor, metric_prefix: str = "" +) -> Tuple: + """Cross-entropy loss with accuracy metric.""" + loss = mint.nn.CrossEntropyLoss()(pred, target.long()) + accuracy = categorical_accuracy(pred, target) + return ( + loss, + { + f"{metric_prefix}_accuracy": accuracy, + f"{metric_prefix}_loss": loss.item(), + }, + ) diff --git a/MindChemistry/applications/orb/src/utils.py b/MindChemistry/applications/orb/src/utils.py new file mode 100644 index 000000000..34bd4623f --- /dev/null +++ b/MindChemistry/applications/orb/src/utils.py @@ -0,0 +1,296 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Experiment utilities.""" + +import math +import random +import re +from collections import defaultdict +from typing import Dict, List, Mapping, Optional, Tuple, TypeVar, Any + +import yaml +import numpy as np +import mindspore as ms +from mindspore import Tensor, mint + +from src import base + +T = TypeVar("T") + + +def load_cfg(filename): + """load_cfg + + Load configurations from yaml file and return a namespace object + """ + from argparse import Namespace + with open(filename, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + return Namespace(**cfg) + + +def ensure_detached(x: base.Metric) -> base.Metric: + """Ensure that the tensor is detached and on the CPU.""" + return x + + +def to_item(x: base.Metric) -> base.Metric: + """Convert a tensor to a python scalar.""" + if isinstance(x, Tensor): + return x.item() + return x + + +def prefix_keys( + dict_to_prefix: Dict[str, T], prefix: str, sep: str = "/" +) -> Dict[str, T]: + """Add a prefix to dictionary keys with a separator.""" + return {f"{prefix}{sep}{k}": v for k, v in dict_to_prefix.items()} + + +def seed_everything(seed: int, rank: int = 0) -> None: + """Set the seed for all pseudo random number generators.""" + random.seed(seed + rank) + np.random.seed(seed + rank) + ms.manual_seed(seed + rank) + + +class ScalarMetricTracker: + """Keep track of average scalar metric values.""" + + def __init__(self): + self.reset() + + def reset(self): + """Reset the AverageMetrics.""" + self.sums = defaultdict(float) + self.counts = defaultdict(int) + + def update(self, metrics: Mapping[str, base.Metric]) -> None: + """Update the metric counts with new values.""" + for k, v in metrics.items(): + if isinstance(v, Tensor) and v.nelement() > 1: + continue # only track scalar metrics + if isinstance(v, Tensor) and v.isnan().any(): + continue + self.sums[k] += ensure_detached(v) + self.counts[k] += 1 + + def get_metrics(self): + """Get the metric values, possibly reducing across gpu processes.""" + return {k: to_item(v) / self.counts[k] for k, v in self.sums.items()} + + +def gradient_clipping( + model: ms.nn.Cell, clip_value: float +) -> List[Any]: + """Add gradient clipping hooks to a model. + + This is the correct way to implement gradient clipping, because + gradients are clipped as gradients are computed, rather than after + all gradients are computed - this means expoding gradients are less likely, + because they are "caught" earlier. + + Args: + model: The model to add hooks to. + clip_value: The upper and lower threshold to clip the gradients to. + + Returns: + A list of handles to remove the hooks from the parameters. + """ + handles = [] + + def _clip(grad): + if grad is None: + return grad + return grad.clamp(min=-clip_value, max=clip_value) + + for parameter in model.trainable_params(): + if parameter.requires_grad: + h = parameter.register_hook(_clip) + handles.append(h) + + return handles + + +def get_optim( + lr: float, total_steps: int, model: ms.nn.Cell +) -> Tuple[ms.experimental.optim.Optimizer, Optional[ms.experimental.optim.lr_scheduler.LRScheduler]]: + """Configure optimizers, LR schedulers and EMA.""" + + # Initialize parameter groups + params = [] + + # Split parameters based on the regex + for param in model.trainable_params(): + name = param.name + if re.search(r"(.*bias|.*layer_norm.*|.*batch_norm.*)", name): + params.append({"params": param, "weight_decay": 0.0}) + else: + params.append({"params": param}) + + # Create the optimizer with the parameter groups + optimizer = ms.experimental.optim.Adam(params, lr=lr) + + # Create the learning rate scheduler + scheduler = ms.experimental.optim.lr_scheduler.CyclicLR( + optimizer, base_lr=1.0e-9, max_lr=lr, step_size_up=int(total_steps*0.04), step_size_down=total_steps + ) + + return optimizer, scheduler + + +def rand_angles(*shape, dtype=None): + r"""random rotation angles + + Parameters + ---------- + *shape : int + + Returns + ------- + alpha : `Tensor` + tensor of shape :math:`(\mathrm{shape})` + + beta : `Tensor` + tensor of shape :math:`(\mathrm{shape})` + + gamma : `Tensor` + tensor of shape :math:`(\mathrm{shape})` + """ + alpha, gamma = 2 * math.pi * mint.rand(2, *shape, dtype=dtype) + beta = mint.rand(shape, dtype=dtype).mul(2).sub(1).acos() + return alpha, beta, gamma + + +def matrix_x(angle: Tensor) -> Tensor: + r"""matrix of rotation around X axis + + Parameters + ---------- + angle : `Tensor` + tensor of any shape :math:`(...)` + + Returns + ------- + `Tensor` + matrices of shape :math:`(..., 3, 3)` + """ + c = angle.cos() + s = angle.sin() + o = mint.ones_like(angle) + z = mint.zeros_like(angle) + return mint.stack( + [ + mint.stack([o, z, z], dim=-1), + mint.stack([z, c, -s], dim=-1), + mint.stack([z, s, c], dim=-1), + ], + dim=-2, + ) + + +def matrix_y(angle: Tensor) -> Tensor: + r"""matrix of rotation around Y axis + + Parameters + ---------- + angle : `Tensor` + tensor of any shape :math:`(...)` + + Returns + ------- + `Tensor` + matrices of shape :math:`(..., 3, 3)` + """ + c = angle.cos() + s = angle.sin() + o = mint.ones_like(angle) + z = mint.zeros_like(angle) + return mint.stack( + [ + mint.stack([c, z, s], dim=-1), + mint.stack([z, o, z], dim=-1), + mint.stack([-s, z, c], dim=-1), + ], + dim=-2, + ) + + +def matrix_z(angle: Tensor) -> Tensor: + r"""matrix of rotation around Z axis + + Parameters + ---------- + angle : `Tensor` + tensor of any shape :math:`(...)` + + Returns + ------- + `Tensor` + matrices of shape :math:`(..., 3, 3)` + """ + c = angle.cos() + s = angle.sin() + o = mint.ones_like(angle) + z = mint.zeros_like(angle) + return mint.stack( + [ + mint.stack([c, -s, z], dim=-1), + mint.stack([s, c, z], dim=-1), + mint.stack([z, z, o], dim=-1), + ], + dim=-2, + ) + + +def angles_to_matrix(alpha, beta, gamma): + r"""conversion from angles to matrix + + Parameters + ---------- + alpha : `Tensor` + tensor of shape :math:`(...)` + + beta : `Tensor` + tensor of shape :math:`(...)` + + gamma : `Tensor` + tensor of shape :math:`(...)` + + Returns + ------- + `Tensor` + matrices of shape :math:`(..., 3, 3)` + """ + alpha, beta, gamma = ms.numpy.broadcast_arrays(alpha, beta, gamma) + return matrix_y(alpha) @ matrix_x(beta) @ matrix_y(gamma) + + +def rand_matrix(*shape, dtype=None): + r"""random rotation matrix + + Parameters + ---------- + *shape : int + + Returns + ------- + `Tensor` + tensor of shape :math:`(\mathrm{shape}, 3, 3)` + """ + rotation_matrix = angles_to_matrix(*rand_angles(*shape, dtype=dtype)) + return rotation_matrix diff --git a/MindChemistry/mindchemistry/cell/__init__.py b/MindChemistry/mindchemistry/cell/__init__.py index f92153c67..5660308bc 100644 --- a/MindChemistry/mindchemistry/cell/__init__.py +++ b/MindChemistry/mindchemistry/cell/__init__.py @@ -21,6 +21,7 @@ from .deephe3nn import * from .matformer import * from .dimenet import * from .gemnet import * +from .orb import * __all__ = [ "Nequip", 'AutoEncoder', 'FCNet', 'MLPNet', 'CSPNet' @@ -30,3 +31,4 @@ __all__.extend(matformer.__all__) __all__.extend(allegro.__all__) __all__.extend(dimenet.__all__) __all__.extend(gemnet.__all__) +__all__.extend(orb.__all__) diff --git a/MindChemistry/mindchemistry/cell/orb/__init__.py b/MindChemistry/mindchemistry/cell/orb/__init__.py new file mode 100644 index 000000000..709978030 --- /dev/null +++ b/MindChemistry/mindchemistry/cell/orb/__init__.py @@ -0,0 +1,36 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""init""" + +from .orb import ( + NodeHead, + GraphHead, + EnergyHead, + Orb, +) +from .gns import ( + AttentionInteractionNetwork, + MoleculeGNS, +) + +__all__ = [ + "AttentionInteractionNetwork", + "EnergyHead", + "GraphHead", + "MoleculeGNS", + "NodeHead", + "Orb", +] diff --git a/MindChemistry/mindchemistry/cell/orb/gns.py b/MindChemistry/mindchemistry/cell/orb/gns.py new file mode 100644 index 000000000..ab53083f8 --- /dev/null +++ b/MindChemistry/mindchemistry/cell/orb/gns.py @@ -0,0 +1,690 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""GNS Molecule.""" + + +from typing import List, Literal, Optional, Dict, Any, Union +from functools import partial + +import numpy as np +from mindspore import nn, ops, Tensor, mint +from mindspore.common.initializer import Uniform +import mindspore.ops.operations as P + +from mindchemistry.cell.orb.utils import build_mlp + +_KEY = "feat" + + +def mlp_and_layer_norm(in_dim: int, out_dim: int, hidden_dim: int, n_layers: int) -> nn.SequentialCell: + """Create an MLP followed by layer norm. + + Args: + in_dim (int): Input dimension. + out_dim (int): Output dimension. + hidden_dim (int): Hidden dimension. + n_layers (int): Number of hidden layers. + + Returns: + nn.SequentialCell: A sequential cell containing the MLP and layer norm. + """ + layers = build_mlp( + in_dim, + [hidden_dim for _ in range(n_layers)], + out_dim, + ) + layers.append(nn.LayerNorm((out_dim,))) + return layers + + +def get_cutoff(p: int, r: Tensor, r_max: float) -> Tensor: + """Get the cutoff function for attention. + + Args: + p (int): Polynomial order. + r (Tensor): Distance tensor. + r_max (float): Maximum distance for the cutoff. + + Returns: + Tensor: Cutoff tensor. + """ + envelope = 1.0 - ((p + 1.0) * (p + 2.0) / 2.0) * ops.pow(r / r_max, p) + \ + p * (p + 2.0) * ops.pow(r / r_max, p + 1) - \ + (p * (p + 1.0) / 2) * ops.pow(r / r_max, p + 2) + cutoff = ops.expand_dims( + ops.where(r < r_max, envelope, ops.zeros_like(envelope)), -1) + return cutoff + + +def gaussian_basis_function( + scalars: Tensor, + num_bases: Union[Tensor, int], + radius: Union[Tensor, float], + scale: Union[Tensor, float] = 1.0, +) -> Tensor: + """Gaussian basis function applied to a tensor of scalars. + + Args: + scalars (Tensor): Scalars to compute the gbf on. Shape [num_scalars]. + num_bases (Tensor): The number of bases. An Int. + radius (Tensor): The largest centre of the bases. A Float. + scale (Tensor, optional): The width of the gaussians. Defaults to 1. + + Returns: + Tensor: A tensor of shape [num_scalars, num_bases]. + """ + assert len(scalars.shape) == 1 + gaussian_means = ops.arange( + 0, float(radius), float(radius) / num_bases + ) + return mint.exp( + -(scale**2) * (scalars.unsqueeze(1) - gaussian_means.unsqueeze(0)).abs() ** 2 + ) + + +class AtomEmbedding(nn.Cell): + r""" + AtomEmbedding Layer. + + This layer initializes atom embeddings based on the atomic number of elements in the periodic table. + It uses an embedding table initialized with a uniform distribution over the range [-sqrt(3), sqrt(3)]. + + Args: + emb_size (int): Size of the embedding vector for each atom. + num_elements (int): Number of elements in the periodic table (typically 118 for known elements). + + Inputs: + - **x** (Tensor) - Input tensor of shape [..., num_atoms], where + each value represents the atomic number of an atom in the periodic table. + + Outputs: + - **h** (Tensor) - Output tensor of shape [..., num_atoms, emb_size], + where each atom's embedding is represented as a vector of size `emb_size`. + + Supported Platforms: + ``Ascend`` + """ + def __init__(self, emb_size, num_elements): + """init + """ + super().__init__() + self.emb_size = emb_size + self.embeddings = nn.Embedding( + num_elements + 1, emb_size, embedding_table=Uniform(np.sqrt(3))) + + def construct(self, x): + """construct + """ + h = self.embeddings(x) + return h + + +class Encoder(nn.Cell): + r""" + Encoder for Graph Network States (GNS). + + This encoder processes node and edge features using MLPs and layer normalization. + It concatenates the features of nodes and edges, applies MLPs to update their states, + and returns the updated features. + + Args: + num_node_in_features (int): Number of input features for nodes. + num_node_out_features (int): Number of output features for nodes. + num_edge_in_features (int): Number of input features for edges. + num_edge_out_features (int): Number of output features for edges. + num_mlp_layers (int): Number of MLP layers. + mlp_hidden_dim (int): Hidden dimension for the MLP. + node_feature_names (List[str]): List of node feature names. + edge_feature_names (List[str]): List of edge feature names. + + Inputs: + - **nodes** (Dict[str, Tensor]) - Dictionary of node features, where keys are feature names + and values are tensors of shape (num_nodes, num_node_in_features). + - **edges** (Dict[str, Tensor]) - Dictionary of edge features, where keys are feature names + and values are tensors of shape (num_edges, num_edge_in_features). + + Outputs: + - **edges** (Dict[str, Tensor]) - Updated edge features dictionary, where key "feat" contains + the updated edge features of shape (num_edges, num_edge_out_features). + - **nodes** (Dict[str, Tensor]) - Updated node features dictionary, where key "feat" contains + the updated node features of shape (num_nodes, num_node_out_features). + + Supported Platforms: + ``Ascend`` + """ + + def __init__(self, + num_node_in_features: int, + num_node_out_features: int, + num_edge_in_features: int, + num_edge_out_features: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + node_feature_names: List[str], + edge_feature_names: List[str]): + """init + """ + super().__init__() + self.node_feature_names = node_feature_names + self.edge_feature_names = edge_feature_names + self._node_fn = mlp_and_layer_norm( + num_node_in_features, num_node_out_features, mlp_hidden_dim, num_mlp_layers) + self._edge_fn = mlp_and_layer_norm( + num_edge_in_features, num_edge_out_features, mlp_hidden_dim, num_mlp_layers) + + def construct(self, nodes, edges): + """construct + """ + edge_features = ops.cat([edges[k] for k in self.edge_feature_names], axis=-1) + node_features = ops.cat([nodes[k] for k in self.node_feature_names], axis=-1) + + edges.update({_KEY: self._edge_fn(edge_features)}) + nodes.update({_KEY: self._node_fn(node_features)}) + return edges, nodes + + +class InteractionNetwork(nn.Cell): + r""" + Interaction Network. + + Implements a message passing neural network layer that updates node and edge features based on interactions. + This layer combines node and edge features, applies MLPs to update their states, and returns the updated features. + + Args: + num_node_in (int): Number of input features for nodes. + num_node_out (int): Number of output features for nodes. + num_edge_in (int): Number of input features for edges. + num_edge_out (int): Number of output features for edges. + num_mlp_layers (int): Number of MLP layers. + mlp_hidden_dim (int): Hidden dimension for the MLP. + + Inputs: + - **graph_edges** (Dict[str, Tensor]) - Dictionary of edge features, where key "feat" contains + the edge features of shape (num_edges, num_edge_in). + - **graph_nodes** (Dict[str, Tensor]) - Dictionary of node features, where key "feat" contains + the node features of shape (num_nodes, num_node_in). + - **senders** (Tensor) - Indices of the sender nodes for each edge, shape (num_edges,). + - **receivers** (Tensor) - Indices of the receiver nodes for each edge, shape (num_edges,). + + Outputs: + - **edges** (Dict[str, Tensor]) - Updated edge features dictionary, where key "feat" contains + the updated edge features of shape (num_edges, num_edge_out). + - **nodes** (Dict[str, Tensor]) - Updated node features dictionary, where key "feat" contains + the updated node features of shape (num_nodes, num_node_out). + + Supported Platforms: + ``Ascend`` + """ + def __init__(self, + num_node_in: int, + num_node_out: int, + num_edge_in: int, + num_edge_out: int, + num_mlp_layers: int, + mlp_hidden_dim: int): + """init + """ + super().__init__() + self._node_mlp = mlp_and_layer_norm( + num_node_in + num_edge_out, num_node_out, mlp_hidden_dim, num_mlp_layers) + self._edge_mlp = mlp_and_layer_norm( + num_node_in + num_node_in + num_edge_in, num_edge_out, mlp_hidden_dim, num_mlp_layers) + + def construct(self, graph_edges, graph_nodes, senders, receivers): + """construct + """ + nodes = graph_nodes[_KEY] + edges = graph_edges[_KEY] + + sent_attributes = ops.gather(nodes, senders, 0) + received_attributes = ops.gather(nodes, receivers, 0) + + edge_features = ops.cat( + [edges, sent_attributes, received_attributes], axis=1) + updated_edges = self._edge_mlp(edge_features) + + received_attributes = ops.scatter_add( + ops.zeros_like(nodes), receivers, updated_edges) + + node_features = ops.cat([nodes, received_attributes], axis=1) + updated_nodes = self._node_mlp(node_features) + + nodes = graph_nodes[_KEY] + updated_nodes + edges = graph_edges[_KEY] + updated_edges + + node_features = {**graph_nodes, _KEY: nodes} + edge_features = {**graph_edges, _KEY: edges} + return edge_features, node_features + + +# pylint: disable=C0301 +class AttentionInteractionNetwork(nn.Cell): + r""" + Attention interaction network. + Implements attention-based message passing neural network layer for edge updates in molecular graphs. + + Args: + num_node_in (int): Number of input node features. + num_node_out (int): Number of output node features. + num_edge_in (int): Number of input edge features. + num_edge_out (int): Number of output edge features. + num_mlp_layers (int): Number of hidden layers in node and edge update MLPs. + mlp_hidden_dim (int): Hidden dimension size of MLPs. + attention_gate (str, optional): Attention gate type, ``"sigmoid"`` or ``"softmax"``. Default: ``"sigmoid"``. + distance_cutoff (bool, optional): Whether to use distance-based edge cutoff. Default: ``True``. + polynomial_order (int, optional): Order of polynomial cutoff function. Default: ``4``. + cutoff_rmax (float, optional): Maximum distance for cutoff. Default: ``6.0``. + + Inputs: + - **graph_edges** (dict) - Edge feature dictionary, must contain key "feat" with shape :math:`(n_{edges}, num\_edge\_in)`. + - **graph_nodes** (dict) - Node feature dictionary, must contain key "feat" with shape :math:`(n_{nodes}, num\_node\_in)`. + - **senders** (Tensor) - Sender node indices for each edge, shape :math:`(n_{edges},)`. + - **receivers** (Tensor) - Receiver node indices for each edge, shape :math:`(n_{edges},)`. + + Outputs: + - **edges** (dict) - Updated edge feature dictionary with key "feat" of shape :math:`(n_{edges}, num\_edge\_out)`. + - **nodes** (dict) - Updated node feature dictionary with key "feat" of shape :math:`(n_{nodes}, num\_node\_out)`. + + Raises: + ValueError: If `attention_gate` is not "sigmoid" or "softmax". + ValueError: If edge or node features do not contain the required "feat" key. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb.gns import AttentionInteractionNetwork + >>> attn_net = AttentionInteractionNetwork( + ... num_node_in=256, + ... num_node_out=256, + ... num_edge_in=256, + ... num_edge_out=256, + ... num_mlp_layers=2, + ... mlp_hidden_dim=512, + ... ) + >>> n_atoms = 4 + >>> n_edges = 10 + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + ... "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + ... } + >>> edge_features = { + ... "vectors": Tensor(np.random.randn(n_edges, 3).astype(np.float32)), + ... "r": Tensor(np.abs(np.random.randn(n_edges).astype(np.float32) * 10)), + ... "feat": Tensor(np.random.randn(n_edges, 256).astype(np.float32)) + ... } + >>> senders = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> receivers = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> edges, nodes = attn_net( + ... edge_features, + ... node_features, + ... senders, + ... receivers, + ... ) + >>> print(edges["feat"].shape, nodes["feat"].shape) + (10, 256) (4, 256) + """ + + def __init__(self, + num_node_in: int, + num_node_out: int, + num_edge_in: int, + num_edge_out: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + attention_gate: Literal["sigmoid", "softmax"] = "sigmoid", + distance_cutoff: bool = True, + polynomial_order: Optional[int] = 4, + cutoff_rmax: Optional[float] = 6.0): + """init + """ + super().__init__() + self._num_node_in = num_node_in + self._num_node_out = num_node_out + self._num_edge_in = num_edge_in + self._num_edge_out = num_edge_out + self._num_mlp_layers = num_mlp_layers + self._mlp_hidden_dim = mlp_hidden_dim + self._node_mlp = mlp_and_layer_norm( + num_node_in + num_edge_out + num_edge_out, num_node_out, mlp_hidden_dim, num_mlp_layers) + self._edge_mlp = mlp_and_layer_norm( + num_node_in + num_node_in + num_edge_in, num_edge_out, mlp_hidden_dim, num_mlp_layers) + self._receive_attn = nn.Dense(num_edge_in, 1) + self._send_attn = nn.Dense(num_edge_in, 1) + self._distance_cutoff = distance_cutoff + self._r_max = cutoff_rmax + self._polynomial_order = polynomial_order + self._attention_gate = attention_gate + + self.scatter_add = P.TensorScatterAdd() + + def construct(self, graph_edges, graph_nodes, senders, receivers): + """construct + """ + nodes = graph_nodes[_KEY] + edges = graph_edges[_KEY] + + p = self._polynomial_order + r_max = self._r_max + r = graph_edges['r'] + cutoff = get_cutoff(p, r, r_max) + + sent_attributes = ops.gather(nodes, senders, 0) + received_attributes = ops.gather(nodes, receivers, 0) + + if self._attention_gate == "softmax": + receive_attn = ops.softmax(self._receive_attn(edges), axis=0) + send_attn = ops.softmax(self._send_attn(edges), axis=0) + else: + receive_attn = ops.sigmoid(self._receive_attn(edges)) + send_attn = ops.sigmoid(self._send_attn(edges)) + + if self._distance_cutoff: + receive_attn = receive_attn * cutoff + send_attn = send_attn * cutoff + + edge_features = ops.cat( + [edges, sent_attributes, received_attributes], axis=1) + updated_edges = self._edge_mlp(edge_features) + + if senders.ndim < 2: + senders = senders.unsqueeze(-1) + sent_attributes = self.scatter_add( + ops.zeros_like(nodes), senders, updated_edges * send_attn) + if receivers.ndim < 2: + receivers = receivers.unsqueeze(-1) + received_attributes = self.scatter_add( + ops.zeros_like(nodes), receivers, updated_edges * receive_attn) + + node_features = ops.cat( + [nodes, received_attributes, sent_attributes], axis=1) + updated_nodes = self._node_mlp(node_features) + + nodes = graph_nodes[_KEY] + updated_nodes + edges = graph_edges[_KEY] + updated_edges + + node_features = {**graph_nodes, _KEY: nodes} + edge_features = {**graph_edges, _KEY: edges} + return edge_features, node_features + +class Decoder(nn.Cell): + r""" + Decoder for Graph Network States (GNS). + + This decoder processes node features using an MLP to produce predictions. + It takes the node features as input and outputs updated node features with predictions. + + Args: + num_node_in (int): Number of input features for nodes. + num_node_out (int): Number of output features for nodes. + num_mlp_layers (int): Number of MLP layers. + mlp_hidden_dim (int): Hidden dimension for the MLP. + batch_norm (bool, optional): Whether to apply batch normalization. Defaults to False. + + Inputs: + - **graph_nodes** (Dict[str, Tensor]) - Dictionary of node features, where key "feat" contains + the node features of shape (num_nodes, num_node_in). + + Outputs: + - **graph_nodes** (Dict[str, Tensor]) - Updated node features dictionary, where key "pred" contains + the predicted node features of shape (num_nodes, num_node_out). + + Supported Platforms: + ``Ascend`` + """ + def __init__(self, + num_node_in: int, + num_node_out: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + batch_norm: bool = False): + """Initialization. + Args: + num_node_in (int): Number of input features for nodes. + num_node_out (int): Number of output features for nodes. + num_mlp_layers (int): Number of MLP layers. + mlp_hidden_dim (int): Hidden dimension for the MLP. + batch_norm (bool, optional): Whether to apply batch normalization. Defaults to False. + """ + super().__init__() + seq = build_mlp( + num_node_in, + [mlp_hidden_dim for _ in range(num_mlp_layers)], + num_node_out, + ) + if batch_norm: + seq.append(nn.BatchNorm1d(num_node_out)) + self.node_fn = nn.SequentialCell(seq) + + def construct(self, graph_nodes): + """Forward pass of the decoder. + Args: + graph_nodes (Dict[str, Tensor]): Dictionary of node features. + Returns: + Dict[str, Tensor]: Updated node features with predictions. + """ + nodes = graph_nodes[_KEY] + updated = self.node_fn(nodes) + return {**graph_nodes, "pred": updated} + + +# pylint: disable=C0301 +class MoleculeGNS(nn.Cell): + r""" + Molecular graph neural network. + Implements flexible modular graph neural network for molecular property prediction based on message passing + with attention or other interaction mechanisms. Supports node and edge embeddings, multiple message passing + steps, and customizable interaction layers for complex molecular graphs. + + Args: + num_node_in_features (int): Number of input features per node. + num_node_out_features (int): Number of output features per node. + num_edge_in_features (int): Number of input features per edge. + latent_dim (int): Latent dimension for node and edge representations. + num_message_passing_steps (int): Number of message passing layers. + num_mlp_layers (int): Number of hidden layers in node and edge update MLPs. + mlp_hidden_dim (int): Hidden dimension size of MLPs. + node_feature_names (List[str]): List of node feature keys to use from input dictionary. + edge_feature_names (List[str]): List of edge feature keys to use from input dictionary. + use_embedding (bool, optional): Whether to use atomic number embedding for nodes. Default: ``True``. + interactions (str, optional): Type of interaction layer to use (e.g., ``"simple_attention"``). Default: ``"simple_attention"``. + interaction_params (Optional[Dict[str, Any]], optional): Parameters for interaction layer, e.g., cutoff, + polynomial order, gate type. Default: ``None``. + + Inputs: + - **edge_features** (dict) - Edge feature dictionary, must contain keys specified in `edge_feature_names`. + - **node_features** (dict) - Node feature dictionary, must contain keys specified in `node_feature_names`. + - **senders** (Tensor) - Sender node indices for each edge, shape :math:`(n_{edges},)`. + - **receivers** (Tensor) - Receiver node indices for each edge, shape :math:`(n_{edges},)`. + + Outputs: + - **edges** (dict) - Updated edge feature dictionary with key "feat" of shape :math:`(n_{edges}, latent\_dim)`. + - **nodes** (dict) - Updated node feature dictionary with key "feat" of shape :math:`(n_{nodes}, latent\_dim)`. + + Raises: + ValueError: If required feature keys are missing in `edge_features` or `node_features`. + ValueError: If `interactions` is not a supported type. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb.gns import MoleculeGNS + >>> gns_model = MoleculeGNS( + ... num_node_in_features=256, + ... num_node_out_features=3, + ... num_edge_in_features=23, + ... latent_dim=256, + ... interactions="simple_attention", + ... interaction_params={ + ... "distance_cutoff": True, + ... "polynomial_order": 4, + ... "cutoff_rmax": 6, + ... "attention_gate": "sigmoid", + ... }, + ... num_message_passing_steps=15, + ... num_mlp_layers=2, + ... mlp_hidden_dim=512, + ... use_embedding=True, + ... node_feature_names=["feat"], + ... edge_feature_names=["feat"], + ... ) + >>> n_atoms = 4 + >>> n_edges = 10 + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + ... "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + ... } + >>> edge_features = { + ... "vectors": Tensor(np.random.randn(n_edges, 3).astype(np.float32)), + ... "r": Tensor(np.abs(np.random.randn(n_edges).astype(np.float32) * 10)), + ... "feat": Tensor(np.random.randn(n_edges, 256).astype(np.float32)) + ... } + >>> senders = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> receivers = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> edges, nodes = gns_model( + ... edge_features, + ... node_features, + ... senders, + ... receivers, + ... ) + >>> print(edges["feat"].shape, nodes["feat"].shape) + (10, 256) (4, 256) + """ + + def __init__(self, + num_node_in_features: int, + num_node_out_features: int, + num_edge_in_features: int, + latent_dim: int, + num_message_passing_steps: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + node_feature_names: List[str], + edge_feature_names: List[str], + use_embedding: bool = True, + interactions: Literal["default", + "simple_attention"] = "simple_attention", + interaction_params: Optional[Dict[str, Any]] = None): + """init + """ + super().__init__() + self._encoder = Encoder( + num_node_in_features=num_node_in_features, + num_node_out_features=latent_dim, + num_edge_in_features=num_edge_in_features, + num_edge_out_features=latent_dim, + num_mlp_layers=num_mlp_layers, + mlp_hidden_dim=mlp_hidden_dim, + node_feature_names=node_feature_names, + edge_feature_names=edge_feature_names + ) + if interactions == "default": + InteractionNetworkClass = InteractionNetwork + elif interactions == "simple_attention": + InteractionNetworkClass = AttentionInteractionNetwork + self.num_message_passing_steps = num_message_passing_steps + if interaction_params is None: + interaction_params = {} + self.gnn_stacks = nn.CellList([ + InteractionNetworkClass( + num_node_in=latent_dim, + num_node_out=latent_dim, + num_edge_in=latent_dim, + num_edge_out=latent_dim, + num_mlp_layers=num_mlp_layers, + mlp_hidden_dim=mlp_hidden_dim, + **interaction_params + ) for _ in range(self.num_message_passing_steps) + ]) + self._decoder = Decoder( + num_node_in=latent_dim, + num_node_out=num_node_out_features, + num_mlp_layers=num_mlp_layers, + mlp_hidden_dim=mlp_hidden_dim + ) + self.rbf = partial(gaussian_basis_function, num_bases=20, radius=10.0) + self.use_embedding = use_embedding + if self.use_embedding: + self.atom_emb = AtomEmbedding(latent_dim, 118) + + def construct(self, edge_features, node_features, senders, receivers): + """construct + """ + edge_features = self.featurize_edges(edge_features) + node_features = self.featurize_nodes(node_features) + edges, nodes = self._encoder(node_features, edge_features) + for gnn in self.gnn_stacks: + edges, nodes = gnn(edges, nodes, senders, receivers) + nodes = self._decoder(nodes) + return edges, nodes + + def featurize_nodes(self, node_features): + """Featurize the nodes of a graph. + + Args: + node_features (Dict[str, Tensor]): Dictionary of node features. + + Returns: + Dict[str, Tensor]: Updated node features with atomic embeddings. + """ + one_hot_atomic = ops.OneHot()( + node_features["atomic_numbers"], 118, Tensor(1.0), Tensor(0.0) + ) + if self.use_embedding: + atomic_embedding = self.atom_emb(node_features["atomic_numbers"]) + else: + atomic_embedding = one_hot_atomic + + node_features = {**node_features, **{_KEY: atomic_embedding}} + return node_features + + def featurize_edges(self, edge_features): + """Featurize the edges of a graph. + + Args: + edge_features (Dict[str, Tensor]): Dictionary of edge features. + + Returns: + Dict[str, Tensor]: Updated edge features with radial basis functions and unit vectors. + """ + lengths = ops.norm(edge_features['vectors'], dim=1) + non_zero_divisor = ops.where( + lengths == 0, ops.ones_like(lengths), lengths) + unit_vectors = edge_features['vectors'] / ops.expand_dims(non_zero_divisor, 1) + rbfs = self.rbf(lengths) + edges = ops.cat([rbfs, unit_vectors], axis=1) + + edge_features = {**edge_features, **{_KEY: edges}} + return edge_features diff --git a/MindChemistry/mindchemistry/cell/orb/orb.py b/MindChemistry/mindchemistry/cell/orb/orb.py new file mode 100644 index 000000000..8afc55dbf --- /dev/null +++ b/MindChemistry/mindchemistry/cell/orb/orb.py @@ -0,0 +1,698 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Orb GraphRegressor.""" + +from typing import Literal, Optional, Union +import numpy + +import mindspore as ms +from mindspore import Parameter, ops, Tensor, mint + +from mindchemistry.cell.orb.gns import _KEY, MoleculeGNS +from mindchemistry.cell.orb.utils import ( + aggregate_nodes, + build_mlp, + REFERENCE_ENERGIES, +) + + +class LinearReferenceEnergy(ms.nn.Cell): + r""" + Linear reference energy (no bias term). + + This class implements a linear reference energy model that can be used + to compute the reference energy for a given set of atomic numbers. + + Args: + weight_init (numpy.ndarray, optional): Initial weights for the linear layer. + If not provided, the weights will be initialized randomly. + trainable (bool, optional): Whether the weights are trainable or not. + If not provided, the weights will be trainable by default. + + Inputs: + - **atom_types** (Tensor) - A tensor of atomic numbers of shape (n_atoms,). + - **n_node** (Tensor) - A tensor of shape (n_graphs,) containing the number of nodes in each graph. + + Outputs: + - **Tensor** - A tensor of shape (n_graphs, 1) containing the reference energy. + + Raises: + ValueError: If the input tensor shapes are not compatible with the expected shapes. + TypeError: If the input types are not compatible with the expected types. + + Supported Platforms: + ``Ascend`` + """ + def __init__( + self, + weight_init: Optional[numpy.ndarray] = None, + trainable: Optional[bool] = None, + ): + """init + """ + super().__init__() + + if trainable is None: + trainable = weight_init is None + + self.linear = ms.nn.Dense(118, 1, has_bias=False) + if weight_init is not None: + self.linear.weight.set_data(Tensor(weight_init, dtype=ms.float32).reshape(1, 118)) + if not trainable: + self.linear.weight.requires_grad = False + + def construct(self, atom_types: Tensor, n_node: Tensor): + """construct + """ + one_hot_atomic = ops.OneHot()(atom_types, 118, Tensor(1.0, ms.float32), Tensor(0.0, ms.float32)) + + reduced = aggregate_nodes(one_hot_atomic, n_node, reduction="sum") + return self.linear(reduced) + + +class ScalarNormalizer(ms.nn.Cell): + r""" + Scalar normalizer that learns mean and std from data. + + NOTE: Multi-dimensional tensors are flattened before updating + the running mean/std. This is desired behaviour for force targets. + + Args: + init_mean (Tensor or float, optional): Initial mean value for normalization. + If not provided, defaults to 0.0. + init_std (Tensor or float, optional): Initial standard deviation value for normalization. + If not provided, defaults to 1.0. + init_num_batches (int, optional): Initial number of batches for normalization. + If not provided, defaults to 1000. + + Inputs: + - **x** (Tensor) - A tensor of shape (n_samples, n_features) to normalize. + + Outputs: + - **Tensor** - A tensor of the same shape as x, normalized by the running mean and std. + + Raises: + ValueError: If the input tensor is not of the expected shape. + TypeError: If the input types are not compatible with the expected types. + + Supported Platforms: + ``Ascend`` + """ + def __init__( + self, + init_mean: Optional[Union[Tensor, float]] = None, + init_std: Optional[Union[Tensor, float]] = None, + init_num_batches: Optional[int] = 1000, + ): + """init + """ + super().__init__() + self.bn = mint.nn.BatchNorm1d(1, affine=False, momentum=None) + self.bn.running_mean = Parameter(Tensor([0], ms.float32)) + self.bn.running_var = Parameter(Tensor([1], ms.float32)) + self.bn.num_batches_tracked = Parameter(Tensor([1000], ms.float32)) + self.stastics = { + "running_mean": init_mean if init_mean is not None else 0.0, + "running_var": init_std**2 if init_std is not None else 1.0, + "num_batches_tracked": init_num_batches if init_num_batches is not None else 1000, + } + + def construct(self, x: Tensor): + """construct + """ + if self.training: + self.bn(x.view(-1, 1)) + if hasattr(self, "running_mean"): + return (x - self.running_mean) / mint.sqrt(self.running_var) + return (x - self.bn.running_mean) / mint.sqrt(self.bn.running_var) + + def inverse(self, x: Tensor): + """Reverse the construct normalization. + + Args: + x: A tensor of shape (n_samples, n_features) to inverse normalize. + + Returns: + A tensor of the same shape as x, inverse normalized by the running mean and std. + """ + if hasattr(self, "running_mean"): + return x * mint.sqrt(self.running_var) + self.running_mean + return x * mint.sqrt(self.bn.running_var) + self.bn.running_mean + + +# pylint: disable=C0301 +class NodeHead(ms.nn.Cell): + r""" + Node-level prediction head. + + Implements neural network head for predicting node-level properties from node features. This head can be + added to base models to enable auxiliary tasks during pretraining or added in fine-tuning steps. + + Args: + latent_dim (int): Input feature dimension for each node. + num_mlp_layers (int): Number of hidden layers in MLP. + mlp_hidden_dim (int): Hidden dimension size of MLP. + target_property_dim (int): Output dimension of node-level target property. + dropout (Optional[float], optional): Dropout rate for MLP. Default: ``None``. + remove_mean (bool, optional): If True, remove mean from output, typically used for force prediction. + Default: ``True``. + + Inputs: + - **node_features** (dict) - Node feature dictionary, must contain key "feat" with shape :math:`(n_{nodes}, latent\_dim)`. + - **n_node** (Tensor) - Number of nodes in graph, shape :math:`(1,)`. + + Outputs: + - **output** (dict) - Dictionary containing key "node_pred" with value of shape :math:`(n_{nodes}, target\_property\_dim)`. + + Raises: + ValueError: If required feature keys are missing in `node_features`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb.gns import NodeHead + >>> node_head = NodeHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=3, + ... remove_mean=True, + ... ) + >>> n_atoms = 4 + >>> n_node = Tensor([n_atoms], mindspore.int32) + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + ... "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + ... } + >>> output = node_head(node_features, n_node) + >>> print(output['node_pred'].shape) + (4, 3) + """ + def __init__( + self, + latent_dim: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + target_property_dim: int, + dropout: Optional[float] = None, + remove_mean: bool = True, + ): + """init + """ + super().__init__() + self.target_property_dim = target_property_dim + self.normalizer = ScalarNormalizer() + + self.mlp = build_mlp( + input_size=latent_dim, + hidden_layer_sizes=[mlp_hidden_dim] * num_mlp_layers, + output_size=self.target_property_dim, + dropout=dropout, + ) + + self.remove_mean = remove_mean + + def construct(self, node_features, n_node): + """construct + """ + feat = node_features[_KEY] + pred = self.mlp(feat) + if self.remove_mean: + system_means = aggregate_nodes( + pred, n_node, reduction="mean" + ) + node_broadcasted_means = mint.repeat_interleave( + system_means, n_node, dim=0 + ) + pred = pred - node_broadcasted_means + res = {"node_pred": pred} + return res + + def predict(self, node_features, n_node): + """Predict node-level attributes. + + Args: + node_features: Node features tensor of shape (n_nodes, latent_dim). + n_node: Number of nodes in the graph. + + Returns: + node_pred: Node-level predictions of shape (n_nodes, target_property_dim). + """ + out = self(node_features, n_node) + pred = out["node_pred"] + return self.normalizer.inverse(pred) + + +# pylint: disable=C0301 +class GraphHead(ms.nn.Cell): + r""" + Graph-level prediction head. Implements graph-level prediction head that can be attached to base models + for predicting graph-level properties (e.g., stress tensor) from node features using aggregation and MLP. + + Args: + latent_dim (int): Input feature dimension for each node. + num_mlp_layers (int): Number of hidden layers in MLP. + mlp_hidden_dim (int): Hidden dimension size of MLP. + target_property_dim (int): Output dimension of graph-level property. + node_aggregation (str, optional): Aggregation method for node predictions, e.g., ``"mean"`` or ``"sum"``. Default: ``"mean"``. + dropout (Optional[float], optional): Dropout rate for MLP. Default: ``None``. + compute_stress (bool, optional): Whether to compute and output stress tensor. Default: ``False``. + + Inputs: + - **node_features** (dict) - Node feature dictionary, must contain key "feat" with shape :math:`(n_{nodes}, latent\_dim)`. + - **n_node** (Tensor) - Number of nodes in graph, shape :math:`(1,)`. + + Outputs: + - **output** (dict) - Dictionary containing key "stress_pred" with value of shape :math:`(1, target\_property\_dim)`. + + Raises: + ValueError: If required feature keys are missing in `node_features`. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb.gns import GraphHead + >>> graph_head = GraphHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=6, + ... compute_stress=True, + ... ) + >>> n_atoms = 4 + >>> n_node = Tensor([n_atoms], mindspore.int32) + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + ... "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + ... } + >>> output = graph_head(node_features, n_node) + >>> print(output['stress_pred'].shape) + (1, 6) + """ + + def __init__( + self, + latent_dim: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + target_property_dim: int, + node_aggregation: Literal["sum", "mean"] = "mean", + dropout: Optional[float] = None, + compute_stress: Optional[bool] = False, + ): + """init + """ + super().__init__() + self.target_property_dim = target_property_dim + self.normalizer = ScalarNormalizer() + + self.node_aggregation = node_aggregation + self.mlp = build_mlp( + input_size=latent_dim, + hidden_layer_sizes=[mlp_hidden_dim] * num_mlp_layers, + output_size=self.target_property_dim, + dropout=dropout, + ) + self.output_activation = ops.Identity() + self.compute_stress = compute_stress + + def construct(self, node_features, n_node): + """construct + """ + feat = node_features[_KEY] + + # aggregate to get a tensor of shape (num_graphs, latent_dim) + mlp_input = aggregate_nodes( + feat, + n_node, + reduction=self.node_aggregation, + ) + + pred = self.mlp(mlp_input) + if self.compute_stress: + # name the stress prediction differently + res = {"stress_pred": pred} + else: + res = {"graph_pred": pred} + return res + + def predict(self, node_features, n_node, atomic_numbers=None): + """Predict graph-level attributes. + + Args: + node_features: Node features tensor + n_node: Number of nodes + atomic_numbers: Optional atomic numbers for reference energy calculation + + Returns: + probs: Graph-level predictions of shape (n_graphs, target_property_dim). + If compute_stress is True, this will be the stress tensor. + If compute_stress is False, this will be the graph-level property (e.g., energy). + """ + pred = self(node_features, n_node) + if self.compute_stress: + logits = pred["stress_pred"].squeeze(-1) + else: + assert atomic_numbers is not None, "atomic_numbers must be provided for graph prediction" + logits = pred["graph_pred"].squeeze(-1) + probs = self.output_activation(logits) + probs = self.normalizer.inverse(probs) + return probs + + +# pylint: disable=C0301 +class EnergyHead(GraphHead): + r""" + Graph-level energy prediction head. + Implements neural network head for predicting total energy or per-atom average energy of molecular graphs. + Supports node-level aggregation, reference energy offset, and flexible output modes. + + Args: + latent_dim (int): Input feature dimension for each node. + num_mlp_layers (int): Number of hidden layers in MLP. + mlp_hidden_dim (int): Hidden dimension size of MLP. + target_property_dim (int): Output dimension of energy property (typically 1). + predict_atom_avg (bool, optional): Whether to predict per-atom average energy instead of total energy. Default: ``True``. + reference_energy_name (str, optional): Reference energy name for offset, e.g., ``"vasp-shifted"``. Default: ``"mp-traj-d3"``. + train_reference (bool, optional): Whether to train reference energy as learnable parameter. Default: ``False``. + dropout (Optional[float], optional): Dropout rate for MLP. Default: ``None``. + node_aggregation (str, optional): Aggregation method for node predictions, e.g., ``"mean"`` or ``"sum"``. Default: ``None``. + + Inputs: + - **node_features** (dict) - Node feature dictionary, must contain key "feat" with shape :math:`(n_{nodes}, latent\_dim)`. + - **n_node** (Tensor) - Number of nodes in graph, shape :math:`(1,)`. + + Outputs: + - **output** (dict) - Dictionary containing key "graph_pred" with value of shape :math:`(1, target\_property\_dim)`. + + Raises: + ValueError: If required feature keys are missing in `node_features`. + ValueError: If `node_aggregation` is not a supported type. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb.gns import EnergyHead + >>> energy_head = EnergyHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=1, + ... node_aggregation="mean", + ... reference_energy_name="vasp-shifted", + ... train_reference=True, + ... predict_atom_avg=True, + ... ) + >>> n_atoms = 4 + >>> n_node = Tensor([n_atoms], mindspore.int32) + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + ... "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + ... } + >>> output = energy_head(node_features, n_node) + >>> print(output['graph_pred'].shape) + (1, 1) + """ + + def __init__( + self, + latent_dim: int, + num_mlp_layers: int, + mlp_hidden_dim: int, + target_property_dim: int, + predict_atom_avg: bool = True, + reference_energy_name: str = "mp-traj-d3", + train_reference: bool = False, + dropout: Optional[float] = None, + node_aggregation: Optional[str] = "mean", + ): + """init + """ + ref = REFERENCE_ENERGIES[reference_energy_name] + + super().__init__( + latent_dim=latent_dim, + num_mlp_layers=num_mlp_layers, + mlp_hidden_dim=mlp_hidden_dim, + target_property_dim=target_property_dim, + node_aggregation=node_aggregation, + dropout=dropout, + ) + self.reference = LinearReferenceEnergy( + weight_init=ref.coefficients, trainable=train_reference + ) + self.atom_avg = predict_atom_avg + + def predict(self, node_features, n_node, atomic_numbers=None): + """Predict energy. + + Args: + node_features: Node features tensor + n_node: Number of nodes + atomic_numbers: Optional atomic numbers for reference energy calculation + + Returns: + graph_pred: Energy prediction + """ + if atomic_numbers is None: + raise ValueError("atomic_numbers is required for energy prediction") + + pred = self(node_features, n_node)["graph_pred"] + pred = self.normalizer.inverse(pred).squeeze(-1) + if self.atom_avg: + pred = pred * n_node + pred = pred + self.reference(atomic_numbers, n_node) + return pred + + +# pylint: disable=C0301 +class Orb(ms.nn.Cell): + r""" + Orb graph regressor. + Combines a pretrained base model (e.g., MoleculeGNS) with optional node, graph, and stress regression heads, supporting + fine-tuning or feature extraction workflows. + + Args: + model (MoleculeGNS): Pretrained or randomly initialized base model for message passing and feature extraction. + node_head (NodeHead, optional): Regression head for node-level property prediction. Default: ``None``. + graph_head (GraphHead, optional): Regression head for graph-level property prediction (e.g., energy). Default: ``None``. + stress_head (GraphHead, optional): Regression head for stress prediction. Default: ``None``. + model_requires_grad (bool, optional): Whether to fine-tune the base model (True) or freeze its parameters (False). Default: ``True``. + cutoff_layers (int, optional): If provided, only use the first ``cutoff_layers`` message passing layers of the base model. + Default: ``None``. + + Inputs: + - **edge_features** (dict) - Edge feature dictionary (e.g., `{"vectors": Tensor, "r": Tensor}`). + - **node_features** (dict) - Node feature dictionary (e.g., `{"atomic_numbers": Tensor, ...}`). + - **senders** (Tensor) - Sender node indices for each edge. Shape: :math:`(n_{edges},)`. + - **receivers** (Tensor) - Receiver node indices for each edge. Shape: :math:`(n_{edges},)`. + - **n_node** (Tensor) - Number of nodes for each graph in the batch. Shape: :math:`(n_{graphs},)`. + + Outputs: + - **output** (dict) - Dictionary containing: + - **edges** (dict) - Edge features after message passing, e.g., `{..., "feat": Tensor}`. + - **nodes** (dict) - Node features after message passing, e.g., `{..., "feat": Tensor}`. + - **graph_pred** (Tensor) - Graph-level predictions, e.g., energy. Shape: :math:`(n_{graphs}, target\_property\_dim)`. + - **node_pred** (Tensor) - Node-level predictions. Shape: :math:`(n_{nodes}, target\_property\_dim)`. + - **stress_pred** (Tensor) - Stress predictions (if stress_head is provided). Shape: :math:`(n_{graphs}, 6)`. + + Raises: + ValueError: If neither node_head nor graph_head is provided. + ValueError: If cutoff_layers exceeds the number of message passing steps in the base model. + ValueError: If atomic_numbers is not provided when graph_head is required. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> from mindspore import Tensor + >>> from mindchemistry.cell.orb import Orb, MoleculeGNS, EnergyHead, NodeHead, GraphHead + >>> Orb = Orb( + ... model=MoleculeGNS( + ... num_node_in_features=256, + ... num_node_out_features=3, + ... num_edge_in_features=23, + ... latent_dim=256, + ... interactions="simple_attention", + ... interaction_params={ + ... "distance_cutoff": True, + ... "polynomial_order": 4, + ... "cutoff_rmax": 6, + ... "attention_gate": "sigmoid", + ... }, + ... num_message_passing_steps=15, + ... num_mlp_layers=2, + ... mlp_hidden_dim=512, + ... use_embedding=True, + ... node_feature_names=["feat"], + ... edge_feature_names=["feat"], + ... ), + ... graph_head=EnergyHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=1, + ... node_aggregation="mean", + ... reference_energy_name="vasp-shifted", + ... train_reference=True, + ... predict_atom_avg=True, + ... ), + ... node_head=NodeHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=3, + ... remove_mean=True, + ... ), + ... stress_head=GraphHead( + ... latent_dim=256, + ... num_mlp_layers=1, + ... mlp_hidden_dim=256, + ... target_property_dim=6, + ... compute_stress=True, + ... ), + ... ) + >>> n_atoms = 4 + >>> n_edges = 10 + >>> n_node = Tensor([n_atoms], mindspore.int32) + >>> atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + >>> atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + >>> for i, num in enumerate(atomic_numbers.asnumpy()): + ... atomic_numbers_embedding_np[i, num - 1] = 1.0 + >>> node_features = { + ... "atomic_numbers": atomic_numbers, + ... "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + ... "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)) + ... } + >>> edge_features = { + ... "vectors": Tensor(np.random.randn(n_edges, 3).astype(np.float32)), + ... "r": Tensor(np.abs(np.random.randn(n_edges).astype(np.float32) * 10)) + ... } + >>> senders = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> receivers = Tensor(np.random.randint(0, n_atoms, size=(n_edges,), dtype=np.int32)) + >>> output = Orb(edge_features, node_features, senders, receivers, n_node) + >>> print(output['graph_pred'].shape, output['node_pred'].shape, output['stress_pred'].shape) + (1, 1) (4, 3) (1, 6) + """ + + def __init__( + self, + model: MoleculeGNS, + node_head: Optional[NodeHead] = None, + graph_head: Optional[GraphHead] = None, + stress_head: Optional[GraphHead] = None, + model_requires_grad: bool = True, + cutoff_layers: Optional[int] = None, + ): + """init + """ + super().__init__() + + if (node_head is None) and (graph_head is None): + raise ValueError("Must provide at least one node/graph head.") + self.node_head = node_head + self.graph_head = graph_head + self.stress_head = stress_head + self.cutoff_layers = cutoff_layers + + self.model = model + + if self.cutoff_layers is not None: + if self.cutoff_layers > self.model.num_message_passing_steps: + raise ValueError( + f"cutoff_layers ({self.cutoff_layers}) must be less than or equal to" + f" the number of message passing steps ({self.model.num_message_passing_steps})" + ) + self.model.gnn_stacks = self.model.gnn_stacks[: self.cutoff_layers] + self.model.num_message_passing_steps = self.cutoff_layers + + self.model_requires_grad = model_requires_grad + + if not model_requires_grad: + for param in self.model.parameters(): + param.requires_grad = False + + + def predict(self, edge_features, node_features, senders, receivers, n_node, atomic_numbers): + """Predict node and/or graph level attributes. + + Args: + edge_features: A dictionary, e.g., `{"vectors": Tensor, "r": Tensor}`. + node_features: A dictionary, e.g., `{"atomic_numbers": Tensor, "positions": Tensor, + "atomic_numbers_embedding": Tensor}`. + senders: A tensor of shape (n_edges,) containing the sender node indices. + receivers: A tensor of shape (n_edges,) containing the receiver node indices. + n_node: A tensor of shape (1,) containing the number of nodes. + atomic_numbers: A tensor of atomic numbers for reference energy calculation. + + Returns: + ouput_dict: A dictionary containing the predictions: + - `graph_pred`: Graph-level predictions (e.g., energy) of shape (n_graphs, graph_property_dim). + - `stress_pred`: Stress predictions (if stress_head is provided) of shape (n_graphs, stress_dim). + - `node_pred`: Node-level predictions of shape (n_nodes, node_property_dim). + """ + _, nodes = self.model(edge_features, node_features, senders, receivers) + + output = {} + output["graph_pred"] = self.graph_head.predict(nodes, n_node, atomic_numbers) + output["stress_pred"] = self.stress_head.predict(nodes, n_node) + output["node_pred"] = self.node_head.predict(nodes, n_node) + + return output + + def construct(self, edge_features, node_features, senders, receivers, n_node): + """construct + """ + edges, nodes = self.model(edge_features, node_features, senders, receivers) + + res = {"edges": edges, "nodes": nodes} + res.update(self.graph_head(nodes, n_node)) + res.update(self.stress_head(nodes, n_node)) + res.update(self.node_head(nodes, n_node)) + + return res diff --git a/MindChemistry/mindchemistry/cell/orb/utils.py b/MindChemistry/mindchemistry/cell/orb/utils.py new file mode 100644 index 000000000..26797d9df --- /dev/null +++ b/MindChemistry/mindchemistry/cell/orb/utils.py @@ -0,0 +1,737 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Utils.""" + +from typing import NamedTuple, List, Optional, Type + +import numpy as np +import mindspore as ms +from mindspore import nn, ops, Tensor, mint, context + +MSINT = [ms.int64, ms.int32, ms.int16, ms.int8, ms.uint8] + + +def aggregate_nodes(tensor: Tensor, n_node: Tensor, reduction: str = "mean", deterministic: bool = False) -> Tensor: + """Aggregates over a tensor based on graph sizes.""" + count = len(n_node) + if deterministic: + ms.set_seed(1) + segments = ops.arange(count).repeat_interleave(n_node).astype(ms.int32) + if reduction == "sum": + return scatter_sum(tensor, segments, dim=0) + if reduction == "mean": + return scatter_mean(tensor, segments, dim=0) + if reduction == "max": + return scatter_max(tensor, segments, dim=0) + raise ValueError("Invalid reduction argument. Use sum, mean or max.") + + +def segment_sum(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based sum over segments of a tensor.""" + return scatter_sum(data, segment_ids, dim=0, dim_size=num_segments) + + +def segment_max(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based max over segments of a tensor.""" + assert segment_ids is not None, "segment_ids must not be None" + assert num_segments > 0, "num_segments must be greater than 0" + max_op = ops.ArgMaxWithValue(axis=0) + _, max_values = max_op(data) + return max_values + + +def segment_mean(data: Tensor, segment_ids: Tensor, num_segments: int): + """Computes index based mean over segments of a tensor.""" + sum_v = segment_sum(data, segment_ids, num_segments) + count = ops.scatter_add(ops.zeros( + (num_segments,), dtype=ms.int32), segment_ids, ops.ones_like(segment_ids)) + return sum_v / count.astype(sum_v.dtype) + + +def segment_softmax(data: Tensor, segment_ids: Tensor, num_segments: int, weights: Optional[Tensor] = None): + """Computes a softmax over segments of the tensor.""" + data_max = segment_max(data, segment_ids, num_segments) + data = data - data_max[segment_ids] + + unnormalised_probs = ops.exp(data) + if weights is not None: + unnormalised_probs = unnormalised_probs * weights + denominator = segment_sum(unnormalised_probs, segment_ids, num_segments) + + return safe_division(unnormalised_probs, denominator, segment_ids) + + +def safe_division(numerator: Tensor, denominator: Tensor, segment_ids: Tensor): + """Divides logits by denominator, setting 0 where the denominator is zero.""" + result = ops.where(denominator[segment_ids] == + 0, 0, numerator / denominator[segment_ids]) + return result + + +def _broadcast(src: Tensor, other: Tensor, dim: int): + """Broadcasts the source tensor to match the shape of the other tensor along the specified dimension.""" + if dim < 0: + dim = other.ndim + dim + if src.ndim == 1: + for _ in range(0, dim): + src = src.unsqueeze(0) + for _ in range(src.ndim, other.ndim): + src = src.unsqueeze(-1) + src = src.expand_as(other) + return src + + +def scatter_sum( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None, reduce: str = "sum" +) -> Tensor: + """Applies a sum reduction of the orb_models tensor along the specified dimension.""" + assert reduce == "sum" + index = _broadcast(index, src, dim) + if out is None: + size = list(src.shape) + if dim_size is not None: + size[dim] = dim_size + elif index.numel() == 0: + size[dim] = 0 + else: + size[dim] = int(index.max()) + 1 + out = ops.zeros(size, dtype=src.dtype) + return mint.scatter_add(out, dim, index, src) + return mint.scatter_add(out, dim, index, src) + + +def scatter_std( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None, unbiased: bool = True +) -> Tensor: + """Computes the standard deviation of the orb_models tensor along the specified dimension.""" + if out is not None: + dim_size = out.shape[dim] + + if dim < 0: + dim = src.ndim + dim + + count_dim = dim + if index.ndim <= dim: + count_dim = index.ndim - 1 + + ones = ops.ones(index.shape, dtype=src.dtype) + count = scatter_sum(ones, index, count_dim, dim_size=dim_size) + + index = _broadcast(index, src, dim) + tmp = scatter_sum(src, index, dim, dim_size=dim_size) + count = _broadcast(count, tmp, dim).clip(1) + mean = tmp / count + + var = src - mean.gather(dim, index) + var = var * var + out = scatter_sum(var, index, dim, out=out, dim_size=dim_size) + + if unbiased: + count = count - 1 + count = count.clip(1) + out = out / (count + 1e-6) + out = ops.sqrt(out) + return out + + +def scatter_mean( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None +) -> Tensor: + """Computes the mean of the orb_models tensor along the specified dimension.""" + out = scatter_sum(src, index, dim, out=out, dim_size=dim_size) + dim_size = out.shape[dim] + + index_dim = dim + if index_dim < 0: + index_dim = index_dim + src.ndim + if index.ndim <= index_dim: + index_dim = index.ndim - 1 + + ones = ops.ones(index.shape, dtype=src.dtype) + count = scatter_sum(ones, index, index_dim, dim_size=dim_size) + count = count.clip(1) + count = _broadcast(count, out, dim) + out = out / count + return out + + +def scatter_max( + src: Tensor, index: Tensor, dim: int = -1, out: Optional[Tensor] = None, + dim_size: Optional[int] = None +) -> Tensor: + """Computes the maximum of the orb_models tensor for each group defined by index along the specified dimension.""" + if out is not None: + raise NotImplementedError( + "The 'out' argument is not supported for scatter_max") + + if src.dtype in MSINT: + init_value = np.iinfo(src.dtype).min + else: + init_value = np.finfo(src.dtype).min + + if dim < 0: + dim = src.ndim + dim + + if dim_size is None: + dim_size = int(index.max()) + 1 + + result = ops.ones( + (dim_size, *src.shape[:dim], *src.shape[dim + 1:]), dtype=src.dtype) + result = init_value * result + broadcasted_index = _broadcast(index, src, dim) + + scatter_result = ops.ZerosLike()(result) + index = ops.expand_dims(broadcasted_index, dim) + scatter_result = scatter_result.scatter_update(index, src) + result = ops.Maximum()(result, scatter_result) + return result + + +class SSP(nn.Cell): + """Shifted Softplus activation function. + + This activation is twice differentiable so can be used when regressing + gradients for conservative force fields. + """ + + def __init__(self, beta: int = 1, threshold: int = 20): + super().__init__() + self.beta = beta + self.threshold = threshold + + def construct(self, input_x: Tensor) -> Tensor: + sp0 = ops.softplus(ops.zeros(1), self.beta, self.threshold) + return ops.softplus(input_x, self.beta, self.threshold) - sp0 + + +def build_mlp( + input_size: int, + hidden_layer_sizes: List[int], + output_size: Optional[int] = None, + output_activation: Type[nn.Cell] = nn.Identity, + activation: Type[nn.Cell] = SSP, + dropout: Optional[float] = None, +) -> nn.Cell: + """Build a MultiLayer Perceptron. + + Args: + input_size: Size of input layer. + hidden_layer_sizes: An array of input size for each hidden layer. + output_size: Size of the output layer. + output_activation: Activation function for the output layer. + activation: Activation function for the hidden layers. + dropout: Dropout rate for hidden layers. + checkpoint: Whether to use checkpointing. + + Returns: + mlp: An MLP sequential container. + """ + # Size of each layer + layer_sizes = [input_size] + hidden_layer_sizes + if output_size: + layer_sizes.append(output_size) + + # Number of layers + nlayers = len(layer_sizes) - 1 + + # Create a list of activation functions and + # set the last element to output activation function + act = [activation for _ in range(nlayers)] + act[-1] = output_activation + + # Create a list to hold layers + layers = [] + for i in range(nlayers): + if dropout is not None: + layers.append(nn.Dropout(keep_prob=1 - dropout)) + layers.append(nn.Dense(layer_sizes[i], layer_sizes[i + 1])) + layers.append(act[i]()) + + # Create a sequential container + mlp = nn.SequentialCell(layers) + return mlp + + +class CheckpointedSequential(nn.Cell): + """Sequential container with checkpointing.""" + + def __init__(self, *args, n_layers: int = 1): + super().__init__() + self.n_layers = n_layers + self.layers = nn.CellList(list(args)) + + def construct(self, input_x: Tensor) -> Tensor: + """Forward pass with checkpointing enabled in training mode.""" + if context.get_context("mode") == context.GRAPH_MODE: + # In graph mode, checkpointing is handled by MindSpore's graph optimization + for layer in self.layers: + input_x = layer(input_x) + else: + # In PyNative mode, we can manually checkpoint each layer + for i in range(self.n_layers): + input_x = self.layers[i](input_x) + return input_x + + +class ReferenceEnergies(NamedTuple): + """ + Reference energies for an atomic system. + + Our vasp reference energies are computed by running vasp + optimisations on a single atom of each atom-type. + + Other reference energies are fitted using least-squares. + + Doing so with mp-traj-d3 gives the following: + + ---------- LSTQ ---------- + Reference MAE: 13.35608855004781 + (energy - ref) mean: 1.3931169304958624 + (energy - ref) std: 22.45615276341948 + (energy - ref)/natoms mean: 0.16737045963056316 + (energy - ref)/natoms std: 0.8189314920219992 + CO2: Predicted vs DFT: -23.154158610392408 vs -22.97 + H2O: Predicted vs DFT: -11.020918107591324 vs - 14.23 + ---------- VASP ---------- + Reference MAE: 152.4722089438871 + (energy - ref) mean: -152.47090833346033 + (energy - ref) std: 153.89049784836962 + (energy - ref)/natoms mean: -4.734136414817941 + (energy - ref)/natoms std: 1.3603868419157275 + CO2: Predicted vs DFT: -4.35888857 vs -22.97 + H2O: Predicted vs DFT: -2.66521147 vs - 14.23 + ---------- Shifted VASP ---------- + Reference MAE: 28.95948216608197 + (energy - ref) mean: 0.7083632520428979 + (energy - ref) std: 48.61861182844561 + (energy - ref)/natoms mean: 0.17320099403091083 + (energy - ref)/natoms std: 1.3603868419157275 + CO2: Predicted vs DFT: -19.080900796546562 vs -22.97 + H2O: Predicted vs DFT: -12.479886287697706 vs - 14.23 + + Args: + coefficients: Coefficients for each atom in the periodic table. + Must be of length 118 with first entry equal to 0. + residual_mean: Mean of (pred - target) + residual_std: Standard deviation of (pred - target) + residual_mean_per_atom: Mean of (pred - target)/n_atoms. + residual_std_per_atom: Standard deviation of (pred - target)/n_atoms. + """ + + coefficients: np.ndarray + residual_mean: float + residual_std: float + residual_mean_per_atom: float + residual_std_per_atom: float + + +# We have only computed these for the first +# 88 elements, and padded the remainder with 0. +vasp_reference_energies = ReferenceEnergies( + coefficients=np.array( + [ + 0.0, # padding + -1.11725225e00, + 7.69290000e-04, + -3.22788480e-01, + -4.47021900e-02, + -2.90627280e-01, + -1.26297013e00, + -3.12415058e00, + -1.54795922e00, + -4.39757050e-01, + -1.25673900e-02, + -2.63927430e-01, + -1.92670300e-02, + -2.11267040e-01, + -8.24799500e-01, + -1.88734631e00, + -8.91048980e-01, + -2.58371430e-01, + -2.50008000e-02, + -2.71936150e-01, + -7.11147600e-02, + -2.06076796e00, + -2.42753196e00, + -3.57144559e00, + -5.45540047e00, + -5.15708214e00, + -3.31393675e00, + -1.84639284e00, + -6.32812480e-01, + -2.38017450e-01, + -1.41047600e-02, + -2.06349980e-01, + -7.77477960e-01, + -1.70160351e00, + -7.84231510e-01, + -2.27541260e-01, + -2.26104900e-02, + -2.79760570e-01, + -9.92851900e-02, + -2.18560872e00, + -2.26603086e00, + -3.14842282e00, + -4.61199158e00, + -3.34329762e00, + -2.48233722e00, + -1.27872811e00, + -1.47784242e00, + -2.04068960e-01, + -1.89639300e-02, + -1.88520140e-01, + -6.76700640e-01, + -1.42966694e00, + -6.57608340e-01, + -1.89308030e-01, + -1.20491300e-02, + -3.07991050e-01, + -1.58601400e-01, + -4.89728600e-01, + -1.35031403e00, + -3.31509450e-01, + -3.23660410e-01, + -3.15316610e-01, + -3.11184530e-01, + -8.44684689e00, + -1.04408371e01, + -2.30922790e-01, + -2.26295040e-01, + -2.92747580e-01, + -2.92191740e-01, + -2.91465170e-01, + -3.80611000e-02, + -2.87691040e-01, + -3.51528971e00, + -3.51343142e00, + -4.64232388e00, + -2.88816624e00, + -1.46089612e00, + -5.36042350e-01, + -1.87182020e-01, + -1.33549100e-02, + -1.68142250e-01, + -6.25378750e-01, + -1.32291753e00, + -3.26246040e-01, + -1.10239294e00, + -2.30839543e00, + -4.61968511e00, + -7.30638139e00, + -1.04613411e01, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + 0.00000000e00, + ] + ), + residual_mean=-152.47090833346033, + residual_std=153.89049784836962, + residual_mean_per_atom=-4.734136414817941, + residual_std_per_atom=1.3603868419157275, +) + +vasp_shifted_reference_energies = ReferenceEnergies( + coefficients=np.array( + [ + 0.0, # padding + -6.0245896588488534, + -4.9065681188488535, + -5.230125888848853, + -4.952039598848853, + -5.197964688848853, + -6.170307538848854, + -8.031487988848854, + -6.455296628848854, + -5.347094458848853, + -4.919904798848854, + -5.171264838848853, + -4.9266044388488535, + -5.118604448848854, + -5.732136908848854, + -6.794683718848853, + -5.798386388848853, + -5.165708838848854, + -4.932338208848853, + -5.179273558848854, + -4.978452168848854, + -6.968105368848853, + -7.334869368848853, + -8.478782998848853, + -10.362737878848854, + -10.064419548848853, + -8.221274158848853, + -6.7537302488488535, + -5.540149888848854, + -5.145354858848854, + -4.921442168848854, + -5.113687388848853, + -5.684815368848853, + -6.6089409188488535, + -5.691568918848853, + -5.134878668848853, + -4.929947898848853, + -5.187097978848853, + -5.006622598848853, + -7.092946128848853, + -7.173368268848853, + -8.055760228848854, + -9.519328988848853, + -8.250635028848853, + -7.389674628848853, + -6.186065518848854, + -6.3851798288488535, + -5.111406368848853, + -4.9263013388488535, + -5.095857548848853, + -5.5840380488488535, + -6.337004348848853, + -5.564945748848854, + -5.096645438848854, + -4.919386538848854, + -5.2153284588488535, + -5.065938808848854, + -5.397066008848854, + -6.257651438848853, + -5.238846858848854, + -5.230997818848854, + -5.2226540188488535, + -5.218521938848854, + -13.354184298848853, + -15.348174508848853, + -5.138260198848854, + -5.133632448848854, + -5.200084988848854, + -5.199529148848853, + -5.198802578848854, + -4.945398508848854, + -5.195028448848854, + -8.422627118848853, + -8.420768828848853, + -9.549661288848853, + -7.795503648848854, + -6.368233528848854, + -5.443379758848853, + -5.094519428848853, + -4.920692318848854, + -5.075479658848853, + -5.532716158848854, + -6.230254938848853, + -5.2335834488488535, + -6.009730348848853, + -7.2157328388488535, + -9.527022518848852, + -12.213718798848854, + -15.368678508848854, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + -4.9073374088488535, + ] + ), + residual_mean=0.7083632520428979, + residual_std=48.61861182844561, + residual_mean_per_atom=0.17320099403091083, + residual_std_per_atom=1.3603868419157275, +) + +mp_traj_d3_reference_energies = ReferenceEnergies( + coefficients=np.array( + [ + 0.0, # padding + -3.6818229500085327, + -1.3199148098871394, + -3.688797198716366, + -4.938608191337134, + -7.901604711660046, + -8.475968295226822, + -7.42601366967988, + -7.339095157582792, + -4.9239197309790725, + -0.061236726924086424, + -3.0526401941340806, + -3.0836199809602105, + -5.055909838526647, + -7.875649504560413, + -7.175538036602013, + -4.814514763424572, + -2.9198, + -0.13127266880110078, + -2.8792125576832865, + -5.635016298424046, + -8.164720105254204, + -10.712143655281858, + -9.00292017736733, + -9.619640942931085, + -8.610981088341331, + -7.3506162257219385, + -5.943664565392655, + -5.592846831852426, + -3.6868017794232077, + -1.579885044321145, + -3.744040760877656, + -4.945137332817033, + -4.2021571924020655, + -4.045303645442562, + -2.652667661940346, + 6.497305115069106, + -2.806819346028444, + -5.164089337915934, + -10.493037547114369, + -12.256967896681578, + -12.642602087796805, + -9.20874164629371, + -9.292405362859506, + -8.304141715175632, + -7.49355696426791, + -5.44150554776011, + -2.5621691409635474, + -0.9687174918829102, + -3.055905969721681, + -4.02975498585447, + -3.847125028451477, + -3.1016305514702203, + -1.8001556831915142, + 9.742275211909387, + -3.045410331644577, + -5.204088972093178, + -9.267561428901118, + -9.031458669303145, + -8.345252241333469, + -8.584977779192705, + -7.955970517402418, + -8.519743221802353, + -13.927799873369949, + -19.12242499580686, + -8.156787154342183, + -8.505944162624234, + -8.015433843487497, + -7.129355408977684, + -8.166165621829014, + -3.9995952334750644, + -7.884852034766514, + -13.281575162667238, + -14.598283494757041, + -9.729591400065184, + -11.798570715867179, + -9.878207068760076, + -7.891075131963705, + -5.964524120587406, + -2.9665634245721275, + -0.10530075207060852, + -2.649420791761001, + -4.00193074336809, + -3.7403644338639785, + -1.5543122344752192e-15, + -8.881784197001252e-16, + -8.881784197001252e-16, + 0.0, + 0.0, + -5.480602125607218, + -11.9439263006771, + -12.974770001312883, + -14.376719109855834, + -15.49262474740642, + -16.02533150334938, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ] + ), + residual_mean=1.3931169304958624, + residual_std=22.45615276341948, + residual_mean_per_atom=0.16737045963056316, + residual_std_per_atom=0.8189314920219992, +) + +REFERENCE_ENERGIES = { + "vasp": vasp_reference_energies, + "vasp-shifted": vasp_shifted_reference_energies, + "mp-traj-d3": mp_traj_d3_reference_energies, +} diff --git a/tests/st/mindchemistry/cell/test_orb/base.py b/tests/st/mindchemistry/cell/test_orb/base.py new file mode 100644 index 000000000..12ebf5a21 --- /dev/null +++ b/tests/st/mindchemistry/cell/test_orb/base.py @@ -0,0 +1,119 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Base data class.""" + +from typing import Dict, Mapping, NamedTuple, Optional, Union + +from mindspore import Tensor + + +Metric = Union[Tensor, int, float] +TensorDict = Mapping[str, Optional[Tensor]] + + +class ModelOutput(NamedTuple): + """A model's output.""" + + loss: Tensor + log: Mapping[str, Metric] + + +class AtomGraphs(NamedTuple): + """A class representing the input to a model for a graph. + + Args: + senders (ms.Tensor): The integer source nodes for each edge. + receivers (ms.Tensor): The integer destination nodes for each edge. + n_node (ms.Tensor): A (batch_size, ) shaped tensor containing the number of nodes per graph. + n_edge (ms.Tensor): A (batch_size, ) shaped tensor containing the number of edges per graph. + node_features (Dict[str, ms.Tensor]): A dictionary containing node feature tensors. + It will always contain "atomic_numbers" and "positions" keys, representing the + atomic numbers of each node, and the 3d cartesian positions of them respectively. + edge_features (Dict[str, ms.Tensor]): A dictionary containing edge feature tensors. + system_features (Optional[TensorDict]): An optional dictionary containing system-level features. + node_targets (Optional[Dict[ms.Tensor]]): An optional dict of tensors containing targets + for individual nodes. This tensor is commonly expected to have shape (num_nodes, *). + edge_target (Optional[ms.Tensor]): An optional tensor containing targets for individual edges. + This tensor is commonly expected to have (num_edges, *). + system_targets (Optional[Dict[ms.Tensor]]): An optional dict of tensors containing targets for the + entire system. system_id (Optional[ms.Tensor]): An optional tensor containing the ID of the system. + fix_atoms (Optional[ms.Tensor]): An optional tensor containing information on fixed atoms in the system. + """ + + senders: Tensor + receivers: Tensor + n_node: Tensor + n_edge: Tensor + node_features: Dict[str, Tensor] + edge_features: Dict[str, Tensor] + system_features: Dict[str, Tensor] + node_targets: Optional[Dict[str, Tensor]] = None + edge_targets: Optional[Dict[str, Tensor]] = None + system_targets: Optional[Dict[str, Tensor]] = None + system_id: Optional[Tensor] = None + fix_atoms: Optional[Tensor] = None + tags: Optional[Tensor] = None + radius: Optional[float] = None + max_num_neighbors: Optional[int] = None + + @property + def positions(self): + """Get positions of atoms.""" + return self.node_features["positions"] + + @positions.setter + def positions(self, val: Tensor): + self.node_features["positions"] = val + + @property + def atomic_numbers(self): + """Get integer atomic numbers.""" + return self.node_features["atomic_numbers"] + + @atomic_numbers.setter + def atomic_numbers(self, val: Tensor): + self.node_features["atomic_numbers"] = val + + @property + def cell(self): + """Get unit cells.""" + assert self.system_features + return self.system_features.get("cell") + + @cell.setter + def cell(self, val: Tensor): + assert self.system_features + self.system_features["cell"] = val + + def clone(self): + """Clone the AtomGraphs object.""" + return AtomGraphs( + senders=self.senders.clone(), + receivers=self.receivers.clone(), + n_node=self.n_node.clone(), + n_edge=self.n_edge.clone(), + node_features={k: v.clone() for k, v in self.node_features.items()}, + edge_features={k: v.clone() for k, v in self.edge_features.items()}, + system_features={k: v.clone() for k, v in self.system_features.items()}, + node_targets={k: v.clone() for k, v in (self.node_targets or {}).items()}, + edge_targets=self.edge_targets.clone() if self.edge_targets is not None else None, + system_targets={k: v.clone() for k, v in (self.system_targets or {}).items()}, + system_id=self.system_id.clone() if self.system_id is not None else None, + fix_atoms=self.fix_atoms.clone() if self.fix_atoms is not None else None, + tags=self.tags.clone() if self.tags is not None else None, + radius=self.radius, + max_num_neighbors=self.max_num_neighbors + ) diff --git a/tests/st/mindchemistry/cell/test_orb/test_orb.py b/tests/st/mindchemistry/cell/test_orb/test_orb.py new file mode 100644 index 000000000..cbb653408 --- /dev/null +++ b/tests/st/mindchemistry/cell/test_orb/test_orb.py @@ -0,0 +1,451 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""test mindchemistry ORB""" + +import os +import sys +from typing import Optional +import pickle + +import requests +import pytest +import numpy as np +import mindspore +from mindspore import nn, Tensor, load_checkpoint, load_param_into_net + +from mindchemistry.cell import ( + AttentionInteractionNetwork, + MoleculeGNS, + NodeHead, + GraphHead, + EnergyHead, + Orb, +) +import base +from utils import numpy_to_tensor, tensor_to_numpy, is_equal + +# pylint: disable=C0413 +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) +sys.path.append(PROJECT_ROOT) +from common.cell import compare_output + + +def load_graph_data(pkl_path: str): + """Load graph data from pickle file. + Args: + pkl_path: Path to the pickle file + Returns: + tuple: (input_graph_ms, output_graph_np) + """ + with open(pkl_path, "rb") as f: + loaded = pickle.load(f) + + input_graph_np = loaded["input_graph"] + output_graph_np = loaded["output_graph"] + + input_graph_ms = base.AtomGraphs( + *[numpy_to_tensor(getattr(input_graph_np, field)) + for field in input_graph_np._fields] + ) + + return input_graph_ms, output_graph_np + + +def get_gns( + latent_dim: int = 256, + mlp_hidden_dim: int = 512, + num_message_passing_steps: int = 15, + num_edge_in_features: int = 23, + distance_cutoff: bool = True, + attention_gate: str = "sigmoid", +) -> MoleculeGNS: + """Define the base pretrained model architecture. + Args: + latent_dim: The latent dimension of the model. + mlp_hidden_dim: The hidden dimension of the MLP layers. + num_message_passing_steps: The number of message passing steps. + num_edge_in_features: The number of edge input features. + distance_cutoff: Whether to use distance cutoff in the interaction. + attention_gate: The type of attention gate to use. + Returns: + MoleculeGNS: The MoleculeGNS model instance. + """ + return MoleculeGNS( + num_node_in_features=256, + num_node_out_features=3, + num_edge_in_features=num_edge_in_features, + latent_dim=latent_dim, + interactions="simple_attention", + interaction_params={ + "distance_cutoff": distance_cutoff, + "polynomial_order": 4, + "cutoff_rmax": 6, + "attention_gate": attention_gate, + }, + num_message_passing_steps=num_message_passing_steps, + num_mlp_layers=2, + mlp_hidden_dim=mlp_hidden_dim, + use_embedding=True, + node_feature_names=["feat"], + edge_feature_names=["feat"], + ) + + +def load_model_for_inference(model: nn.Cell, weights_path: str) -> nn.Cell: + """Load a pretrained model in inference mode. + Args: + model: The model to load the weights into. + weights_path: Path to the checkpoint file. + Returns: + nn.Cell: The model with loaded weights. + Raises: + FileNotFoundError: If the checkpoint file does not exist. + ValueError: If the checkpoint file has more parameters than the model. + """ + if not os.path.exists(weights_path): + raise FileNotFoundError(f"Checkpoint file {weights_path} not found.") + param_dict = load_checkpoint(weights_path) + + try: + load_param_into_net(model, param_dict) + except ValueError: + print("Warning: The checkpoint file has more parameters than the model. \ + This may be due to a mismatch in the model architecture or version.") + params = [] + for key in param_dict: + params.append(param_dict[key]) + for parameters in model.trainable_params(): + param_ckpt = params.pop(0) + assert parameters.shape == param_ckpt.shape, f"Shape mismatch: {parameters.name}" + param_ckpt = param_ckpt.reshape(parameters.shape) + parameters.set_data(param_ckpt) + + model.set_train(False) + return model + +def orb_v2(weights_path: Optional[str]) -> nn.Cell: + """Load ORB v2. + Args: + weights_path: Path to the checkpoint file. + Returns: + Orb GraphRegressor: The ORB v2 model instance. + """ + gns = get_gns() + + model = Orb( + graph_head=EnergyHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=1, + node_aggregation="mean", + reference_energy_name="vasp-shifted", + train_reference=True, + predict_atom_avg=True, + ), + node_head=NodeHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=3, + remove_mean=True, + ), + stress_head=GraphHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=6, + compute_stress=True, + ), + model=gns, + ) + model = load_model_for_inference(model, weights_path) + return model + + +def orb_mptraj_only_v2( + weights_path: Optional[str] = None, +): + """Load ORB MPTraj Only v2.""" + + return orb_v2(weights_path,) + + +def download_file(url, local_filename): + """Download a file from a URL to a local path.""" + response = requests.get(url, timeout=30) + if response.status_code == 200: + with open(local_filename, 'wb') as f: + f.write(response.content) + else: + print(f"Failed to download file. HTTP Status Code: {response.status_code}") + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_attn(): + """ + Feature: Test AttentionInteractionNetwork in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + # prepare data + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/test/attn_input_output.pkl', + 'attn_input_output.pkl' + ) + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/orb_ckpts/attn_net.ckpt', + 'attn_net.ckpt' + ) + input_graph_ms, output_graph_np = load_graph_data('attn_input_output.pkl') + + attn_net = AttentionInteractionNetwork( + num_node_in=256, + num_node_out=256, + num_edge_in=256, + num_edge_out=256, + num_mlp_layers=2, + mlp_hidden_dim=512, + ) + + # load checkpoint + param_dict = load_checkpoint('attn_net.ckpt') + load_param_into_net(attn_net, param_dict) + + # inference + edges, nodes = attn_net( + input_graph_ms.edge_features, + input_graph_ms.node_features, + input_graph_ms.senders, + input_graph_ms.receivers, + ) + + # Validate results + out_node_feats = tensor_to_numpy(nodes["feat"]) + out_edge_feats = tensor_to_numpy(edges["feat"]) + out_node_feats_np = output_graph_np.node_features["feat"] + out_edge_feats_np = output_graph_np.edge_features["feat"] + + flag_node = is_equal(out_node_feats, out_node_feats_np) + flag_edge = is_equal(out_edge_feats, out_edge_feats_np) + assert flag_node, "Failed! Node features mismatch in attention network" + assert flag_edge, "Failed! Edge features mismatch in attention network" + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_gns(): + """ + Feature: Test MoleculeGNS network in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/test/gns_input_output.pkl', + 'gns_input_output.pkl' + ) + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/orb_ckpts/gns_net.ckpt', + 'gns_net.ckpt' + ) + input_graph_ms, output_graph_np = load_graph_data('gns_input_output.pkl') + + # load gns model and checkpoint + gns_model = get_gns() + + # load checkpoint + param_dict = load_checkpoint('gns_net.ckpt') + load_param_into_net(gns_model, param_dict) + + edges, nodes = gns_model( + input_graph_ms.edge_features, + input_graph_ms.node_features, + input_graph_ms.senders, + input_graph_ms.receivers, + ) + + out_node_feats = tensor_to_numpy(nodes["feat"]) + out_edge_feats = tensor_to_numpy(edges["feat"]) + out_node_feats_np = output_graph_np.node_features["feat"] + out_edge_feats_np = output_graph_np.edge_features["feat"] + + flag_node = is_equal(out_node_feats, out_node_feats_np) + flag_edge = is_equal(out_edge_feats, out_edge_feats_np) + assert flag_node, "Failed! Node features mismatch in MoleculeGNS network" + assert flag_edge, "Failed! Edge features mismatch in MoleculeGNS network" + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_node_head(): + """ + Feature: Test NodeHead in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + node_head = NodeHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=3, + remove_mean=True, + ) + + n_atoms = 4 + n_node = Tensor([n_atoms], mindspore.int32) + atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + for i, num in enumerate(atomic_numbers.asnumpy()): + atomic_numbers_embedding_np[i, num - 1] = 1.0 + + node_features = { + "atomic_numbers": atomic_numbers, + "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + } + + output = node_head(node_features, n_node) + assert output['node_pred'].shape == (4, 3), \ + f"Expected node_pred shape (4, 3), but got {output['node_pred'].shape}" + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_graph_head(): + """ + Feature: Test GraphHead in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + graph_head = GraphHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=6, + compute_stress=True, + ) + + n_atoms = 4 + n_node = Tensor([n_atoms], mindspore.int32) + atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + for i, num in enumerate(atomic_numbers.asnumpy()): + atomic_numbers_embedding_np[i, num - 1] = 1.0 + + node_features = { + "atomic_numbers": atomic_numbers, + "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + } + + output = graph_head(node_features, n_node) + assert output['stress_pred'].shape == (1, 6), \ + f"Expected stress_pred shape (1, 6), but got {output['stress_pred'].shape}" + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_energy_head(): + """ + Feature: Test EnergyHead in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + energy_head = EnergyHead( + latent_dim=256, + num_mlp_layers=1, + mlp_hidden_dim=256, + target_property_dim=1, + node_aggregation="mean", + reference_energy_name="vasp-shifted", + train_reference=True, + predict_atom_avg=True, + ) + + n_atoms = 4 + n_node = Tensor([n_atoms], mindspore.int32) + atomic_numbers = Tensor(np.random.randint(1, 119, size=(n_atoms,), dtype=np.int32)) + atomic_numbers_embedding_np = np.zeros((n_atoms, 118), dtype=np.float32) + for i, num in enumerate(atomic_numbers.asnumpy()): + atomic_numbers_embedding_np[i, num - 1] = 1.0 + + node_features = { + "atomic_numbers": atomic_numbers, + "atomic_numbers_embedding": Tensor(atomic_numbers_embedding_np), + "positions": Tensor(np.random.randn(n_atoms, 3).astype(np.float32)), + "feat": Tensor(np.random.randn(n_atoms, 256).astype(np.float32)) + } + + output = energy_head(node_features, n_node) + assert output['graph_pred'].shape == (1, 1), \ + f"Expected graph_pred shape {(1, 1)}, but got {output['graph_pred'].shape}" + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +def test_inference(): + """ + Feature: Test Orb network in platform ascend. + Description: The forward output should has expected shape and accuracy. + Expectation: Success or throw AssertionError. + """ + mindspore.set_context(mode=mindspore.PYNATIVE_MODE) + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/test/orb_input_output.pkl', + 'orb_input_output.pkl' + ) + download_file( + 'https://download-mindspore.osinfra.cn/mindscience/mindchemistry/orb/orb_ckpts/orb-mptraj-only-v2.ckpt', + 'orb-mptraj-only-v2.ckpt' + ) + reference_path = 'orb_input_output.pkl' + with open(reference_path, "rb") as f: + loaded = pickle.load(f) + + atom_graph_ms = loaded["input_graph"] + output_pt = loaded["output"] + + regressor = orb_mptraj_only_v2(weights_path='orb-mptraj-only-v2.ckpt') + regressor.set_train(False) + + out_ms = regressor.predict( + atom_graph_ms.edge_features, + atom_graph_ms.node_features, + atom_graph_ms.senders, + atom_graph_ms.receivers, + atom_graph_ms.n_node, + atom_graph_ms.atomic_numbers, + ) + + out_ms = {k: tensor_to_numpy(v) for k, v in out_ms.items()} + + for k in out_ms: + flag = compare_output(out_ms[k], output_pt[k]) + assert flag, f"Failed! Orb network inference output {k} mismatch" diff --git a/tests/st/mindchemistry/cell/test_orb/utils.py b/tests/st/mindchemistry/cell/test_orb/utils.py new file mode 100644 index 000000000..ad100964a --- /dev/null +++ b/tests/st/mindchemistry/cell/test_orb/utils.py @@ -0,0 +1,105 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Utils""" +import os +import sys +from typing import Any + +import numpy as np +from mindspore import Tensor + +# pylint: disable=C0413 +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) +sys.path.append(PROJECT_ROOT) +from common.cell import compare_output, FP32_ATOL, FP32_RTOL + + +def tensor_to_numpy(data: Any) -> Any: + """Convert MindSpore Tensors to NumPy arrays recursively. + This function traverses the input data structure and converts all MindSpore Tensors + to NumPy arrays, while leaving other data types unchanged. + Args: + data (Any): Input data which can be a MindSpore Tensor, dict, list, tuple, or other types. + Returns: + Any: Data structure with MindSpore Tensors converted to NumPy arrays. + """ + if isinstance(data, Tensor): + return data.numpy() + if isinstance(data, dict): + return {k: tensor_to_numpy(v) for k, v in data.items()} + if isinstance(data, (list, tuple)): + return type(data)(tensor_to_numpy(v) for v in data) + return data + + +def numpy_to_tensor(data: Any) -> Any: + """Convert NumPy arrays to MindSpore Tensors recursively. + This function traverses the input data structure and converts all NumPy arrays + to MindSpore Tensors, while leaving other data types unchanged. + Args: + data (Any): Input data which can be a NumPy array, dict, list, tuple, or other types. + Returns: + Any: Data structure with NumPy arrays converted to MindSpore Tensors. + """ + if isinstance(data, np.ndarray): + return Tensor(data) + if isinstance(data, dict): + return {k: numpy_to_tensor(v) for k, v in data.items()} + if isinstance(data, (list, tuple)): + return type(data)(numpy_to_tensor(v) for v in data) + return data + + +def is_equal(a: Any, b: Any) -> bool: + """Compare two objects for equality with special handling for different types. + + This function performs a deep comparison between two objects, supporting: + - NumPy arrays (using tolerance-based comparison) + - Dictionaries (recursive comparison of values) + - Lists and tuples (element-wise comparison) + - NamedTuples (field-wise comparison) + - Other types (using standard equality comparison) + + Args: + a (Any): First object to compare + b (Any): Second object to compare + + Returns: + bool: True if objects are considered equal, False otherwise + + Examples: + >>> is_equal(np.array([1.0]), np.array([1.0])) + True + >>> is_equal({'a': 1, 'b': 2}, {'a': 1, 'b': 2}) + True + >>> is_equal([1, 2, 3], [1, 2, 3]) + True + """ + if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): + return compare_output(a, b, FP32_ATOL, FP32_RTOL) + if isinstance(a, dict) and isinstance(b, dict): + if a.keys() != b.keys(): + return False + return all(is_equal(a[k], b[k]) for k in a) + if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + if len(a) != len(b): + return False + return all(is_equal(x, y) for x, y in zip(a, b)) + if hasattr(a, "_fields") and hasattr(b, "_fields"): + if a._fields != b._fields: + return False + return all(is_equal(getattr(a, f), getattr(b, f)) for f in a._fields) + return a == b -- Gitee From 6b32593b898b4db819368c6393b5ffd4e5798d3a Mon Sep 17 00:00:00 2001 From: hsliu_ustc Date: Sat, 5 Jul 2025 06:10:39 +0000 Subject: [PATCH 12/30] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20.gitee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitee/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitee/.keep diff --git a/.gitee/.keep b/.gitee/.keep new file mode 100644 index 000000000..e69de29bb -- Gitee From b003ff797b44d5ec36d3c22d418d28d82f648d5f Mon Sep 17 00:00:00 2001 From: hsliu_ustc Date: Sat, 5 Jul 2025 06:11:16 +0000 Subject: [PATCH 13/30] =?UTF-8?q?=E6=96=B0=E5=BB=BA=20ISSUE=5FTEMPLATE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitee/ISSUE_TEMPLATE/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitee/ISSUE_TEMPLATE/.keep diff --git a/.gitee/ISSUE_TEMPLATE/.keep b/.gitee/ISSUE_TEMPLATE/.keep new file mode 100644 index 000000000..e69de29bb -- Gitee From 52902750f9256b5bd985ecaf3a575653c5cbf42d Mon Sep 17 00:00:00 2001 From: hsliu_ustc Date: Sat, 5 Jul 2025 06:16:06 +0000 Subject: [PATCH 14/30] add .gitee/ISSUE_TEMPLATE/config.yaml. Signed-off-by: hsliu_ustc --- .gitee/ISSUE_TEMPLATE/config.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitee/ISSUE_TEMPLATE/config.yaml diff --git a/.gitee/ISSUE_TEMPLATE/config.yaml b/.gitee/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..b8ffbb4b2 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Gitee 帮助中心 + url: https://help.gitee.com/ + about: 提供 Git 使用指南、教程、Gitee.com 平台基本功能使用、介绍和常见问题解答 \ No newline at end of file -- Gitee From 676fa62b7ce75a114c4b8588b91e7865d2a099e5 Mon Sep 17 00:00:00 2001 From: jinduoxia Date: Mon, 7 Jul 2025 11:54:05 +0000 Subject: [PATCH 15/30] add ISSUE_TEMPLATE of mindscience --- .gitee/ISSUE_TEMPLATE/1-documentation.yml | 62 ++++ .gitee/ISSUE_TEMPLATE/2-installation.yml | 68 +++++ .gitee/ISSUE_TEMPLATE/3-bug-report.yml | 308 ++++++++++++++++++++ .gitee/ISSUE_TEMPLATE/4-ci-failure.yml | 83 ++++++ .gitee/ISSUE_TEMPLATE/5-feature-request.yml | 56 ++++ .gitee/ISSUE_TEMPLATE/6-new-model.yml | 46 +++ .gitee/ISSUE_TEMPLATE/7-RFC.yml | 87 ++++++ .gitee/ISSUE_TEMPLATE/8-internship.yml | 81 +++++ 8 files changed, 791 insertions(+) create mode 100644 .gitee/ISSUE_TEMPLATE/1-documentation.yml create mode 100644 .gitee/ISSUE_TEMPLATE/2-installation.yml create mode 100644 .gitee/ISSUE_TEMPLATE/3-bug-report.yml create mode 100644 .gitee/ISSUE_TEMPLATE/4-ci-failure.yml create mode 100644 .gitee/ISSUE_TEMPLATE/5-feature-request.yml create mode 100644 .gitee/ISSUE_TEMPLATE/6-new-model.yml create mode 100644 .gitee/ISSUE_TEMPLATE/7-RFC.yml create mode 100644 .gitee/ISSUE_TEMPLATE/8-internship.yml diff --git a/.gitee/ISSUE_TEMPLATE/1-documentation.yml b/.gitee/ISSUE_TEMPLATE/1-documentation.yml new file mode 100644 index 000000000..128e7e8c8 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/1-documentation.yml @@ -0,0 +1,62 @@ +name: 📚 Documentation +description: Request updates or additions to MindScience documentation +title: "[Doc]: " +labels: ["documentation"] + +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to help MindScience and improve our documentation! + - If this is your first time, please read [our contributor guidelines](https://gitee.com/mindspore/mindscience/blob/master/CONTRIBUTION.md). + - You also confirm that you have searched the [open documentation issues](https://gitee.com/mindspore/mindscience/issues) and have found no duplicates for this request +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: dropdown + id: new_or_correction + attributes: + label: Is this for new documentation, or an update to existing docs? + options: + - New + - Update + validations: + required: true + +- type: textarea + attributes: + label: 📚 The doc issue + description: > + Describe the incorrect/future/missing documentation. + value: | + 1. 【Document Link】/【文档链接】 + + 2. 【Issues Section】/【问题文档片段】 + + 3. 【Existing Issues】/【存在的问题】 + + 4. 【Expected Result】【预期结果】 + + validations: + required: true +- type: textarea + attributes: + label: Suggest a potential alternative/fix + description: > + Tell us how we could improve the documentation in this regard. +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/2-installation.yml b/.gitee/ISSUE_TEMPLATE/2-installation.yml new file mode 100644 index 000000000..5938cfe5d --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/2-installation.yml @@ -0,0 +1,68 @@ +name: 🛠️ Installation +description: Report an issue here when you hit errors during installation. +title: "[Installation]: " +labels: ["installation"] + +body: +- type: markdown + attributes: + value: > + #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: textarea + attributes: + label: Your current environment + description: | + Environment / 环境信息 (Mandatory / 必填) + value: | + - **Hardware Environment / 硬件环境(Mandatory / 必填)**: + Hardware (e.g.`Atlas 800T A2`) + + 样例: + + | 后端类型| 硬件具体类别 | + | --- | --- | + | Server | Atlas 800T A2 | + | CPU| Mac CPU/Win CPU/Linux CPU| + + + - **Software Environment / 软件环境 (Mandatory / 必填)**: + 迭代版本新增问题样例:(根据实际修改和增删) + + | Software | Version(根据实际修改,必填)| + | --- | --- | + | MindSpore | MindSpore 2.4.0 | + | CANN | 8.0.0.beta1 | + | Python | Python XXXXXX | + | OS platform | Ubuntu XXXXXX | + | GCC/Compiler version | XXXXXX | + + validations: + required: true +- type: textarea + attributes: + label: How you are installing MindScience + description: | + Paste the full command you are trying to execute. + placeholder: | + ```sh + pip install mindsponge_*.whl + ``` +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/3-bug-report.yml b/.gitee/ISSUE_TEMPLATE/3-bug-report.yml new file mode 100644 index 000000000..ae8c1bae7 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/3-bug-report.yml @@ -0,0 +1,308 @@ +name: 🐛 Bug report +description: Raise an issue here if you find a bug. +title: "[Bug]: " +labels: ["bug"] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to help MindScience and fill out this bug report! + - If this is your first time, please read [our contributor guidelines](https://gitee.com/mindspore/mindscience/blob/master/CONTRIBUTION.md). + - You also confirm that you have searched the [open documentation issues](https://gitee.com/mindspore/mindscience/issues) and have found no duplicates for this request + - type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + + - type: input + id: version + attributes: + label: Version + description: What version of MindScience are you running? + placeholder: "example: r0.7" + validations: + required: true + + - type: textarea + attributes: + label: installation-method + description: | + Paste the full command you are trying to execute. + placeholder: | + ```sh + pip install mindsponge_*.whl + ``` + + - type: textarea + attributes: + label: Your current environment + description: | + Environment / 环境信息 (Mandatory / 必填) + value: | + - **Hardware Environment / 硬件环境(Mandatory / 必填)**: + Hardware (e.g.`Atlas 800T A2`) + + 样例: + + | 后端类型| 硬件具体类别 | + | --- | --- | + | Server | Atlas 800T A2 | + | CPU| Mac CPU/Win CPU/Linux CPU| + + + - **Software Environment / 软件环境 (Mandatory / 必填)**: + 迭代版本新增问题样例:(根据实际修改和增删) + + | Software | Version(根据实际修改,必填)| + | --- | --- | + | MindSpore | MindSpore 2.4.0 | + | CANN | 8.0.0.beta1 | + | Python | Python XXXXXX | + | OS platform | Ubuntu XXXXXX | + | GCC/Compiler version | XXXXXX | + + + bugfix版本问题引入样例:(根据实际修改和增删) + + | Software | Version(根据实际修改,必填)| + | --- | --- | + | MindSpore | MindSpore 2.4.0 (成功)master_202407131XXXXXX _a4230c71d(失败)| + | CANN | 8.0.0.beta1 | + | CANN 归档地址 | | + | Python | Python XXXXXX | + | OS platform | Ubuntu XXXXXX | + | GCC/Compiler version | XXXXXX | + + validations: + required: true + + + - type: textarea + id: description + attributes: + label: Describe the issue + description: | + Please provide a complete and succinct description of the problem, including what you expected to happen. + value: | + #### 1.Describe the current behavior / 问题描述 (Mandatory / 必填) + + 样例: (根据实际修改和增删) + + > sponge.colvar.Distance()报错,同时 sponge.metrics.Metric其子类的 .update() 报错 + + #### 2. / 关联用例 (Mandatory / 必填)Related testcase + + ```python + from mindspore import Tensor + from sponge.colvar import Distance + from sponge.metrics import MetricCV + cv = Distance([0,1]) + coordinate = Tensor([[[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]]) + metric = MetricCV(cv) + metric.update(coordinate) + print(metric.eval()) + ``` + + #### 3.Steps to reproduce the issue / 重现步骤 (Mandatory / 必填) + + > 测试步骤:运行关联用例即可 + > 用例执行命令:来自CI日志或者用户执行命令 + + + #### 4.Describe the expected behavior / 预期结果 (Mandatory / 必填) + + > **【预期结果】**:MindSpore 1.10.1 版本下,可正常运行。预期输出为 [1.] + + #### 5.Related log / screenshot / 日志 / 截图 (Mandatory / 必填) + + ```shell + --------------------------------------------------------------------------- + ValueError Traceback (most recent call last) + Cell In[4], line 7 + 5 coordinate = Tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) + 6 metric = MetricCV(cv) + ----> 7 metric.update(coordinate) + 8 print(metric.eval()) + + File ~/mindscience/MindSPONGE/./src/sponge/metrics/metrics.py:190, in MetricCV.update(self, coordinate, pbc_box, energy, force, potentials, total_bias, biases) + 163 """ + 164 + 165 Args: + (...) + 186 V: Number of bias potential energies. + 187 """ + 188 #pylint: disable=unused-argument + --> 190 colvar = self.colvar(coordinate, pbc_box) + 192 self._value = self._convert_data(colvar) + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:705, in Cell.__call__(self, *args, **kwargs) + 703 except Exception as err: + 704 _pynative_executor.clear_res() + --> 705 raise err + 707 if isinstance(output, Parameter): + 708 output = output.data + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:701, in Cell.__call__(self, *args, **kwargs) + 699 try: + 700 _pynative_executor.new_graph(self, *args, **kwargs) + --> 701 output = self._run_construct(args, kwargs) + 702 _pynative_executor.end_graph(self, output, *args, **kwargs) + 703 except Exception as err: + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:482, in Cell._run_construct(self, cast_inputs, kwargs) + 480 output = self._shard_fn(*cast_inputs, **kwargs) + 481 else: + --> 482 output = self.construct(*cast_inputs, **kwargs) + 483 if self._enable_forward_hook: + 484 output = self._run_forward_hook(cast_inputs, output) + + File ~/mindscience/MindSPONGE/./src/sponge/colvar/basic/distance.py:146, in Distance.construct(self, coordinate, pbc_box) + 131 r"""calculate distance. + 132 + 133 Args: + (...) + 142 + 143 """ + 145 # (B, ..., D) + --> 146 vector = self.vector(coordinate, pbc_box) + 148 # (B, ...) or (B, ..., 1) + 149 if self.norm_last_dim is None: + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:705, in Cell.__call__(self, *args, **kwargs) + 703 except Exception as err: + 704 _pynative_executor.clear_res() + --> 705 raise err + 707 if isinstance(output, Parameter): + 708 output = output.data + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:701, in Cell.__call__(self, *args, **kwargs) + 699 try: + 700 _pynative_executor.new_graph(self, *args, **kwargs) + --> 701 output = self._run_construct(args, kwargs) + 702 _pynative_executor.end_graph(self, output, *args, **kwargs) + 703 except Exception as err: + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:482, in Cell._run_construct(self, cast_inputs, kwargs) + 480 output = self._shard_fn(*cast_inputs, **kwargs) + 481 else: + --> 482 output = self.construct(*cast_inputs, **kwargs) + 483 if self._enable_forward_hook: + 484 output = self._run_forward_hook(cast_inputs, output) + + File ~/mindscience/MindSPONGE/./src/sponge/colvar/atoms/vector.py:183, in Vector.construct(self, coordinate, pbc_box) + 180 atoms1 = self.atoms1(coordinate, pbc_box) + 181 else: + 182 # (B, ..., 2, D) + --> 183 atoms = self.atoms(coordinate, pbc_box) + 184 # (B, ..., 1, D) <- (B, ..., 2, D) + 185 atoms0, atoms1 = self.split2(atoms) + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:705, in Cell.__call__(self, *args, **kwargs) + 703 except Exception as err: + 704 _pynative_executor.clear_res() + --> 705 raise err + 707 if isinstance(output, Parameter): + 708 output = output.data + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:701, in Cell.__call__(self, *args, **kwargs) + 699 try: + 700 _pynative_executor.new_graph(self, *args, **kwargs) + --> 701 output = self._run_construct(args, kwargs) + 702 _pynative_executor.end_graph(self, output, *args, **kwargs) + 703 except Exception as err: + + File ~/.local/lib/python3.8/site-packages/mindspore/nn/cell.py:482, in Cell._run_construct(self, cast_inputs, kwargs) + 480 output = self._shard_fn(*cast_inputs, **kwargs) + 481 else: + --> 482 output = self.construct(*cast_inputs, **kwargs) + 483 if self._enable_forward_hook: + 484 output = self._run_forward_hook(cast_inputs, output) + + File ~/mindscience/MindSPONGE/./src/sponge/colvar/atoms/atoms.py:232, in Atoms.construct(self, coordinate, pbc_box) + 219 r"""get position coordinate(s) of specific atom(s) + 220 + 221 Args: + (...) + 229 + 230 """ + 231 # (B, a_1, a_2, ..., a_{n}, D) <- (B, A, D) + --> 232 atoms = func.gather_vector(coordinate, self.index) + 233 if self.keep_in_box: + 234 atoms = self.coordinate_in_pbc(atoms, pbc_box) + + File ~/.local/lib/python3.8/site-packages/mindspore/common/api.py:718, in jit..wrap_mindspore..staging_specialize(*args, **kwargs) + 716 if _is_pynative_parallel() and func.__name__ == _PYNATIVE_PARALLEL_FUNC_NAME: + 717 process_obj = hash_args + --> 718 out = _MindsporeFunctionExecutor(func, hash_obj, input_signature, process_obj, jit_config)(*args, **kwargs) + 719 return out + + File ~/.local/lib/python3.8/site-packages/mindspore/common/api.py:121, in _wrap_func..wrapper(*arg, **kwargs) + 119 @wraps(fn) + 120 def wrapper(*arg, **kwargs): + --> 121 results = fn(*arg, **kwargs) + 122 return _convert_python_data(results) + + File ~/.local/lib/python3.8/site-packages/mindspore/common/api.py:350, in _MindsporeFunctionExecutor.__call__(self, *args, **kwargs) + 348 except Exception as err: + 349 _pynative_executor.clear_res() + --> 350 raise err + 352 if context.get_context("precompile_only"): + 353 return None + + File ~/.local/lib/python3.8/site-packages/mindspore/common/api.py:344, in _MindsporeFunctionExecutor.__call__(self, *args, **kwargs) + 342 if context.get_context("mode") == context.PYNATIVE_MODE: + 343 _pynative_executor.set_jit_compile_status(True, phase) + --> 344 phase = self.compile(self.fn.__name__, *args_list, **kwargs) + 345 _pynative_executor.set_jit_compile_status(False, phase) + 346 else: + + File ~/.local/lib/python3.8/site-packages/mindspore/common/api.py:435, in _MindsporeFunctionExecutor.compile(self, method_name, *args, **kwargs) + 433 else: + 434 setattr(self.fn, "__jit_function__", True) + --> 435 is_compile = self._graph_executor.compile(self.fn, compile_args, kwargs, phase, True) + 436 if isinstance(self.fn, types.MethodType): + 437 delattr(self.fn.__func__, "__jit_function__") + + ValueError: For primitive[BroadcastTo], the attribute[x shape] must be less than or equal to 1, but got 2. + + ---------------------------------------------------- + - C++ Call Stack: (For framework developers) + ---------------------------------------------------- + mindspore/core/utils/check_convert_utils.cc:675 Check + ``` + + ### + + #### 6.Special notes for this issue/备注 (Optional / 选填) + + **【定位人】**吴某某(根据实际修改) + + validations: + required: true + + + - type: textarea + id: mvr + attributes: + label: Minimum reproducible example + description: Please supply a [minimum reproducible code example](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) here. + render: shell + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please paste relevant error and log output here + render: shell + diff --git a/.gitee/ISSUE_TEMPLATE/4-ci-failure.yml b/.gitee/ISSUE_TEMPLATE/4-ci-failure.yml new file mode 100644 index 000000000..b63c24804 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/4-ci-failure.yml @@ -0,0 +1,83 @@ +name: 🧪 CI failure report +description: Report a failing test. +title: "[CI Failure]: " +labels: ["ci-failure"] + +body: +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: markdown + attributes: + value: > + #### Include the name of the failing Buildkite step and test file in the title. +- type: input + attributes: + label: Name of failing test + description: | + Paste in the fully-qualified name of the failing test from the logs. + placeholder: | + `path/to/test_file.py::test_name[params]` + validations: + required: true +- type: checkboxes + attributes: + label: Basic information + description: Select all items that apply to the failing test. + options: + - label: Flaky test + - label: Can reproduce locally + - label: Caused by external libraries (e.g. bug in `transformers`) +- type: textarea + attributes: + label: 🧪 Describe the failing test + description: | + Please provide a clear and concise description of the failing test. + placeholder: | + A clear and concise description of the failing test. + + ``` + The error message you got, with the full traceback and the error logs with [dump_input.py:##] if present. + ``` + validations: + required: true +- type: textarea + attributes: + label: 📝 History of failing test + description: | + Since when did the test start to fail? + + If you have time, identify the PR that caused the test to fail on main. You can do so via the following methods: + + - Use Buildkite Test Suites to find the PR where the test failure first occurred, and reproduce the failure locally. + + - Run [`git bisect`](https://git-scm.com/docs/git-bisect) locally. + + - Manually unblock Buildkite steps for suspected PRs on main and check the results. (authorized users only) + placeholder: | + Approximate timeline and/or problematic PRs + + A link to the Buildkite analytics of the failing test (if available) + validations: + required: true +- type: textarea + attributes: + label: CC List. + description: > + The list of people you want to CC. Usually, this includes those who worked on the PR that failed the test. +- type: markdown + attributes: + value: > + Thanks for reporting 🙏! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/5-feature-request.yml b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml new file mode 100644 index 000000000..d500041de --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml @@ -0,0 +1,56 @@ +name: 🚀 Feature request +description: Submit a proposal/request for a new MindScience feature +title: "[Feature]: " +labels: ["feature"] + +body: +- type: markdown + attributes: + value: > + #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true +- type: dropdown + id: new_or_improvement + attributes: + label: Is this a new feature, an improvement, or a change to existing functionality? + options: + - New Feature + - Improvement + - Change + validations: + required: true + +- type: textarea + attributes: + label: 🚀 The feature, motivation and pitch + description: > + A clear and concise description of the feature proposal. Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too. + validations: + required: true +- type: textarea + attributes: + label: Alternatives + description: > + A description of any alternative solutions or features you've considered, if any. +- type: textarea + attributes: + label: Additional context + description: > + Add any other context or screenshots about the feature request. +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/6-new-model.yml b/.gitee/ISSUE_TEMPLATE/6-new-model.yml new file mode 100644 index 000000000..41a53abb8 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/6-new-model.yml @@ -0,0 +1,46 @@ +name: 🤗 Support request for a new model of science +description: Submit a proposal/request for a new model of science +title: "[New Model]: " +labels: ["new-model"] + +body: +- type: markdown + attributes: + value: > + #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: textarea + attributes: + label: The model to consider. + description: > + A url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 . + validations: + required: true +- type: textarea + attributes: + label: The closest model MindScience already supports. + description: > + Here is the list of models already supported by MindScience: https://gitee.com/mindspore/mindscience#%E6%A6%82%E8%BF%B0 . Which model is the most similar to the model you want to add support for? +- type: textarea + attributes: + label: What's your difficulty of supporting the model you want? + description: > + For example, any new operators or new architecture? +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/7-RFC.yml b/.gitee/ISSUE_TEMPLATE/7-RFC.yml new file mode 100644 index 000000000..63a9035e7 --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/7-RFC.yml @@ -0,0 +1,87 @@ +name: 💬 Request for comments (RFC). +description: Ask for feedback on major architectural changes or design choices. +title: "[RFC]: " +labels: ["RFC"] + +body: +- type: markdown + attributes: + value: > + #### Please take a look at previous [RFCs](https://gitee.com/mindspore/mindscience/issues) for reference. +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: textarea + attributes: + label: Backgroud. + description: > + Backgroud(背景信息) + placeholder: | + - Describe/Explain the status of the problem you wish to solve. + - Attach relevant issues if there is any. + validations: + required: true +- type: textarea + attributes: + label: Origin + description: > + Origin(信息来源) + placeholder: | + - Explain which department/team made this request so that its priority can be given. + validations: + required: true +- type: textarea + attributes: + label: Benefit / Necessity + description: > + Benefit / Necessity (价值/作用) + placeholder: | + - Describe/Explain the key value by fulfilling the request. + validations: + required: true +- type: textarea + attributes: + label: Design + description: > + Design(设计方案) + placeholder: | + - Describe/Explain the general idea of the design. Pseudo-code is allowed + validations: + required: true +- type: textarea + attributes: + label: Feedback Period. + description: > + The feedback period of the RFC. Usually at least one week. + validations: + required: false +- type: textarea + attributes: + label: CC List. + description: > + The list of people you want to CC. + validations: + required: false +- type: textarea + attributes: + label: Any Other Things. + description: > + Any other things you would like to mention. + validations: + required: false +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/ISSUE_TEMPLATE/8-internship.yml b/.gitee/ISSUE_TEMPLATE/8-internship.yml new file mode 100644 index 000000000..5bd065bcf --- /dev/null +++ b/.gitee/ISSUE_TEMPLATE/8-internship.yml @@ -0,0 +1,81 @@ +name: 💻 Internship +description: This issue is intended for the MindScience open source internship project for college students +title: "[Internship]: " +labels: ["internship"] + + +body: +- type: markdown + attributes: + value: | + - This issue is intended for the MindSpore open source internship project for college students. Developers who do not participate in this project are not allowed to receive it. + - 本issue为面向高校学生的“MindSpore开源实习”项目的任务,非参加该项目的人员勿领。 +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSpore Science Core + - applications-SPONGE + - applications-Flow + - applications-Energy + - applications-Chemistry + - applications-Earth + - Others + validations: + required: true + +- type: textarea + attributes: + label: Your information. + description: > + Your information for intership. + value: | + 【Task score】 + 【Background description】 + 【Requirements】 + 【Development environment】 + - Hardware: + - Software: + + 【Programming language】 + 【Acceptance criteria】 + 【PR Submission address】 + 【Expected completion time】 + 【Development guide】 + 【Tutor & email】 + + Note: This issue is intended for the MindSpore open source internship project for college students. Developers who do not participate in this project are not allowed to receive it. + + --- + + 【任务分值】 + 【背景描述】 + 【需求描述】 + 【环境要求】 + - 硬件: + - 软件: + + 【编程语言】 + 【产出标准】 + 【PR提交地址】 + 【期望完成时间】 + 【开发指导】 + 【导师及邮箱】 + + 本issue为面向高校学生的“MindSpore开源实习”项目的任务,非参加该项目的人员勿领。 + + validations: + required: false + +- type: textarea + attributes: + label: Any Other Things. + description: > + Any other things you would like to mention. + validations: + required: false +- type: markdown + attributes: + value: > + Thanks for contributing 🎉! \ No newline at end of file -- Gitee From f0c5b013a707fd177177986604be824ad67e3372 Mon Sep 17 00:00:00 2001 From: cs123abc Date: Thu, 10 Jul 2025 02:09:49 +0000 Subject: [PATCH 16/30] !2281 update .gitee/PULL_REQUEST_TEMPLATE * update .gitee/PULL_REQUEST_TEMPLATE --- .../PULL_REQUEST_TEMPLATE.en.md | 30 +++++++++++++++++++ .../PULL_REQUEST_TEMPLATE.zh-CN.md | 30 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md create mode 100644 .gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md diff --git a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md new file mode 100644 index 000000000..9108f2790 --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md @@ -0,0 +1,30 @@ +### PR Source +- [ ] Issue (Please link related issue) +- [ ] Feature request +- [ ] Bug report +- [ ] Community contributor + +### Change Description +- **Reason for Modification:** + +- **Content Modified:** + +### Function Validation +- [ ] **Self-verification** +- [ ] **Screenshots of local test cases** + +### Checklist +- [ ] **Code reviewed** +- [ ] **UT test coverage** (If not, explain reason: ____________________) +- [ ] **Involves public API changes in MindSpore Science** +- [ ] **Documentation updated** + +### Code Review Requirements +- Changes over 1000 lines require organized review meeting with conclusions +- PR without function validation cannot be merged +- PR with incomplete checklist cannot be merged +- PR without clear source identification or change description cannot be merged + +### Change Notification +- [ ] **Documentation modified** +- [ ] **API change description** (If API changed, detail description): \ No newline at end of file diff --git a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 000000000..c166c30dc --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,30 @@ +### PR来源 +- [ ] issue单(请关联issue) +- [ ] 需求特性 +- [ ] 问题单 +- [ ] 社区开发者贡献 + +### 修改描述 +- **修改原因:** + +- **修改内容:** + +### 功能验证 +- [ ] **功能自验** +- [ ] **本地自验用例截图** + +### 检查清单 +- [ ] **是否经过代码检视** +- [ ] **是否具备UT测试用例看护**(如不符合,请说明原因:____________________) +- [ ] **是否涉及MindSpore Science公共接口变更** +- [ ] **是否涉及文档更新** + +### 代码检视要求 +- 合入代码超过1000行,需组织会议检视并附上结论 +- 未完成功能验证不允许合入 +- 未完成检查清单不允许合入 +- PR来源未标识或修改描述不清晰不允许合入 + +### 变更说明 +- [ ] **文档修改** +- [ ] **接口变更说明**(如涉及接口变更需详细描述): \ No newline at end of file -- Gitee From 2f60c87ace489dbae82d05d241c006a3d5d804fe Mon Sep 17 00:00:00 2001 From: goto Date: Thu, 10 Jul 2025 11:31:38 +0800 Subject: [PATCH 17/30] fix AdaHessian test cases for MS2.6 --- tests/st/mindflow/cell/test_optimizers.py | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/st/mindflow/cell/test_optimizers.py b/tests/st/mindflow/cell/test_optimizers.py index eaa34f1fc..4085507fd 100644 --- a/tests/st/mindflow/cell/test_optimizers.py +++ b/tests/st/mindflow/cell/test_optimizers.py @@ -22,7 +22,7 @@ import pytest import numpy as np import mindspore as ms -from mindspore import ops, set_seed, nn +from mindspore import ops, set_seed, nn, mint from mindspore import dtype as mstype from mindflow import UNet2D, TransformerBlock, MultiHeadAttention, AdaHessian from mindflow.cell.attention import FeedForward @@ -99,10 +99,11 @@ class TestAttentionBlock(TransformerBlock): self.act_fn = nn.ReLU() # replace `gelu` with `relu` to avoid `vjp` problem class TestMultiHeadAttention(MultiHeadAttention): - ''' MultiHeadAttention modified to avoid vjp bug ''' + ''' MultiHeadAttention modified to support vjp ''' def get_qkv(self, x: ms.Tensor) -> tuple[ms.Tensor]: ''' use masks to select out q, k, v, instead of tensor reshaping & indexing ''' - b, n, c = x.shape + b, n, c_full = x.shape + c = c_full // self.num_heads # use matmul with masks to select out q, k, v to avoid vjp problem q_mask = ms.Tensor(np.vstack([np.eye(c), np.zeros([2 * c, c])]), dtype=self.compute_dtype) @@ -110,10 +111,11 @@ class TestAttentionBlock(TransformerBlock): v_mask = ms.Tensor(np.vstack([np.zeros([2 * c, c]), np.eye(c)]), dtype=self.compute_dtype) qkv = self.qkv(x) + qkv = qkv.reshape(b, n, self.num_heads, -1).swapaxes(1, 2) - q = ops.swapaxes(ops.matmul(qkv, q_mask).reshape(b, n, self.num_heads, -1), 1, 2) - k = ops.swapaxes(ops.matmul(qkv, k_mask).reshape(b, n, self.num_heads, -1), 1, 2) - v = ops.swapaxes(ops.matmul(qkv, v_mask).reshape(b, n, self.num_heads, -1), 1, 2) + q = mint.matmul(qkv, q_mask) + k = mint.matmul(qkv, k_mask) + v = mint.matmul(qkv, v_mask) return q, k, v @@ -156,7 +158,7 @@ def test_adahessian_accuracy(mode): in_channels=2, out_channels=4, kernel_size=3, has_bias=True, weight_init=weight_init, bias_init=bias_init) def forward(a): - return ops.mean(net(a)**2)**.5 + return ops.sqrt(ops.mean(ops.square(net(a)))) grad_fn = ms.grad(forward, grad_position=None, weights=net.trainable_params()) @@ -192,7 +194,7 @@ def test_adahessian_st(mode, model_option): # default test with Attention network net = TestAttentionBlock(in_channels=256, num_heads=4) - inputs = ms.Tensor(np.random.rand(4, 100, 256), dtype=ms.float32) + inputs = ms.Tensor(np.sin(np.arange(102400)).reshape(4, 100, 256), dtype=ms.float32) # test with UNet network if model_option.lower() == 'unet': @@ -210,7 +212,7 @@ def test_adahessian_st(mode, model_option): inputs = ms.Tensor(np.random.rand(2, 2, 64, 64), dtype=ms.float32) def forward(a): - return ops.mean(net(a)**2)**.5 + return ops.sqrt(ops.mean(ops.square(net(a)))) grad_fn = ms.grad(forward, grad_position=None, weights=net.trainable_params()) @@ -242,10 +244,10 @@ def test_adahessian_compare(mode): def get_loss(optimizer_option): ''' compare Adam and AdaHessian ''' net = TestAttentionBlock(in_channels=256, num_heads=4) - inputs = ms.Tensor(np.random.rand(4, 100, 256), dtype=ms.float32) + inputs = ms.Tensor(np.sin(np.arange(102400)).reshape(4, 100, 256), dtype=ms.float32) def forward(a): - return ops.mean(net(a)**2)**.5 + return ops.sqrt(ops.mean(ops.square(net(a)))) grad_fn = ms.grad(forward, grad_position=None, weights=net.trainable_params()) -- Gitee From 8ab94e28b05a240f3d5b13b238fa721b17cadfda Mon Sep 17 00:00:00 2001 From: cs123abc Date: Tue, 8 Jul 2025 15:27:27 +0800 Subject: [PATCH 18/30] update .gitee/PULL_REQUEST_TEMPLATE --- .gitee/PULL_REQUEST_TEMPLATE.en.md | 30 +++++++++++++++++++++++++++ .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md | 30 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 .gitee/PULL_REQUEST_TEMPLATE.en.md create mode 100644 .gitee/PULL_REQUEST_TEMPLATE.zh-CN.md diff --git a/.gitee/PULL_REQUEST_TEMPLATE.en.md b/.gitee/PULL_REQUEST_TEMPLATE.en.md new file mode 100644 index 000000000..9108f2790 --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE.en.md @@ -0,0 +1,30 @@ +### PR Source +- [ ] Issue (Please link related issue) +- [ ] Feature request +- [ ] Bug report +- [ ] Community contributor + +### Change Description +- **Reason for Modification:** + +- **Content Modified:** + +### Function Validation +- [ ] **Self-verification** +- [ ] **Screenshots of local test cases** + +### Checklist +- [ ] **Code reviewed** +- [ ] **UT test coverage** (If not, explain reason: ____________________) +- [ ] **Involves public API changes in MindSpore Science** +- [ ] **Documentation updated** + +### Code Review Requirements +- Changes over 1000 lines require organized review meeting with conclusions +- PR without function validation cannot be merged +- PR with incomplete checklist cannot be merged +- PR without clear source identification or change description cannot be merged + +### Change Notification +- [ ] **Documentation modified** +- [ ] **API change description** (If API changed, detail description): \ No newline at end of file diff --git a/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md new file mode 100644 index 000000000..c166c30dc --- /dev/null +++ b/.gitee/PULL_REQUEST_TEMPLATE.zh-CN.md @@ -0,0 +1,30 @@ +### PR来源 +- [ ] issue单(请关联issue) +- [ ] 需求特性 +- [ ] 问题单 +- [ ] 社区开发者贡献 + +### 修改描述 +- **修改原因:** + +- **修改内容:** + +### 功能验证 +- [ ] **功能自验** +- [ ] **本地自验用例截图** + +### 检查清单 +- [ ] **是否经过代码检视** +- [ ] **是否具备UT测试用例看护**(如不符合,请说明原因:____________________) +- [ ] **是否涉及MindSpore Science公共接口变更** +- [ ] **是否涉及文档更新** + +### 代码检视要求 +- 合入代码超过1000行,需组织会议检视并附上结论 +- 未完成功能验证不允许合入 +- 未完成检查清单不允许合入 +- PR来源未标识或修改描述不清晰不允许合入 + +### 变更说明 +- [ ] **文档修改** +- [ ] **接口变更说明**(如涉及接口变更需详细描述): \ No newline at end of file -- Gitee From 300b85c45af2998ecc06c364f886c460d0af6900 Mon Sep 17 00:00:00 2001 From: brian Date: Thu, 10 Jul 2025 19:47:42 +0800 Subject: [PATCH 19/30] mod template --- .gitee/.keep | 0 .gitee/ISSUE_TEMPLATE/5-feature-request.yml | 20 +++-- ...6-new-model.yml => 6-application-case.yml} | 89 +++++++++---------- .../PULL_REQUEST_TEMPLATE.en.md | 30 ------- .../PULL_REQUEST_TEMPLATE.zh-CN.md | 30 ------- 5 files changed, 55 insertions(+), 114 deletions(-) delete mode 100644 .gitee/.keep rename .gitee/ISSUE_TEMPLATE/{6-new-model.yml => 6-application-case.yml} (75%) delete mode 100644 .gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md delete mode 100644 .gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md diff --git a/.gitee/.keep b/.gitee/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/.gitee/ISSUE_TEMPLATE/5-feature-request.yml b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml index d500041de..a5129a7b4 100644 --- a/.gitee/ISSUE_TEMPLATE/5-feature-request.yml +++ b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml @@ -9,16 +9,18 @@ body: value: > #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). - type: dropdown - id: domain + id: module attributes: - label: Which domain the issue belongs to? + label: Which module the issue belongs to? options: - - MindSpore Science Core - - applications-SPONGE - - applications-Flow - - applications-Energy - - applications-Chemistry - - applications-Earth + - MindScience data + - MindScience common + - MindScience e3nn + - MindScience models + - MindScience sciops + - MindScience solver + - MindScience sharker + - MindScience utils - Others validations: required: true @@ -37,7 +39,7 @@ body: attributes: label: 🚀 The feature, motivation and pitch description: > - A clear and concise description of the feature proposal. Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too. + A clear and concise description of the feature proposal. Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too. For design design, you can refer to [feature design template](https://gitee.com/mindspore/mindscience/blob/br_refactor/docs/template/feature_design.md). validations: required: true - type: textarea diff --git a/.gitee/ISSUE_TEMPLATE/6-new-model.yml b/.gitee/ISSUE_TEMPLATE/6-application-case.yml similarity index 75% rename from .gitee/ISSUE_TEMPLATE/6-new-model.yml rename to .gitee/ISSUE_TEMPLATE/6-application-case.yml index 41a53abb8..dfbf8e154 100644 --- a/.gitee/ISSUE_TEMPLATE/6-new-model.yml +++ b/.gitee/ISSUE_TEMPLATE/6-application-case.yml @@ -1,46 +1,45 @@ -name: 🤗 Support request for a new model of science -description: Submit a proposal/request for a new model of science -title: "[New Model]: " -labels: ["new-model"] - -body: -- type: markdown - attributes: - value: > - #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). -- type: dropdown - id: domain - attributes: - label: Which domain the issue belongs to? - options: - - MindSpore Science Core - - applications-SPONGE - - applications-Flow - - applications-Energy - - applications-Chemistry - - applications-Earth - - Others - validations: - required: true - -- type: textarea - attributes: - label: The model to consider. - description: > - A url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 . - validations: - required: true -- type: textarea - attributes: - label: The closest model MindScience already supports. - description: > - Here is the list of models already supported by MindScience: https://gitee.com/mindspore/mindscience#%E6%A6%82%E8%BF%B0 . Which model is the most similar to the model you want to add support for? -- type: textarea - attributes: - label: What's your difficulty of supporting the model you want? - description: > - For example, any new operators or new architecture? -- type: markdown - attributes: - value: > +name: 🤗 Support request for a new application case of science +description: Submit a proposal/request for a new application case of science +title: "[Application Case]: " +labels: ["application-case"] + +body: +- type: markdown + attributes: + value: > + #### Before submitting an issue, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://gitee.com/mindspore/mindscience/issues). +- type: dropdown + id: domain + attributes: + label: Which domain the issue belongs to? + options: + - MindSPONGE + - MindFlow + - MindEnergy + - MindChemistry + - MindEarth + - Others + validations: + required: true + +- type: textarea + attributes: + label: The model to consider. + description: > + A url, pointing to the model, e.g. https://huggingface.co/openai-community/gpt2 . + validations: + required: true +- type: textarea + attributes: + label: The closest model MindScience already supports. + description: > + Here is the list of models already supported by MindScience: https://gitee.com/mindspore/mindscience#%E6%A6%82%E8%BF%B0 . Which model is the most similar to the model you want to add support for? +- type: textarea + attributes: + label: What's your difficulty of supporting the model you want? + description: > + For example, any new operators or new architecture? +- type: markdown + attributes: + value: > Thanks for contributing 🎉! \ No newline at end of file diff --git a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md deleted file mode 100644 index 9108f2790..000000000 --- a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.en.md +++ /dev/null @@ -1,30 +0,0 @@ -### PR Source -- [ ] Issue (Please link related issue) -- [ ] Feature request -- [ ] Bug report -- [ ] Community contributor - -### Change Description -- **Reason for Modification:** - -- **Content Modified:** - -### Function Validation -- [ ] **Self-verification** -- [ ] **Screenshots of local test cases** - -### Checklist -- [ ] **Code reviewed** -- [ ] **UT test coverage** (If not, explain reason: ____________________) -- [ ] **Involves public API changes in MindSpore Science** -- [ ] **Documentation updated** - -### Code Review Requirements -- Changes over 1000 lines require organized review meeting with conclusions -- PR without function validation cannot be merged -- PR with incomplete checklist cannot be merged -- PR without clear source identification or change description cannot be merged - -### Change Notification -- [ ] **Documentation modified** -- [ ] **API change description** (If API changed, detail description): \ No newline at end of file diff --git a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md b/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md deleted file mode 100644 index c166c30dc..000000000 --- a/.gitee/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md +++ /dev/null @@ -1,30 +0,0 @@ -### PR来源 -- [ ] issue单(请关联issue) -- [ ] 需求特性 -- [ ] 问题单 -- [ ] 社区开发者贡献 - -### 修改描述 -- **修改原因:** - -- **修改内容:** - -### 功能验证 -- [ ] **功能自验** -- [ ] **本地自验用例截图** - -### 检查清单 -- [ ] **是否经过代码检视** -- [ ] **是否具备UT测试用例看护**(如不符合,请说明原因:____________________) -- [ ] **是否涉及MindSpore Science公共接口变更** -- [ ] **是否涉及文档更新** - -### 代码检视要求 -- 合入代码超过1000行,需组织会议检视并附上结论 -- 未完成功能验证不允许合入 -- 未完成检查清单不允许合入 -- PR来源未标识或修改描述不清晰不允许合入 - -### 变更说明 -- [ ] **文档修改** -- [ ] **接口变更说明**(如涉及接口变更需详细描述): \ No newline at end of file -- Gitee From fe47bdeff72264226c1c2f7fcf2a6f1d34dd0d2d Mon Sep 17 00:00:00 2001 From: brian Date: Thu, 10 Jul 2025 19:48:53 +0800 Subject: [PATCH 20/30] fix --- .gitee/ISSUE_TEMPLATE/5-feature-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitee/ISSUE_TEMPLATE/5-feature-request.yml b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml index a5129a7b4..a51b2526c 100644 --- a/.gitee/ISSUE_TEMPLATE/5-feature-request.yml +++ b/.gitee/ISSUE_TEMPLATE/5-feature-request.yml @@ -39,7 +39,7 @@ body: attributes: label: 🚀 The feature, motivation and pitch description: > - A clear and concise description of the feature proposal. Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too. For design design, you can refer to [feature design template](https://gitee.com/mindspore/mindscience/blob/br_refactor/docs/template/feature_design.md). + A clear and concise description of the feature proposal. Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too. For feature design, you can refer to [feature design template](https://gitee.com/mindspore/mindscience/blob/br_refactor/docs/template/feature_design.md). validations: required: true - type: textarea -- Gitee From 5d542484ce28a27594f655644ec43169e1ca988e Mon Sep 17 00:00:00 2001 From: goto Date: Fri, 11 Jul 2025 16:16:48 +0800 Subject: [PATCH 21/30] Add API-mindflow-Fourier --- MindFlow/mindflow/core/__init__.py | 9 + MindFlow/mindflow/core/fourier.py | 713 ++++++++++++++++++++ tests/st/mindflow/operators/test_fourier.py | 228 +++++++ 3 files changed, 950 insertions(+) create mode 100644 MindFlow/mindflow/core/fourier.py create mode 100644 tests/st/mindflow/operators/test_fourier.py diff --git a/MindFlow/mindflow/core/__init__.py b/MindFlow/mindflow/core/__init__.py index df3b756b6..7595f98be 100644 --- a/MindFlow/mindflow/core/__init__.py +++ b/MindFlow/mindflow/core/__init__.py @@ -17,6 +17,7 @@ from .lr_scheduler import get_poly_lr, get_multi_step_lr, get_warmup_cosine_anne from .losses import get_loss_metric, WaveletTransformLoss, MTLWeightedLoss, RelativeRMSELoss from .derivatives import batched_hessian, batched_jacobian from .optimizers import AdaHessian +from .fourier import DFTn, IDFTn, RDFTn, IRDFTn, DCT, IDCT, DST, IDST __all__ = ["get_poly_lr", "get_multi_step_lr", @@ -28,6 +29,14 @@ __all__ = ["get_poly_lr", "batched_hessian", "batched_jacobian", "AdaHessian", + "DFTn", + "IDFTn", + "RDFTn", + "IRDFTn", + "DCT", + "IDCT", + "DST", + "IDST", ] __all__.sort() diff --git a/MindFlow/mindflow/core/fourier.py b/MindFlow/mindflow/core/fourier.py new file mode 100644 index 000000000..9425d32da --- /dev/null +++ b/MindFlow/mindflow/core/fourier.py @@ -0,0 +1,713 @@ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +''' provide complex dft based on the real dft API in mindflow.dft ''' +import numpy as np +from scipy.linalg import dft +import mindspore as ms +import mindspore.common.dtype as mstype +from mindspore import nn, ops, Tensor, mint +from mindspore.common.initializer import Zero +from mindspore.ops import operations as P + +from ..utils.check_func import check_param_no_greater, check_param_value, check_param_type, check_param_even + + +class MyRoll(nn.Cell): + ''' Custom defined roll operator to avoid bug in MindSpore ''' + def __init__(self): + super().__init__() + + if ms.get_context('device_target') == 'Ascend' and ms.get_context('mode') == ms.GRAPH_MODE: + self.roller = mint.roll + else: + self.roller = None + + def construct(self, x, shifts, dims): + ''' Same as mint.roll ''' + shifts = np.atleast_1d(shifts).astype(int).tolist() + dims = np.atleast_1d(dims).astype(int).tolist() + + if self.roller: + return self.roller(x, shifts, dims) + + for i, j in zip(shifts, dims): + n = x.shape[j] + x = ops.swapaxes(x, j, 0) + x = ops.cat([x[n - i % n:], x[:n - i % n]], axis=0) + x = ops.swapaxes(x, j, 0) + return x + +class MyFlip(nn.Cell): + ''' Custom defined flip operator to avoid bug in MindSpore ''' + def __init__(self): + super().__init__() + msver = tuple([int(s) for s in ms.__version__.split('.')]) + + if msver <= (2, 4, 0) and \ + ms.get_context('device_target') == 'Ascend' and \ + ms.get_context('mode') == ms.PYNATIVE_MODE: + self.fliper = None + else: + self.fliper = mint.flip + + def construct(self, x, dims): + ''' same as mint.flip ''' + dims = np.atleast_1d(dims).astype(int).tolist() + + if self.fliper: + return self.fliper(x, dims) + + for j in dims: + x = ops.swapaxes(x, j, 0) + x = x[::-1] + x = ops.swapaxes(x, j, 0) + return x + + +def convert_params(shape, modes, dim): + ''' convert input arguments to suitable format ''' + if dim is None: + ndim = len(shape) + dim = tuple([n - ndim for n in range(ndim)]) + else: + dim = tuple(np.atleast_1d(dim).astype(int).tolist()) + + shape = tuple(np.atleast_1d(shape).astype(int).tolist()) + modes = tuple(np.atleast_1d(modes).astype(int).tolist()) + + return shape, modes, dim + + +def check_params(shape, modes, dim): + ''' check lawfulness of input arguments ''' + check_param_type(dim, "dim", data_type=tuple) + check_param_type(shape, "shape", data_type=tuple) + check_param_type(modes, "modes", data_type=tuple) + check_param_no_greater(len(dim), "dim length", 3) + check_param_value(len(shape), "shape length", len(dim)) + check_param_value(len(modes), "modes length", len(dim)) + check_param_even(shape, "shape") + for i, (m, n) in enumerate(zip(modes, shape)): + check_param_no_greater(m, f'mode{i+1}', n // 2 + (i == len(dim) - 1)) + + +class _DFT1d(nn.Cell): + '''One dimensional Discrete Fourier Transformation''' + + def __init__(self, n, modes, last_index, idx=0, inv=False, compute_dtype=mstype.float32): + super().__init__() + + self.n = n + self.dft_mat = dft(n, scale="sqrtn") + self.modes = modes + self.last_index = last_index + self.inv = inv + self.idx = idx + self.compute_dtype = compute_dtype + + self.dft_mode_mat_upper = self.dft_mat[:, :modes] + self.a_re_upper = Tensor( + self.dft_mode_mat_upper.real, dtype=compute_dtype) + self.a_im_upper = Tensor( + self.dft_mode_mat_upper.imag, dtype=compute_dtype) + + self.dft_mode_mat_lower = self.dft_mat[:, -modes:] + self.a_re_lower = Tensor( + self.dft_mode_mat_lower.real, dtype=compute_dtype) + self.a_im_lower = Tensor( + self.dft_mode_mat_lower.imag, dtype=compute_dtype) + self.concat = ops.Concat(axis=-1) + + if self.inv: + self.a_re_upper = self.a_re_upper.T + self.a_im_upper = -self.a_im_upper.T + if last_index: + if modes == n // 2 + 1: + self.dft_mat_res = self.dft_mat[:, -modes + 2:] + else: + self.dft_mat_res = self.dft_mat[:, -modes + 1:] + + mat = Tensor(np.zeros(n,), dtype=compute_dtype).reshape(n, 1) + self.a_re_res = MyFlip()(Tensor(self.dft_mat_res.real, dtype=compute_dtype), dims=-1) + self.a_im_res = MyFlip()(Tensor(self.dft_mat_res.imag, dtype=compute_dtype), dims=-1) + if modes == n // 2 + 1: + self.a_re_res = self.concat((mat, self.a_re_res, mat)) + self.a_im_res = self.concat((mat, self.a_im_res, mat)) + else: + self.a_re_res = self.concat((mat, self.a_re_res)) + self.a_im_res = self.concat((mat, self.a_im_res)) + + self.a_re_res = self.a_re_res.T + self.a_im_res = -self.a_im_res.T + else: + self.a_re_res = self.a_re_lower.T + self.a_im_res = -self.a_im_lower.T + + if (self.n - 2 * self.modes) > 0: + self.mat = Tensor(shape=(self.n - 2 * self.modes), + dtype=compute_dtype, init=Zero()) + + def swap_axes(self, x_re, x_im): + return x_re.swapaxes(-1, self.idx), x_im.swapaxes(-1, self.idx) + + def complex_matmul(self, x_re, x_im, a_re, a_im): + y_re = ops.matmul(x_re, a_re) - ops.matmul(x_im, a_im) + y_im = ops.matmul(x_im, a_re) + ops.matmul(x_re, a_im) + return y_re, y_im + + def zero_mat(self, dims): + length = len(dims) + mat = self.mat + for i in range(length - 1, -1, -1): + mat = mint.repeat_interleave(mat.expand_dims(0), dims[i], 0) + return mat + + def compute_forward(self, x_re, x_im): + ''' Forward transform for rdft ''' + y_re, y_im = self.complex_matmul( + x_re=x_re, x_im=x_im, a_re=self.a_re_upper, a_im=self.a_im_upper) + + if self.last_index: + return y_re, y_im + + y_re2, y_im2 = self.complex_matmul( + x_re=x_re, x_im=x_im, a_re=self.a_re_lower, a_im=self.a_im_lower) + + if self.n == self.modes * 2: + y_re = self.concat((y_re, y_re2)) + y_im = self.concat((y_im, y_im2)) + else: + mat = self.zero_mat(x_re.shape[:-1]) + y_re = self.concat((y_re, mat, y_re2)) + y_im = self.concat((y_im, mat, y_im2)) + + return y_re, y_im + + def compute_inverse(self, x_re, x_im): + ''' Inverse transform for irdft ''' + y_re, y_im = self.complex_matmul(x_re=x_re[..., :self.modes], + x_im=x_im[..., :self.modes], + a_re=self.a_re_upper, + a_im=self.a_im_upper) + if self.last_index: + y_re_res, y_im_res = self.complex_matmul(x_re=x_re, + x_im=x_im, + a_re=self.a_re_res, + a_im=-self.a_im_res) + else: + y_re_res, y_im_res = self.complex_matmul(x_re=x_re[..., -self.modes:], + x_im=x_im[..., -self.modes:], + a_re=self.a_re_res, + a_im=self.a_im_res) + return y_re + y_re_res, y_im + y_im_res + + def construct(self, x): + ''' perform 1d rdft/irdft with matmul operations ''' + x_re, x_im = x + x_re, x_im = P.Cast()(x_re, self.compute_dtype), P.Cast()(x_im, self.compute_dtype) + x_re, x_im = self.swap_axes(x_re, x_im) + if self.inv: + y_re, y_im = self.compute_inverse(x_re, x_im) + else: + y_re, y_im = self.compute_forward(x_re, x_im) + y_re, y_im = self.swap_axes(y_re, y_im) + return y_re, y_im + + +class _DFTn(nn.Cell): + r""" + N dimensional Discrete Fourier Transformation + + Args: + shape (tuple): Dimension of the input 'x'. + modes (tuple): The length of the output transform axis. The `modes` must be no greater than half of the + dimension of input 'x'. + dim (tuple): Dimensions to be transformed. Default: None, the leading dimensions will be transformed. + inv (bool): Whether to compute inverse transformation. Default: False. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **x** (Tensor, Tensor): The input data. It's 3-D tuple of Tensor. It's a complex, + including x real and imaginary. Tensor of shape :math:`(*, *)`. + + Returns: + Complex tensor with the same shape of input x. + + Raises: + TypeError: If `shape` is not a tuple. + ValueError: If the length of `shape` is greater than 3. + """ + def __init__(self, shape, modes, dim=None, inv=False, compute_dtype=mstype.float32): + super().__init__() + + if dim is None: + dim = range(len(shape)) + self.dft1_seq = nn.SequentialCell() + last_index = [False for _ in range(len(shape))] + last_index[-1] = True + for dim_id, idx in enumerate(dim): + self.dft1_seq.append( + _DFT1d(n=shape[dim_id], modes=modes[dim_id], last_index=last_index[dim_id], idx=idx, inv=inv, + compute_dtype=compute_dtype)) + + def construct(self, x): + return self.dft1_seq(x) + + +class RDFTn(nn.Cell): + r""" + 1/2/3D discrete real Fourier transformation on real number. The results should be same as + `scipy.fft.rfftn() `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **ar** (Tensor) - The real tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **br** (Tensor) - Real part of the output tensor, with trailing dimensions aligned with `shape`, + except for the last dimension, which should be shape[-1] / 2 + 1. + - **bi** (Tensor) - Imag part of the output tensor, with trailing dimensions aligned with `shape`, + except for the last dimension, which should be shape[-1] / 2 + 1. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.core import RDFTn + >>> ar = ops.rand((2, 32, 512)) + >>> dft_cell = RDFTn(x.shape[-2:]) + >>> br, bi = dft_cell(ar) + >>> print(br.shape) + (2, 32, 257) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + + n = shape[-1] + ndim = len(shape) + modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 + + shape, modes, dim = convert_params(shape, modes, dim=None) + check_params(shape, modes, dim) + + self.n = n + self.ndim = ndim + self.shape = shape + self.scale = float(np.prod(shape) ** .5) + + self.dft_cell = _DFTn(shape, modes, dim, inv=False, compute_dtype=compute_dtype) + + def construct(self, ar): + ''' perform n-dimensional rDFT on real tensor ''' + # n-D Fourier transform with last axis being real-transformed, output dimension (..., m, n//2+1) + br, bi = self.dft_cell((ar, ar * 0)) # the last ndim dimensions of ar must accord with shape + return br * self.scale, bi * self.scale + + +class IRDFTn(nn.Cell): + r""" + 1/2/3D discrete inverse real Fourier transformation on complex number. The results should be same as + `scipy.fft.irfftn() `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **ar** (Tensor) - Real part of the tensor to be transformed, with trailing dimensions aligned with `shape`, + except for the last dimension, which should be shape[-1] / 2 + 1. + - **ai** (Tensor) - Imag part of the tensor to be transformed, with trailing dimensions aligned with `shape`, + except for the last dimension, which should be shape[-1] / 2 + 1. + + Outputs: + - **br** (Tensor) - The output real tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.core import IRDFTn + >>> ar = ops.rand((2, 32, 257)) + >>> ai = ops.rand((2, 32, 257)) + >>> dft_cell = IRDFTn(x.shape[-2:]) + >>> br = dft_cell(ar) + >>> print(br.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + + n = shape[-1] + ndim = len(shape) + modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 + + shape, modes, dim = convert_params(shape, modes, dim=None) + check_params(shape, modes, dim) + + self.n = n + self.ndim = ndim + self.shape = shape + self.scale = float(np.prod(self.shape) ** .5) + + self.idft_cell = _DFTn(shape, modes, dim, inv=True, compute_dtype=compute_dtype) + + def construct(self, ar, ai): + ''' perform n-dimensional irDFT on complex tensor and output real tensor ''' + br, _ = self.idft_cell((ar, ai)) + return br / self.scale + + +class DFTn(nn.Cell): + r""" + 1/2/3D discrete Fourier transformation on complex number. The results should be same as + `scipy.fft.fftn() `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **ar** (Tensor) - Real part of the tensor to be transformed, with trailing dimensions aligned with `shape`. + - **ai** (Tensor) - Imag part of the tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **br** (Tensor) - Real part of the output tensor, with trailing dimensions aligned with `shape`. + - **bi** (Tensor) - Imag part of the output tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import DFTn + >>> ar = ops.rand((2, 32, 512)) + >>> ai = ops.rand((2, 32, 512)) + >>> dft_cell = DFTn(x.shape[-2:]) + >>> br, bi = dft_cell(ar, ai) + >>> print(br.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + + n = shape[-1] + ndim = len(shape) + modes = tuple([_ // 2 for _ in shape[-ndim:-1]] + [n // 2 + 1]) if ndim > 1 else n // 2 + 1 + + shape, modes, dim = convert_params(shape, modes, dim=None) + check_params(shape, modes, dim) + + self.n = n + self.ndim = ndim + self.shape = shape + self.scale = float(np.prod(shape) ** .5) + + self.dft_cell = RDFTn(shape, compute_dtype) + + # use mask to assemble slices of Tensors, avoiding dynamic shape + mask_x0 = np.ones(self.n//2 + 1) + mask_xm = np.ones(self.n//2 + 1) + mask_y0 = np.ones(self.shape) + mask_z0 = np.ones(self.shape) + mask_x0[0] = 0 + mask_xm[-1] = 0 + if self.ndim > 1: + mask_y0[..., 0, :] = 0 + if self.ndim > 2: + mask_z0[..., 0, :, :] = 0 + + self.mask_x0 = Tensor(mask_x0, dtype=compute_dtype, const_arg=True) + self.mask_xm = Tensor(mask_xm, dtype=compute_dtype, const_arg=True) + self.mask_y0 = Tensor(mask_y0, dtype=compute_dtype, const_arg=True) + self.mask_z0 = Tensor(mask_z0, dtype=compute_dtype, const_arg=True) + + self.fliper = MyFlip() + self.roller = MyRoll() + + def construct(self, ar, ai): + ''' perform n-dimensional DFT on complex tensor ''' + # n-D complex Fourier transform, output dimension (..., m, n) + # call dft for real & imag parts separately and then assemble + brr, bri = self.dft_cell(ar) # ar and ai must have same shape + bir, bii = self.dft_cell(ai) # the last ndim dimensions of ai must accord with shape + + n = self.n + + br_half1 = ops.pad((brr - bii) * self.mask_xm, [0, n//2 - 1]) + bi_half1 = ops.pad((bri + bir) * self.mask_xm, [0, n//2 - 1]) + + br_half2 = ops.pad((brr + bii) * self.mask_x0, [n//2 - 1, 0]) + bi_half2 = ops.pad((bir - bri) * self.mask_x0, [n//2 - 1, 0]) + br_half2 = self.roller(self.fliper(br_half2, dims=-1), n//2, dims=-1) + bi_half2 = self.roller(self.fliper(bi_half2, dims=-1), n//2, dims=-1) + + if self.ndim > 1: + br_half2_1 = br_half2 * (1 - self.mask_y0) + bi_half2_1 = bi_half2 * (1 - self.mask_y0) + br_half2_2 = br_half2 * self.mask_y0 + bi_half2_2 = bi_half2 * self.mask_y0 + br_half2 = br_half2_1 + self.roller(self.fliper(br_half2_2, dims=-2), 1, dims=-2) + bi_half2 = bi_half2_1 + self.roller(self.fliper(bi_half2_2, dims=-2), 1, dims=-2) + + if self.ndim > 2: + br_half2_1 = br_half2 * (1 - self.mask_z0) + bi_half2_1 = bi_half2 * (1 - self.mask_z0) + br_half2_2 = br_half2 * self.mask_z0 + bi_half2_2 = bi_half2 * self.mask_z0 + br_half2 = br_half2_1 + self.roller(self.fliper(br_half2_2, dims=-3), 1, dims=-3) + bi_half2 = bi_half2_1 + self.roller(self.fliper(bi_half2_2, dims=-3), 1, dims=-3) + + br = br_half1 + br_half2 + bi = bi_half1 + bi_half2 + + return br, bi + + +class IDFTn(nn.Cell): + r""" + 1/2/3D discrete inverse Fourier transformation on complex number. The results should be same as + `scipy.fft.ifftn() `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **ar** (Tensor) - Real part of the tensor to be transformed, with trailing dimensions aligned with `shape`. + - **ai** (Tensor) - Imag part of the tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **br** (Tensor) - Real part of the output tensor, with trailing dimensions aligned with `shape`. + - **bi** (Tensor) - Imag part of the output tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import DFTn + >>> ar = ops.rand((2, 32, 512)) + >>> ai = ops.rand((2, 32, 512)) + >>> dft_cell = DFTn(x.shape[-2:]) + >>> br, bi = dft_cell(ar, ai) + >>> print(br.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + self.dft_cell = DFTn(shape, compute_dtype) + + def construct(self, ar, ai): + ''' perform n-dimensional iDFT on complex tensor ''' + scale = self.dft_cell.scale**2 + br, bi = self.dft_cell(ar, -ai) + return br / scale, -bi / scale + + +class DCT(nn.Cell): + r""" + 1D discrete cosine transformation on real number on the last axis. The results should be same as + `scipy.fft.dct() `_ . + Reference: `Type 2 DCT using N FFT (Makhoul) `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + Must be a length-1 tuple. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **a** (Tensor) - The real tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **b** (Tensor) - The output real tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import DCT + >>> a = ops.rand((2, 32, 512)) + >>> dft_cell = DCT(x.shape[-1:]) + >>> b = dft_cell(a) + >>> print(b.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + self.dft_cell = DFTn(shape, compute_dtype) + assert self.dft_cell.ndim == 1, 'only support 1D dct' + n, = self.dft_cell.shape + w = Tensor(np.arange(n) * np.pi / (2 * n), dtype=compute_dtype) + self.cosw = ops.cos(w) + self.sinw = ops.sin(w) + + self.fliper = MyFlip() + + def construct(self, a): + ''' perform 1-dimensional DCT on real tensor ''' + b_half1 = a[..., ::2] + b_half2 = self.fliper(a[..., 1::2], dims=-1) + b = ops.cat([b_half1, b_half2], axis=-1) + cr, ci = self.dft_cell(b, b * 0) + return 2 * (cr * self.cosw + ci * self.sinw) + + +class IDCT(nn.Cell): + r""" + 1D inverse discrete cosine transformation on real number on the last axis. The results should be same as + `scipy.fft.dct() `_ . + Reference: `A fast cosine transform in one and two dimensions + `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + Must be a length-1 tuple. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **a** (Tensor) - The real tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **b** (Tensor) - The output real tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import IDCT + >>> a = ops.rand((2, 32, 512)) + >>> dft_cell = IDCT(x.shape[-1:]) + >>> b = dft_cell(a) + >>> print(b.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + + self.dft_cell = IRDFTn(shape, compute_dtype) + assert self.dft_cell.ndim == 1, 'only support 1D dct' + n, = self.dft_cell.shape + assert n % 2 == 0, 'only support even length' # n has to be even, or IRDFTn would fail + + w = Tensor(np.arange(0, n // 2 + 1, 1) * np.pi / (2 * n), dtype=compute_dtype) + self.cosw = ops.cos(w) + self.sinw = ops.sin(w) + + self.fliper = MyFlip() + + def construct(self, a): + ''' perform 1-dimensional iDCT on real tensor ''' + n = a.shape[-1] + + br = a[..., :n // 2 + 1] + bi = ops.pad(self.fliper(- a[..., -(n // 2):], dims=-1), (1, 0)) + vr = (br * self.cosw - bi * self.sinw) / 2 + vi = (bi * self.cosw + br * self.sinw) / 2 + + c = self.dft_cell(vr, vi) # (..., n) + c1 = c[..., :(n + 1) // 2] + c2 = self.fliper(c[..., (n + 1) // 2:], dims=-1) + d1 = ops.pad(c1[..., None], (0, 1)).reshape(*c1.shape[:-1], -1) + d2 = ops.pad(c2[..., None], (1, 0)).reshape(*c2.shape[:-1], -1) + return d1 + d2 + + +class DST(nn.Cell): + r""" + 1D discrete sine transformation on real number on the last axis. The results should be same as + `scipy.fft.dct() `_ . + Reference: `Wikipedia `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + Must be a length-1 tuple. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **a** (Tensor) - The real tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **b** (Tensor) - The output real tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import DST + >>> a = ops.rand((2, 32, 512)) + >>> dft_cell = DST(x.shape[-1:]) + >>> b = dft_cell(a) + >>> print(b.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + self.dft_cell = DCT(shape, compute_dtype) + multiplier = np.ones(shape) + multiplier[..., 1::2] *= -1 + self.multiplier = Tensor(multiplier, dtype=compute_dtype) + + def construct(self, a): + ''' perform 1-dimensional DST on real tensor ''' + return self.dft_cell.fliper(self.dft_cell(a * self.multiplier), dims=-1) + + +class IDST(nn.Cell): + r""" + 1D inverse discrete sine transformation on real number on the last axis. The results should be same as + `scipy.fft.dct() `_ . + Reference: `Wikipedia `_ . + + Args: + shape (tuple): The shape of the dimensions to be transformed, other dimensions need not be included. + Must be a length-1 tuple. + compute_dtype (mindspore.dtype): The type of input tensor. Default: mindspore.float32. + + Inputs: + - **a** (Tensor) - The real tensor to be transformed, with trailing dimensions aligned with `shape`. + + Outputs: + - **b** (Tensor) - The output real tensor, with trailing dimensions aligned with `shape`. + + Supported Platforms: + ``Ascend`` ``CPU`` + + Examples: + >>> from mindspore import ops + >>> from mindflow.cell import IDST + >>> a = ops.rand((2, 32, 512)) + >>> dft_cell = IDST(x.shape[-1:]) + >>> b = dft_cell(a) + >>> print(b.shape) + (2, 32, 512) + """ + def __init__(self, shape, compute_dtype=mstype.float32): + super().__init__() + self.dft_cell = IDCT(shape, compute_dtype) + multiplier = np.ones(shape) + multiplier[..., 1::2] *= -1 + self.multiplier = Tensor(multiplier, dtype=compute_dtype) + + def construct(self, a): + ''' perform 1-dimensional iDST on real tensor ''' + return self.dft_cell(self.dft_cell.fliper(a, dims=-1)) * self.multiplier diff --git a/tests/st/mindflow/operators/test_fourier.py b/tests/st/mindflow/operators/test_fourier.py new file mode 100644 index 000000000..44f354cc8 --- /dev/null +++ b/tests/st/mindflow/operators/test_fourier.py @@ -0,0 +1,228 @@ +# ============================================================================ +# Copyright 2025 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Optimizers Test Case""" +import os +import random +import sys +from time import time as toc +import pytest +import numpy as np +from scipy.fft import dct, dst +import mindspore as ms +from mindspore import set_seed, ops +from mindflow import DFTn, IDFTn, RDFTn, IRDFTn, DCT, IDCT, DST, IDST + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")) +sys.path.append(PROJECT_ROOT) + +# pylint: disable=wrong-import-position + +from common.cell import FP32_RTOL +from common.cell.utils import compare_output + +# pylint: enable=wrong-import-position + +set_seed(0) +np.random.seed(0) +random.seed(0) + + +def gen_input(shape=(5, 6, 4, 8), rand_test=True): + ''' Generate random or deterministic tensor for input of the tests + ''' + a = np.random.rand(*shape) + 1j * np.random.rand(*shape) + if not rand_test: + a = sum([np.arange(n).reshape([n] + [1] * j) for j, n in enumerate(shape[::-1])]) + 1j * \ + sum([np.arange(n).reshape([n] + [1] * j) for j, n in enumerate(shape[::-1])]) + ar, ai = (ms.Tensor(a.real, dtype=ms.float32), ms.Tensor(a.imag, dtype=ms.float32)) + return a, ar, ai + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_rdft_accuracy(device_target, mode, ndim): + """ + Feature: Test RDFTn & IRDFTn accuracy + Description: Input random tensor, compare the results of RDFTn and IRDFTn with numpy results + Expectation: The output tensors should be equal within tolerance + """ + ms.set_context(device_target=device_target, mode=mode) + a, ar, _ = gen_input() + shape = a.shape + + b = np.fft.rfftn(a.real, s=a.shape[-ndim:], axes=range(-ndim, 0)) + br, bi = RDFTn(shape[-ndim:])(ar) + cr = IRDFTn(shape[-ndim:])(br, bi) + + assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(b.imag, bi.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_dft_accuracy(device_target, mode, ndim): + """ + Feature: Test DFTn & IDFTn accuracy + Description: Input random tensor, compare the results of DFTn and IDFTn with numpy results + Expectation: The output tensors should be equal within tolerance + """ + ms.set_context(device_target=device_target, mode=mode) + a, ar, ai = gen_input() + shape = a.shape + + b = np.fft.fftn(a, s=a.shape[-ndim:], axes=range(-ndim, 0)) + br, bi = DFTn(shape[-ndim:])(ar, ai) + cr, ci = IDFTn(shape[-ndim:])(br, bi) + + assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(b.imag, bi.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + assert compare_output(a.imag, ci.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_dct_accuracy(device_target, mode): + """ + Feature: Test DCT & IDCT accuracy + Description: Input random tensor, compare the results of DCT and IDCT with numpy results + Expectation: The output tensors should be equal within tolerance + """ + ms.set_context(device_target=device_target, mode=mode) + a, ar, _ = gen_input() + shape = a.shape + + b = dct(a.real) + br = DCT(shape[-1:])(ar) + cr = IDCT(shape[-1:])(br) + + assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_dst_accuracy(device_target, mode): + """ + Feature: Test DST & IDST accuracy + Description: Input random tensor, compare the results of DST and IDST with numpy results + Expectation: The output tensors should be equal within tolerance + """ + ms.set_context(device_target=device_target, mode=mode) + a, ar, _ = gen_input() + shape = a.shape + + b = dst(a.real) + br = DST(shape[-1:])(ar) + cr = IDST(shape[-1:])(br) + + assert compare_output(b.real, br.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(b)) + assert compare_output(a.real, cr.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(a)) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_dft_speed(device_target, mode, ndim): + """ + Feature: Test DFTn & IDFTn speed + Description: Input random tensor, clock the time of 10 runs of the + gradient function containing DFT & iDFT operators + Expectation: The average time of each run should be within 0.5s + """ + # test dftn & idftn speed + ms.set_context(device_target=device_target, mode=mode) + a, ar, ai = gen_input(shape=(64, 128, 256)) + shape = a.shape + + warmup_steps = 10 + timed_steps = 10 + + dft_cell = DFTn(shape[-ndim:]) + idft_cell = IDFTn(shape[-ndim:]) + + def forward_fn(xr, xi): + br, bi = dft_cell(xr, xi) + cr, ci = idft_cell(br, bi) + return ops.sum(cr * cr + ci * ci) + + grad_fn = ms.value_and_grad(forward_fn, grad_position=(0, 1)) + + # warmup run + for _ in range(warmup_steps): + _, (g1, g2) = grad_fn(ar, ai) + ar = ar - .1 * g1 + ai = ai - .1 * g2 + + # timed run + tic = toc() + for _ in range(timed_steps): + _, (g1, g2) = grad_fn(ar, ai) + ar = ar - .1 * g1 + ai = ai - .1 * g2 + + assert (toc() - tic) / timed_steps < 0.5 + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('device_target', ['CPU', 'Ascend']) +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +@pytest.mark.parametrize('ndim', [1, 2, 3]) +def test_dft_grad(device_target, mode, ndim): + """ + Feature: Test the correctness of DFTn & IDFTn grad calculation + Description: Input random tensor, compare the autograd results with theoretic solutions + Expectation: The autograd results should be equal to theoretic solutions + """ + ms.set_context(device_target=device_target, mode=mode) + a, ar, ai = gen_input() + shape = a.shape + + dft_cell = DFTn(shape[-ndim:]) + + def forward_fn(xr, xi): + yr, yi = dft_cell(xr, xi) + return ops.sum(yr * yr + yi * yi) + + grad_fn = ms.value_and_grad(forward_fn, grad_position=(0, 1)) + _, (g1, g2) = grad_fn(ar, ai) + + # analytic solution of the gradient + b = np.fft.fftn(a, s=a.shape[-ndim:], axes=range(-ndim, 0)) + g = np.fft.ifftn(b, s=a.shape[-ndim:], axes=range(-ndim, 0)) * 2 * np.prod(a.shape[-ndim:]) + + assert compare_output(g.real, g1.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(g)) + assert compare_output(g.imag, g2.numpy(), rtol=FP32_RTOL, atol=FP32_RTOL * np.linalg.norm(g)) -- Gitee From f36dbf4af16a276e148d22b902abac6bc5ebe810 Mon Sep 17 00:00:00 2001 From: brian Date: Fri, 11 Jul 2025 16:37:44 +0800 Subject: [PATCH 22/30] version adapation --- .../test/config/flow_config/dependent_packages.yaml | 2 +- MindFlow/mindflow/cell/neural_operators/dft.py | 10 +++++----- MindFlow/mindflow/cell/neural_operators/fno.py | 4 ++-- tests/st/mindflow/cfd/couette/test_couette.py | 2 +- tests/st/mindflow/networks/burgers/test_burgers.py | 2 +- tests/st/mindflow/networks/fno/test_fno.py | 2 +- .../navier_stokes/test_mindflow_navier_stokes.py | 2 +- tests/st/mindflow/networks/test_vit.py | 2 +- tests/st/mindflow/operators/test_dft.py | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.jenkins/test/config/flow_config/dependent_packages.yaml b/.jenkins/test/config/flow_config/dependent_packages.yaml index f850afdbb..9e70df642 100644 --- a/.jenkins/test/config/flow_config/dependent_packages.yaml +++ b/.jenkins/test/config/flow_config/dependent_packages.yaml @@ -1,2 +1,2 @@ mindspore: - '/mindspore/mindspore/version/202501/20250109/r2.4.1_20250109024509_cb55369bbd7d8008c0b817d4ea2f8cabe9ae1646/' + '/mindspore/mindspore/version/202503/20250326/master_20250326010019_b91eca2945e61641319f9887aa76a1ccb38604d3_newest/' \ No newline at end of file diff --git a/MindFlow/mindflow/cell/neural_operators/dft.py b/MindFlow/mindflow/cell/neural_operators/dft.py index 4993b6749..e41ba2b49 100644 --- a/MindFlow/mindflow/cell/neural_operators/dft.py +++ b/MindFlow/mindflow/cell/neural_operators/dft.py @@ -19,7 +19,7 @@ from scipy.linalg import dft import mindspore import mindspore.common.dtype as mstype -from mindspore import nn, ops, Tensor, Parameter +from mindspore import nn, ops, Tensor, Parameter, mint from mindspore.common.initializer import Zero from mindspore.ops import operations as P @@ -112,7 +112,7 @@ class DFT1d(nn.Cell): length = len(dims) mat = self.mat for i in range(length - 1, -1, -1): - mat = mat.expand_dims(0).repeat(dims[i], 0) + mat = mint.repeat_interleave(mat.expand_dims(0), dims[i], 0) y_re = self.concat((y_re, mat, y_re2)) y_im = self.concat((y_im, mat, y_im2)) @@ -612,7 +612,7 @@ class SpectralConv2dDft(SpectralConvDft): x_ft_im[:, :, -self.n_modes[0]:, :self.n_modes[1]], self._w_re2) batch_size = x.shape[0] - mat = self._mat.repeat(batch_size, 0) + mat = mint.repeat_interleave(self._mat, batch_size, 0) out_re = self._concat((out_ft_re1, mat, out_ft_re2)) out_im = self._concat((out_ft_im1, mat, out_ft_im2)) @@ -708,8 +708,8 @@ class SpectralConv3dDft(SpectralConvDft): :self.n_modes[2]], self._w_re4) batch_size = x.shape[0] - mat_x = self._mat_x.repeat(batch_size, 0) - mat_y = self._mat_y.repeat(batch_size, 0) + mat_x = mint.repeat_interleave(self._mat_x, batch_size, 0) + mat_y = mint.repeat_interleave(self._mat_y, batch_size, 0) out_re1 = ops.concat((out_ft_re1, mat_x, out_ft_re2), -3) out_im1 = ops.concat((out_ft_im1, mat_x, out_ft_im2), -3) diff --git a/MindFlow/mindflow/cell/neural_operators/fno.py b/MindFlow/mindflow/cell/neural_operators/fno.py index f013c6b32..8eda5b682 100644 --- a/MindFlow/mindflow/cell/neural_operators/fno.py +++ b/MindFlow/mindflow/cell/neural_operators/fno.py @@ -16,7 +16,7 @@ ''' # pylint: disable=W0235 -from mindspore import nn, ops, Tensor +from mindspore import nn, ops, Tensor, mint import mindspore.common.dtype as mstype from .dft import SpectralConv1dDft, SpectralConv2dDft, SpectralConv3dDft @@ -306,7 +306,7 @@ class FNO(nn.Cell): def construct(self, x: Tensor): """construct""" batch_size = x.shape[0] - grid = self._positional_embedding.repeat(batch_size, axis=0).astype(x.dtype) + grid = mint.repeat_interleave(self._positional_embedding.astype(x.dtype), batch_size, dim=0) if self.data_format != "channels_last": x = ops.transpose(x, input_perm=self._output_perm) if self.positional_embedding: diff --git a/tests/st/mindflow/cfd/couette/test_couette.py b/tests/st/mindflow/cfd/couette/test_couette.py index 6ff9aca0f..f5f7c218b 100644 --- a/tests/st/mindflow/cfd/couette/test_couette.py +++ b/tests/st/mindflow/cfd/couette/test_couette.py @@ -79,7 +79,7 @@ def train(): return err -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard def test_couette_gpu(): diff --git a/tests/st/mindflow/networks/burgers/test_burgers.py b/tests/st/mindflow/networks/burgers/test_burgers.py index 14776d3a7..407a01ad9 100644 --- a/tests/st/mindflow/networks/burgers/test_burgers.py +++ b/tests/st/mindflow/networks/burgers/test_burgers.py @@ -161,6 +161,6 @@ def test_mindflow_burgers_pinns(): eval_error = calculate_l2_error(model, inputs, label, 5) print("eval_error:", eval_error) - assert epoch_time < 0.01 + assert epoch_time < 0.05 assert train_loss < 0.6 assert eval_error < 0.8 diff --git a/tests/st/mindflow/networks/fno/test_fno.py b/tests/st/mindflow/networks/fno/test_fno.py index 7616cd36f..10e9ad45c 100644 --- a/tests/st/mindflow/networks/fno/test_fno.py +++ b/tests/st/mindflow/networks/fno/test_fno.py @@ -25,7 +25,7 @@ from mindflow.cell.neural_operators.dft import SpectralConv1dDft, SpectralConv2d RTOL = 0.001 set_seed(123456) -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard def test_fno_output(): diff --git a/tests/st/mindflow/networks/navier_stokes/test_mindflow_navier_stokes.py b/tests/st/mindflow/networks/navier_stokes/test_mindflow_navier_stokes.py index 0e6e71e02..1a8c827c9 100644 --- a/tests/st/mindflow/networks/navier_stokes/test_mindflow_navier_stokes.py +++ b/tests/st/mindflow/networks/navier_stokes/test_mindflow_navier_stokes.py @@ -120,5 +120,5 @@ def test_mindflow_navier_stokes(): f"epoch: {epoch} train loss: {train_loss} epoch time: {epoch_time}s") model.set_train(False) - assert epoch_time < 0.01 + assert epoch_time < 0.05 assert train_loss < 0.8 diff --git a/tests/st/mindflow/networks/test_vit.py b/tests/st/mindflow/networks/test_vit.py index 48b4ddbd9..262050bc1 100644 --- a/tests/st/mindflow/networks/test_vit.py +++ b/tests/st/mindflow/networks/test_vit.py @@ -22,7 +22,7 @@ from mindspore import dtype as mstype from mindflow.cell import ViT -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard def test_vit_output(): diff --git a/tests/st/mindflow/operators/test_dft.py b/tests/st/mindflow/operators/test_dft.py index d1d9741ac..e001cee12 100644 --- a/tests/st/mindflow/operators/test_dft.py +++ b/tests/st/mindflow/operators/test_dft.py @@ -87,7 +87,7 @@ def idft_2d_ms(x_re, x_im, shape, mode, dim=(-1)): return x_ms.asnumpy() -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard def test_dft1d(): @@ -107,7 +107,7 @@ def test_dft1d(): assert np.sum(x_torch1d - x_ms1d) < 0.001 -@pytest.mark.level1 +@pytest.mark.level0 @pytest.mark.platform_arm_ascend910b_training @pytest.mark.env_onecard def test_dft2d(): -- Gitee From ddf897594c9df4ab190e261ab7b05242a8859853 Mon Sep 17 00:00:00 2001 From: brian Date: Fri, 11 Jul 2025 17:10:12 +0800 Subject: [PATCH 23/30] format --- .gitee/ISSUE_TEMPLATE/6-application-case.yml | 4 ++-- .../mindflow/cell/mindflow.cell.MultiHeadAttention.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitee/ISSUE_TEMPLATE/6-application-case.yml b/.gitee/ISSUE_TEMPLATE/6-application-case.yml index dfbf8e154..6ea117ca9 100644 --- a/.gitee/ISSUE_TEMPLATE/6-application-case.yml +++ b/.gitee/ISSUE_TEMPLATE/6-application-case.yml @@ -1,5 +1,5 @@ -name: 🤗 Support request for a new application case of science -description: Submit a proposal/request for a new application case of science +name: 🤗 Support request for a new application case of AIForScience +description: Submit a proposal/request for a new application case of AIForScience title: "[Application Case]: " labels: ["application-case"] diff --git a/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst b/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst index 419893510..5d245889d 100644 --- a/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst +++ b/docs/api_python/mindflow/cell/mindflow.cell.MultiHeadAttention.rst @@ -10,7 +10,7 @@ mindflow.cell.MultiHeadAttention - **num_heads** (int) - 输出的输出特征维度。 - **enable_flash_attn** (bool) - 是否使能FlashAttention。FlashAttention只支持 `Ascend` 后端。具体细节参见 `FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness `_ 。 默认值: ``False`` 。 - - **fa_dtype** (mindspore.dtype): FlashAttention计算类型。支持以下类型: `mstype.bfloat16`、 `mstype.float16`。默认值: ``mstype.bfloat16`` ,表示 ``mindspore.bfloat16`` 。 + - **fa_dtype** (mindspore.dtype) - FlashAttention计算类型。支持以下类型: `mstype.bfloat16`、 `mstype.float16`。默认值: ``mstype.bfloat16`` ,表示 ``mindspore.bfloat16`` 。 - **drop_mode** (str) - dropout方式。默认值: ``dropout`` 。支持以下类型: ``dropout`` 和 ``droppath`` 。 - **dropout_rate** (float) - dropout层丢弃的比率。取值在 `[0, 1]` 。默认值: ``0.0`` 。 - **compute_dtype** (mindspore.dtype) - 网络层的数据类型。默认值: ``mstype.float32`` ,表示 ``mindspore.float32`` 。 -- Gitee From 10a7ff61ad1d8921953cfaa8535bf4ffbf9683b7 Mon Sep 17 00:00:00 2001 From: caiwen Date: Mon, 30 Jun 2025 19:37:37 +0800 Subject: [PATCH 24/30] add the module and tests of FFNO --- MindFlow/mindflow/cell/__init__.py | 6 +- .../cell/neural_operators/__init__.py | 3 +- .../mindflow/cell/neural_operators/ffno.py | 811 ++++++++++++++++++ .../mindflow/cell/neural_operators/ffno_sp.py | 468 ++++++++++ tests/st/mindflow/networks/ffno/test_ffno.py | 381 ++++++++ 5 files changed, 1666 insertions(+), 3 deletions(-) create mode 100644 MindFlow/mindflow/cell/neural_operators/ffno.py create mode 100644 MindFlow/mindflow/cell/neural_operators/ffno_sp.py create mode 100644 tests/st/mindflow/networks/ffno/test_ffno.py diff --git a/MindFlow/mindflow/cell/__init__.py b/MindFlow/mindflow/cell/__init__.py index d71670098..fb6dc6d8d 100644 --- a/MindFlow/mindflow/cell/__init__.py +++ b/MindFlow/mindflow/cell/__init__.py @@ -15,7 +15,8 @@ """init""" from .activation import get_activation from .basic_block import LinearBlock, ResBlock, InputScale, FCSequential, MultiScaleFCSequential, DropPath -from .neural_operators import FNO1D, FNO2D, FNO3D, KNO1D, KNO2D, PDENet, PeRCNN, SNO, SNO1D, SNO2D, SNO3D +from .neural_operators import (FNO1D, FNO2D, FNO3D, KNO1D, KNO2D, PDENet, PeRCNN, SNO, SNO1D, SNO2D, SNO3D, FFNO, + FFNO1D, FFNO2D, FFNO3D) from .attention import Attention, MultiHeadAttention, TransformerBlock from .vit import ViT from .unet2d import UNet2D @@ -26,6 +27,7 @@ from .diffusion_transformer import DiffusionTransformer, ConditionDiffusionTrans __all__ = ["get_activation", "FNO1D", "FNO2D", "FNO3D", "KNO1D", "KNO2D", "PDENet", "UNet2D", "PeRCNN", "SNO", "SNO1D", "SNO2D", "SNO3D", "Attention", "MultiHeadAttention", "TransformerBlock", "ViT", "DDPMPipeline", "DDIMPipeline", "DiffusionTrainer", "DiffusionScheduler", "DDPMScheduler", - "DDIMScheduler", "DiffusionTransformer", "ConditionDiffusionTransformer"] + "DDIMScheduler", "DiffusionTransformer", "ConditionDiffusionTransformer", + "FFNO", "FFNO1D", "FFNO2D", "FFNO3D"] __all__.extend(basic_block.__all__) __all__.extend(sno_utils.__all__) diff --git a/MindFlow/mindflow/cell/neural_operators/__init__.py b/MindFlow/mindflow/cell/neural_operators/__init__.py index 4498dba83..682152fd8 100644 --- a/MindFlow/mindflow/cell/neural_operators/__init__.py +++ b/MindFlow/mindflow/cell/neural_operators/__init__.py @@ -19,8 +19,9 @@ from .kno2d import KNO2D from .pdenet import PDENet from .percnn import PeRCNN from .sno import SNO, SNO1D, SNO2D, SNO3D +from .ffno import FFNOBlocks, FFNO, FFNO1D, FFNO2D, FFNO3D __all__ = ["FNOBlocks", "FNO1D", "FNO2D", "FNO3D", "KNO1D", "KNO2D", "PDENet", "PeRCNN", - "SNO", "SNO1D", "SNO2D", "SNO3D"] + "SNO", "SNO1D", "SNO2D", "SNO3D", "FFNOBlocks", "FFNO", "FFNO1D", "FFNO2D", "FFNO3D"] __all__.sort() diff --git a/MindFlow/mindflow/cell/neural_operators/ffno.py b/MindFlow/mindflow/cell/neural_operators/ffno.py new file mode 100644 index 000000000..8d8fe6694 --- /dev/null +++ b/MindFlow/mindflow/cell/neural_operators/ffno.py @@ -0,0 +1,811 @@ +'''' +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +''' +# pylint: disable=W0235 + +from mindspore import nn, ops, Tensor, Parameter, ParameterTuple, mint +from mindspore.common.initializer import XavierNormal, initializer +import mindspore.common.dtype as mstype + +from .ffno_sp import SpectralConv1d, SpectralConv2d, SpectralConv3d +from ...core.math import get_grid_1d, get_grid_2d, get_grid_3d +from ...utils.check_func import check_param_type + + +class FFNOBlocks(nn.Cell): + r""" + The FFNOBlock, which usually accompanied by a Lifting Layer ahead and a Projection Layer behind, + is a part of Factorized Fourier Neural Operator. It contains a Factorized Fourier Layer. The details can be found + in `A. Tran, A. Mathews, et. al: FACTORIZED FOURIER NEURAL OPERATORS `_. + + Args: + in_channels (int): The number of channels in the input space. + out_channels (int): The number of channels in the output space. + n_modes (Union[int, list(int)]): The number of modes reserved after linear transformation in Fourier Layer. + resolutions (Union[int, list(int)]): The resolutions of the input tensor. + factor (int): The number of neurons in the hidden layer of a feedforward network. Default: ``1``. + n_ff_layers (int): The number of layers (hidden layers) in the feedforward neural network. Default: ``2``. + ff_weight_norm (bool): Whether to do weight normalization in feedforward or not. Used as a reserved function + interface, the weight normalization is not supported in feedforward. Default: ``False``. + layer_norm (bool): Whether to do layer normalization in feedforward or not. Default: ``True``. + dropout (float): The value of percent be dropped when applying dropout regularization. Default: ``0.0``. + r_padding (int): The number used to pad a tensor on the right in a certain dimension. Pad the domain if + input is non-periodic. Default: ``0``. + use_fork (bool): Whether to perform forecasting or not. Default: ``False``. + forecast_ff (Feedforward): The feedforward network of generating "backcast" output. Default: ``None``. + backcast_ff (Feedforward): The feedforward network of generating "forecast" output. Default: ``None``. + fourier_weight (ParameterTuple[Parmemter]): The fourier weight for transforming data in the frequency + domain, with a ParameterTuple of Parmemter with a length of 2N. + + - Even indices (0, 2, 4, ...) represent the real parts of the complex parmemter. + - Odd indices (1, 3, 5, ...) represent the imaginary parts of the complex parmemter. + - Default: ``None``, meaning no data is provided. + dft_compute_dtype (dtype.Number): The computation type of DFT in SpectralConv. Default: ``mstype.float32``. + ffno_compute_dtype (dtype.Number): The computation type of MLP in ffno skip. Default: ``mstype.float16``. + Should be ``mstype.float32`` or ``mstype.float16``. mstype.float32 is recommended for the GPU backend, + mstype.float16 is recommended for the Ascend backend. + + Inputs: + - **x** (Tensor) - Tensor of shape :math:`(batch\_size, in\_channels, resolution)`. + + Outputs: + Tensor, the output of this FFNOBlocks. + + - **output** (Tensor) -Tensor of shape :math:`(batch\_size, out\_channels, resolution)`. + + Raises: + TypeError: If `in_channels` is not an int. + TypeError: If `out_channels` is not an int. + TypeError: If `factor` is not an int. + TypeError: If `n_ff_layers` is not an int. + TypeError: If `ff_weight_norm` is not a Boolean value. + ValueError: If `ff_weight_norm` is not ``False``. + TypeError: If `layer_norm` is not a Boolean value. + TypeError: If `dropout` is not a float. + TypeError: If `r_padding` is not an int. + TypeError: If `use_fork` is not a Boolean value. + + Supported Platforms: + ``Ascend`` + + Examples:` + >>> import numpy as np + >>> from mindspore import Tensor + >>> import mindspore.common.dtype as mstype + >>> from mindflow.cell.neural_operators import FFNOBlocks + >>> data = Tensor(np.ones([2, 128, 128, 2]), mstype.float32) + >>> net = FFNOBlocks(in_channels=2, out_channels=2, n_modes=[20, 20], resolutions=[128, 128]) + >>> out0, out1 = net(data) + >>> print(data.shape, out0.shape, out1.shape) + (2, 128, 128, 2) (2, 128, 128, 2) (2, 128, 128, 2) + """ + + def __init__(self, + in_channels, + out_channels, + n_modes, + resolutions, + factor=1, + n_ff_layers=2, + ff_weight_norm=False, + layer_norm=True, + dropout=0.0, + r_padding=0, + use_fork=False, + forecast_ff=None, + backcast_ff=None, + fourier_weight=None, + dft_compute_dtype=mstype.float32, + ffno_compute_dtype=mstype.float32 + ): + super().__init__() + check_param_type(in_channels, "in_channels", data_type=int) + check_param_type(out_channels, "out_channels", data_type=int) + self.in_channels = in_channels + self.out_channels = out_channels + self.n_modes, self.resolutions = validate_and_expand_dimensions( + 1, n_modes, resolutions, False) + + check_param_type(factor, "factor", data_type=int) + check_param_type(n_ff_layers, "n_ff_layers", data_type=int) + check_param_type(ff_weight_norm, "ff_weight_norm", data_type=bool) + check_param_type(layer_norm, "layer_norm", data_type=bool) + check_param_type(dropout, "dropout", data_type=float) + check_param_type(r_padding, 'r_padding', data_type=int) + + if ff_weight_norm: + raise ValueError( + f"The weight normalization is not supported in feedforward\ + but got value of ff_weight_norm {ff_weight_norm}") + + if r_padding < 0: + raise ValueError( + f"The right padding value cannot be negative\ + but got value of r_padding {r_padding}") + + check_param_type(use_fork, "use_fork", data_type=bool) + self.factor = factor + self.ff_weight_norm = ff_weight_norm + self.n_ff_layers = n_ff_layers + self.layer_norm = layer_norm + self.dropout = dropout + self.r_padding = r_padding + self.use_fork = use_fork + self.forecast_ff = forecast_ff + self.backcast_ff = backcast_ff + self.fourier_weight = fourier_weight + self.dft_compute_dtype = dft_compute_dtype + self.ffno_compute_dtype = ffno_compute_dtype + + if len(self.resolutions) == 1: + spectral_conv = SpectralConv1d + elif len(self.resolutions) == 2: + spectral_conv = SpectralConv2d + elif len(self.resolutions) == 3: + spectral_conv = SpectralConv3d + else: + raise ValueError( + f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {len(self.resolutions)}") + + self._convs = spectral_conv(self.in_channels, + self.out_channels, + self.n_modes, + self.resolutions, + forecast_ff=self.forecast_ff, + backcast_ff=self.backcast_ff, + fourier_weight=self.fourier_weight, + factor=self.factor, + ff_weight_norm=self.ff_weight_norm, + n_ff_layers=self.n_ff_layers, + layer_norm=self.layer_norm, + use_fork=self.use_fork, + dropout=self.dropout, + r_padding=self.r_padding, + compute_dtype=self.dft_compute_dtype, + filter_mode='full') + + def construct(self, x: Tensor): + b, _ = self._convs(x) + x = ops.add(x, b) + return x, b + + +def validate_and_expand_dimensions(dim, n_modes, resolutions, is_validate_dim=True): + """validate and expand the dimension of inputs""" + if isinstance(n_modes, int): + n_modes = [n_modes] * dim + if isinstance(resolutions, int): + resolutions = [resolutions] * dim + + n_modes_num = len(n_modes) + resolutions_num = len(resolutions) + + if is_validate_dim: + if n_modes_num != dim: + raise ValueError( + f"The dimension of n_modes should be equal to {dim} when using FFNO{dim}D\ + but got dimension of n_modes {n_modes_num}") + if resolutions_num != dim: + raise ValueError( + f"The dimension of resolutions should be equal to {dim} when using FFNO{dim}D\ + but got dimension of resolutions {resolutions_num}") + if n_modes_num != resolutions_num: + raise ValueError( + f"The dimension of n_modes should be equal to that of resolutions\ + but got dimension of n_modes {n_modes_num} and dimension of resolutions {resolutions_num}") + + return n_modes, resolutions + + +class FFNO(nn.Cell): + r""" + The FFNO base class, which usually contains a Lifting Layer, a Factorized Fourier Block Layer and a Projection + Layer. The details can be found in + `A. Tran, A. Mathews, et. al: FACTORIZED FOURIER NEURAL OPERATORS `_. + + Args: + in_channels (int): The number of channels in the input space. + out_channels (int): The number of channels in the output space. + n_modes (Union[int, list(int)]): The number of modes reserved after linear transformation in Fourier Layer. + resolutions (Union[int, list(int)]): The resolutions of the input tensor. + hidden_channels (int): The number of channels of the FNOBlock input and output. Default: ``20``. + lifting_channels (int): The number of channels of the lifting layer mid channels. Default: None. + projection_channels (int): The number of channels of the projection layer mid channels. Default: ``128``. + factor (int): The number of neurons in the hidden layer of a feedforward network. Default: ``1``. + n_layers (int): The number that Fourier Layer nests. Default: ``4``. + n_ff_layers (int): The number of layers (hidden layers) in the feedforward neural network. Default: ``2``. + ff_weight_norm (bool): Whether to do weight normalization in feedforward or not. Used as a reserved function + interface, the weight normalization is not supported in feedforward. Default: ``False``. + layer_norm (bool): Whether to do layer normalization in feedforward or not. Default: ``True``. + share_weight (bool): Whether to share weights between SpectralConv layers or not. Default: ``False``. + r_padding (int): The number used to pad a tensor on the right in a certain dimension. Pad the domain if + input is non-periodic. Default: ``0``. + data_format (str): The input data channel sequence. Default: ``channels_last``. + positional_embedding (bool): Whether to embed positional information or not. Default: ``True``. + dft_compute_dtype (dtype.Number): The computation type of DFT in SpectralConvDft. Default: ``mstype.float32``. + ffno_compute_dtype (dtype.Number): The computation type of MLP in fno skip. Default: ``mstype.float16``. + Should be ``mstype.float32`` or ``mstype.float16``. mstype.float32 is recommended for + the GPU backend, mstype.float16 is recommended for the Ascend backend. + + Inputs: + - **x** (Tensor) - Tensor of shape :math:`(batch\_size, resolution, in\_channels)`. + + Outputs: + Tensor, the output of this FNOBlocks. + + - **output** (Tensor) -Tensor of shape :math:`(batch\_size, resolution, out\_channels)`. + + Raises: + TypeError: If `in_channels` is not an int. + TypeError: If `out_channels` is not an int. + TypeError: If `hidden_channels` is not an int. + TypeError: If `lifting_channels` is not an int. + TypeError: If `projection_channels` is not an int. + TypeError: If `factor` is not an int. + TypeError: If `n_layers` is not an int. + TypeError: If `n_ff_layers` is not an int. + TypeError: If `ff_weight_norm` is not a Boolean value. + ValueError: If `ff_weight_norm` is not ``False``. + TypeError: If `layer_norm` is not a Boolean value. + TypeError: If `share_weight` is not a Boolean value. + TypeError: If `r_padding` is not an int. + TypeError: If `data_format` is not a str. + TypeError: If `positional_embedding` is not a bool. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> from mindspore import Tensor + >>> import mindspore.common.dtype as mstype + >>> from mindflow.cell.neural_operators.ffno import FFNO + >>> data = Tensor(np.ones([2, 128, 128, 2]), mstype.float32) + >>> net = FFNO(in_channels=2, out_channels=2, n_modes=[20, 20], resolutions=[128, 128]) + >>> out = net(data) + >>> print(data.shape, out.shape) + (2, 128, 128, 2) (2, 128, 128, 2) + """ + + def __init__( + self, + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels=20, + lifting_channels=None, + projection_channels=128, + factor=1, + n_layers=4, + n_ff_layers=2, + ff_weight_norm=False, + layer_norm=True, + share_weight=False, + r_padding=0, + data_format="channels_last", + positional_embedding=True, + dft_compute_dtype=mstype.float32, + ffno_compute_dtype=mstype.float16 + ): + super().__init__() + check_param_type(in_channels, "in_channels", data_type=int, exclude_type=bool) + check_param_type(out_channels, "out_channels", data_type=int, exclude_type=bool) + check_param_type(hidden_channels, "hidden_channels", data_type=int, exclude_type=bool) + check_param_type(factor, "factor", data_type=int, exclude_type=bool) + check_param_type(n_layers, "n_layers", data_type=int, exclude_type=bool) + check_param_type(n_ff_layers, "n_ff_layers", data_type=int, exclude_type=bool) + check_param_type(ff_weight_norm, "ff_weight_norm", data_type=bool, exclude_type=str) + check_param_type(layer_norm, "layer_norm", data_type=bool, exclude_type=str) + check_param_type(share_weight, "share_weight", data_type=bool, exclude_type=str) + check_param_type(r_padding, "r_padding", data_type=int, exclude_type=bool) + check_param_type(data_format, "data_format", data_type=str, exclude_type=bool) + check_param_type(positional_embedding, "positional_embedding", data_type=bool, exclude_type=str) + + if ff_weight_norm: + raise ValueError( + f"The weight normalization is not supported in feedforward\ + but got value of ff_weight_norm {ff_weight_norm}") + + if r_padding < 0: + raise ValueError(f"The right padding value cannot be negative but got value of r_padding {r_padding}") + + self.in_channels = in_channels + self.out_channels = out_channels + self.hidden_channels = hidden_channels + self.lifting_channels = lifting_channels + self.projection_channels = projection_channels + self.n_modes, self.resolutions = validate_and_expand_dimensions( + 1, n_modes, resolutions, False) + self.n_layers = n_layers + self.r_padding = r_padding + self.data_format = data_format + self.positional_embedding = positional_embedding + if self.positional_embedding: + self.in_channels += len(self.resolutions) + self.dft_compute_dtype = dft_compute_dtype + self.ffno_compute_dtype = ffno_compute_dtype + self._concat = ops.Concat(axis=-1) + self._positional_embedding, self._input_perm, self._output_perm = self._transpose(len(self.resolutions)) + self._padding = self._pad(len(self.resolutions)) + if self.lifting_channels: + self._lifting = nn.SequentialCell([ + nn.Dense(self.in_channels, self.lifting_channels, has_bias=True).to_float(self.ffno_compute_dtype), + nn.Dense(self.lifting_channels, self.hidden_channels, has_bias=True).to_float(self.ffno_compute_dtype)]) + else: + self._lifting = nn.SequentialCell( + nn.Dense(self.in_channels, self.hidden_channels, has_bias=True).to_float(self.ffno_compute_dtype) + ) + + self.fourier_weight = None + if share_weight: + param_list = [] + for i, n_mode in enumerate(self.n_modes): + weight_shape = [hidden_channels, hidden_channels, n_mode] + + w_re = Parameter(initializer(XavierNormal(), weight_shape, mstype.float32), name=f'base_w_re_{i}', + requires_grad=True) + w_im = Parameter(initializer(XavierNormal(), weight_shape, mstype.float32), name=f'base_w_im_{i}', + requires_grad=True) + + param_list.append(w_re) + param_list.append(w_im) + + self.fourier_weight = ParameterTuple([param for param in param_list]) + + self.factor = factor + self.ff_weight_norm = ff_weight_norm + self.n_ff_layers = n_ff_layers + self.layer_norm = layer_norm + + self._ffno_blocks = nn.CellList([FFNOBlocks(in_channels=self.hidden_channels, + out_channels=self.hidden_channels, + n_modes=self.n_modes, + resolutions=self.resolutions, + factor=self.factor, + n_ff_layers=self.n_ff_layers, + ff_weight_norm=self.ff_weight_norm, + layer_norm=self.layer_norm, + dropout=0.0, r_padding=self.r_padding, + use_fork=False, forecast_ff=None, backcast_ff=None, + fourier_weight=self.fourier_weight, + dft_compute_dtype=self.dft_compute_dtype + ) for _ in range(self.n_layers)]) + + if self.projection_channels: + self._projection = nn.SequentialCell([ + nn.Dense(self.hidden_channels, self.projection_channels, has_bias=True).to_float( + self.ffno_compute_dtype), + nn.Dense(self.projection_channels, self.out_channels, has_bias=True).to_float( + self.ffno_compute_dtype) + ]) + else: + self._projection = nn.SequentialCell( + nn.Dense(self.hidden_channels, self.out_channels, has_bias=True).to_float( + self.ffno_compute_dtype)) + + def construct(self, x: Tensor): + """construct""" + batch_size = x.shape[0] + grid = mint.repeat_interleave(self._positional_embedding.astype(x.dtype), repeats=batch_size, dim=0) + if self.data_format != "channels_last": + x = ops.transpose(x, input_perm=self._output_perm) + if self.positional_embedding: + x = self._concat((x, grid)) + + x = self._lifting(x) + x = ops.transpose(x, input_perm=self._input_perm) + if self.r_padding != 0: + x = ops.Pad(self._padding)(x) + + x = ops.transpose(x, input_perm=self._output_perm) + + b = Tensor(0, dtype=mstype.float32) + for block in self._ffno_blocks: + x, b = block(x) + if self.r_padding != 0: + b = self._remove_padding(len(self.resolutions), b) + x = self._projection(b) + if self.data_format != "channels_last": + x = ops.transpose(x, input_perm=self._input_perm) + return x + + def _transpose(self, n_dim): + """transpose tensor""" + if n_dim == 1: + positional_embedding = Tensor(get_grid_1d(resolution=self.resolutions)) + input_perm = (0, 2, 1) + output_perm = (0, 2, 1) + elif n_dim == 2: + positional_embedding = Tensor(get_grid_2d(resolution=self.resolutions)) + input_perm = (0, 3, 1, 2) + output_perm = (0, 2, 3, 1) + elif n_dim == 3: + positional_embedding = Tensor(get_grid_3d(resolution=self.resolutions)) + input_perm = (0, 4, 1, 2, 3) + output_perm = (0, 2, 3, 4, 1) + else: + raise ValueError(f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {n_dim}") + return positional_embedding, input_perm, output_perm + + def _pad(self, n_dim): + """pad the domain if input is non-periodic""" + if n_dim == 1: + pad = ([0, 0], [0, 0], [0, self.r_padding]) + elif n_dim == 2: + pad = ([0, 0], [0, 0], [0, self.r_padding], [0, self.r_padding]) + elif n_dim == 3: + pad = ([0, 0], [0, 0], [0, self.r_padding], [0, self.r_padding], [0, self.r_padding]) + else: + raise ValueError(f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {n_dim}") + return pad + + def _remove_padding(self, n_dim, b_input): + """remove pad domain""" + if n_dim == 1: + b = b_input[..., :-self.r_padding, :] + elif n_dim == 2: + b = b_input[..., :-self.r_padding, :-self.r_padding, :] + elif n_dim == 3: + b = b_input[..., :-self.r_padding, :-self.r_padding, :-self.r_padding, :] + else: + raise ValueError(f"The length of input resolutions dimensions should be in [1, 2, 3], but got: {n_dim}") + return b + + +class FFNO1D(FFNO): + r""" + The 1D Factorized Fourier Neural Operator, which usually contains a Lifting Layer, + a Factorized Fourier Block Layer and a Projection Layer. The details can be found in + `A. Tran, A. Mathews, et. al: FACTORIZED FOURIER NEURAL OPERATORS `_. + + Args: + in_channels (int): The number of channels in the input space. + out_channels (int): The number of channels in the output space. + n_modes (Union[int, list(int)]): The number of modes reserved after linear transformation in Fourier Layer. + resolutions (Union[int, list(int)]): The resolutions of the input tensor. + hidden_channels (int): The number of channels of the FNOBlock input and output. Default: ``20``. + lifting_channels (int): The number of channels of the lifting layer mid channels. Default: None. + projection_channels (int): The number of channels of the projection layer mid channels. Default: ``128``. + factor (int): The number of neurons in the hidden layer of a feedforward network. Default: ``1``. + n_layers (int): The number that Fourier Layer nests. Default: ``4``. + n_ff_layers (int): The number of layers (hidden layers) in the feedforward neural network. Default: ``2``. + ff_weight_norm (bool): Whether to do weight normalization in feedforward or not. Used as a reserved function + interface, the weight normalization is not supported in feedforward. Default: ``False``. + layer_norm (bool): Whether to do layer normalization in feedforward or not. Default: ``True``. + share_weight (bool): Whether to share weights between SpectralConv layers or not. Default: ``False``. + r_padding (int): The number used to pad a tensor on the right in a certain dimension. Default: ``0``. + data_format (str): The input data channel sequence. Default: ``channels_last``. + positional_embedding (bool): Whether to embed positional information or not. Default: ``True``. + dft_compute_dtype (dtype.Number): The computation type of DFT in SpectralConvDft. Default: ``mstype.float32``. + ffno_compute_dtype (dtype.Number): The computation type of MLP in fno skip. Default: ``mstype.float16``. + Should be ``mstype.float32`` or ``mstype.float16``. mstype.float32 is recommended for + the GPU backend, mstype.float16 is recommended for the Ascend backend. + + Inputs: + - **x** (Tensor) - Tensor of shape :math:`(batch\_size, resolution, in\_channels)`. + + Outputs: + Tensor, the output of this FNOBlocks. + + - **output** (Tensor) -Tensor of shape :math:`(batch\_size, resolution, out\_channels)`. + + Raises: + TypeError: If `in_channels` is not an int. + TypeError: If `out_channels` is not an int. + TypeError: If `hidden_channels` is not an int. + TypeError: If `lifting_channels` is not an int. + TypeError: If `projection_channels` is not an int. + TypeError: If `factor` is not an int. + TypeError: If `n_layers` is not an int. + TypeError: If `n_ff_layers` is not an int. + TypeError: If `ff_weight_norm` is not a Boolean value. + ValueError: If `ff_weight_norm` is not ``False``. + TypeError: If `layer_norm` is not a Boolean value. + TypeError: If `share_weight` is not a Boolean value. + TypeError: If `r_padding` is not an int. + TypeError: If `data_format` is not a str. + TypeError: If `positional_embedding` is not a bool. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> import mindflow + >>> from mindspore import Tensor + >>> import mindspore.common.dtype as mstype + >>> from mindflow.cell import FFNO1D + >>> data = Tensor(np.ones([2, 128, 3]), mstype.float32) + >>> net = FFNO1D(in_channels=3, out_channels=3, n_modes=[20], resolutions=[128]) + >>> out = net(data) + >>> print(data.shape, out.shape) + (2, 128, 3) (2, 128, 3) + """ + + def __init__( + self, + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels=20, + lifting_channels=None, + projection_channels=128, + factor=1, + n_layers=4, + n_ff_layers=2, + ff_weight_norm=False, + layer_norm=True, + share_weight=False, + r_padding=0, + data_format="channels_last", + positional_embedding=True, + dft_compute_dtype=mstype.float32, + ffno_compute_dtype=mstype.float16 + ): + n_modes, resolutions = validate_and_expand_dimensions(1, n_modes, resolutions) + super().__init__( + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels, + lifting_channels, + projection_channels, + factor, + n_layers, + n_ff_layers, + ff_weight_norm, + layer_norm, + share_weight, + r_padding, + data_format, + positional_embedding, + dft_compute_dtype, + ffno_compute_dtype + ) + + +class FFNO2D(FFNO): + r""" + The 2D Factorized Fourier Neural Operator, which usually contains a Lifting Layer, + a Factorized Fourier Block Layer and a Projection Layer. The details can be found in + `A. Tran, A. Mathews, et. al: FACTORIZED FOURIER NEURAL OPERATORS `_. + + Args: + in_channels (int): The number of channels in the input space. + out_channels (int): The number of channels in the output space. + n_modes (Union[int, list(int)]): The number of modes reserved after linear transformation in Fourier Layer. + resolutions (Union[int, list(int)]): The resolutions of the input tensor. + hidden_channels (int): The number of channels of the FNOBlock input and output. Default: ``20``. + lifting_channels (int): The number of channels of the lifting layer mid channels. Default: None. + projection_channels (int): The number of channels of the projection layer mid channels. Default: ``128``. + factor (int): The number of neurons in the hidden layer of a feedforward network. Default: ``1``. + n_layers (int): The number that Fourier Layer nests. Default: ``4``. + n_ff_layers (int): The number of layers (hidden layers) in the feedforward neural network. Default: ``2``. + ff_weight_norm (bool): Whether to do weight normalization in feedforward or not. Used as a reserved function + interface, the weight normalization is not supported in feedforward. Default: ``False``. + layer_norm (bool): Whether to do layer normalization in feedforward or not. Default: ``True``. + share_weight (bool): Whether to share weights between SpectralConv layers or not. Default: ``False``. + r_padding (int): The number used to pad a tensor on the right in a certain dimension. Default: ``0``. + data_format (str): The input data channel sequence. Default: ``channels_last``. + positional_embedding (bool): Whether to embed positional information or not. Default: ``True``. + dft_compute_dtype (dtype.Number): The computation type of DFT in SpectralConvDft. Default: ``mstype.float32``. + ffno_compute_dtype (dtype.Number): The computation type of MLP in fno skip. Default: ``mstype.float16``. + Should be ``mstype.float32`` or ``mstype.float16``. mstype.float32 is recommended for + the GPU backend, mstype.float16 is recommended for the Ascend backend. + + Inputs: + - **x** (Tensor) - Tensor of shape :math:`(batch\_size, resolution, in\_channels)`. + + Outputs: + Tensor, the output of this FNOBlocks. + + - **output** (Tensor) -Tensor of shape :math:`(batch\_size, resolution, out\_channels)`. + + Raises: + TypeError: If `in_channels` is not an int. + TypeError: If `out_channels` is not an int. + TypeError: If `hidden_channels` is not an int. + TypeError: If `lifting_channels` is not an int. + TypeError: If `projection_channels` is not an int. + TypeError: If `factor` is not an int. + TypeError: If `n_layers` is not an int. + TypeError: If `n_ff_layers` is not an int. + TypeError: If `ff_weight_norm` is not a Boolean value. + ValueError: If `ff_weight_norm` is not ``False``. + TypeError: If `layer_norm` is not a Boolean value. + TypeError: If `share_weight` is not a Boolean value. + TypeError: If `r_padding` is not an int. + TypeError: If `data_format` is not a str. + TypeError: If `positional_embedding` is not a bool. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> import mindflow + >>> from mindspore import Tensor + >>> import mindspore.common.dtype as mstype + >>> from mindflow.cell import FFNO2D + >>> data = Tensor(np.ones([2, 128, 128, 3]), mstype.float32) + >>> net = FFNO2D(in_channels=3, out_channels=3, n_modes=[20, 20], resolutions=[128, 128]) + >>> out = net(data) + >>> print(data.shape, out.shape) + (2, 128, 128, 3) (2, 128, 128, 3) + """ + + def __init__( + self, + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels=20, + lifting_channels=None, + projection_channels=128, + factor=1, + n_layers=4, + n_ff_layers=2, + ff_weight_norm=False, + layer_norm=True, + share_weight=False, + r_padding=0, + data_format="channels_last", + positional_embedding=True, + dft_compute_dtype=mstype.float32, + ffno_compute_dtype=mstype.float16 + ): + n_modes, resolutions = validate_and_expand_dimensions(2, n_modes, resolutions) + super().__init__( + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels, + lifting_channels, + projection_channels, + factor, + n_layers, + n_ff_layers, + ff_weight_norm, + layer_norm, + share_weight, + r_padding, + data_format, + positional_embedding, + dft_compute_dtype, + ffno_compute_dtype + ) + + +class FFNO3D(FFNO): + r""" + The 3D Factorized Fourier Neural Operator, which usually contains a Lifting Layer, + a Factorized Fourier Block Layer and a Projection Layer. The details can be found in + `A. Tran, A. Mathews, et. al: FACTORIZED FOURIER NEURAL OPERATORS `_. + + Args: + in_channels (int): The number of channels in the input space. + out_channels (int): The number of channels in the output space. + n_modes (Union[int, list(int)]): The number of modes reserved after linear transformation in Fourier Layer. + resolutions (Union[int, list(int)]): The resolutions of the input tensor. + hidden_channels (int): The number of channels of the FNOBlock input and output. Default: ``20``. + lifting_channels (int): The number of channels of the lifting layer mid channels. Default: None. + projection_channels (int): The number of channels of the projection layer mid channels. Default: ``128``. + factor (int): The number of neurons in the hidden layer of a feedforward network. Default: ``1``. + n_layers (int): The number that Fourier Layer nests. Default: ``4``. + n_ff_layers (int): The number of layers (hidden layers) in the feedforward neural network. Default: ``2``. + ff_weight_norm (bool): Whether to do weight normalization in feedforward or not. Used as a reserved function + interface, the weight normalization is not supported in feedforward. Default: ``False``. + layer_norm (bool): Whether to do layer normalization in feedforward or not. Default: ``True``. + share_weight (bool): Whether to share weights between SpectralConv layers or not. Default: ``False``. + r_padding (int): The number used to pad a tensor on the right in a certain dimension. Default: ``0``. + data_format (str): The input data channel sequence. Default: ``channels_last``. + positional_embedding (bool): Whether to embed positional information or not. Default: ``True``. + dft_compute_dtype (dtype.Number): The computation type of DFT in SpectralConvDft. Default: ``mstype.float32``. + ffno_compute_dtype (dtype.Number): The computation type of MLP in fno skip. Default: ``mstype.float16``. + Should be ``mstype.float32`` or ``mstype.float16``. mstype.float32 is recommended for + the GPU backend, mstype.float16 is recommended for the Ascend backend. + + Inputs: + - **x** (Tensor) - Tensor of shape :math:`(batch\_size, resolution, in\_channels)`. + + Outputs: + Tensor, the output of this FNOBlocks. + + - **output** (Tensor) -Tensor of shape :math:`(batch\_size, resolution, out\_channels)`. + + Raises: + TypeError: If `in_channels` is not an int. + TypeError: If `out_channels` is not an int. + TypeError: If `hidden_channels` is not an int. + TypeError: If `lifting_channels` is not an int. + TypeError: If `projection_channels` is not an int. + TypeError: If `factor` is not an int. + TypeError: If `n_layers` is not an int. + TypeError: If `n_ff_layers` is not an int. + TypeError: If `ff_weight_norm` is not a Boolean value. + ValueError: If `ff_weight_norm` is not ``False``. + TypeError: If `layer_norm` is not a Boolean value. + TypeError: If `share_weight` is not a Boolean value. + TypeError: If `r_padding` is not an int. + TypeError: If `data_format` is not a str. + TypeError: If `positional_embedding` is not a bool. + + Supported Platforms: + ``Ascend`` + + Examples: + >>> import numpy as np + >>> import mindspore + >>> import mindflow + >>> from mindspore import Tensor + >>> import mindspore.common.dtype as mstype + >>> from mindflow.cell import FFNO3D + >>> data = Tensor(np.ones([2, 128, 128, 128, 3]), mstype.float32) + >>> net = FFNO3D(in_channels=3, out_channels=3, n_modes=[20, 20, 20], resolutions=[128, 128, 128]) + >>> out = net(data) + >>> print(data.shape, out.shape) + (2, 128, 128, 128, 3) (2, 128, 128, 128, 3) + """ + + def __init__( + self, + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels=20, + lifting_channels=None, + projection_channels=128, + factor=1, + n_layers=4, + n_ff_layers=2, + ff_weight_norm=False, + layer_norm=True, + share_weight=False, + r_padding=0, + data_format="channels_last", + positional_embedding=True, + dft_compute_dtype=mstype.float32, + ffno_compute_dtype=mstype.float16 + ): + n_modes, resolutions = validate_and_expand_dimensions(3, n_modes, resolutions) + super().__init__( + in_channels, + out_channels, + n_modes, + resolutions, + hidden_channels, + lifting_channels, + projection_channels, + factor, + n_layers, + n_ff_layers, + ff_weight_norm, + layer_norm, + share_weight, + r_padding, + data_format, + positional_embedding, + dft_compute_dtype, + ffno_compute_dtype + ) diff --git a/MindFlow/mindflow/cell/neural_operators/ffno_sp.py b/MindFlow/mindflow/cell/neural_operators/ffno_sp.py new file mode 100644 index 000000000..8ad65613c --- /dev/null +++ b/MindFlow/mindflow/cell/neural_operators/ffno_sp.py @@ -0,0 +1,468 @@ +'''' +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +''' +import mindspore as ms +import mindspore.common.dtype as mstype +from mindspore import nn, ops, Tensor, Parameter, ParameterTuple, mint +from mindspore.common.initializer import XavierNormal, initializer +from ...core.math import get_grid_1d, get_grid_2d, get_grid_3d +from .dft import dft1, idft1 + + +class FeedForward(nn.Cell): + """FeedForward cell""" + + def __init__(self, dim, factor, ff_weight_norm, n_layers, layer_norm, dropout): + super().__init__() + self.layers = nn.CellList() + for i in range(n_layers): + in_dim = dim if i == 0 else dim * factor + out_dim = dim if i == n_layers - 1 else dim * factor + layer = nn.SequentialCell([ + nn.Dense(in_dim, out_dim, has_bias=True) if not ff_weight_norm else nn.Identity(), + nn.Dropout(p=dropout), + nn.ReLU() if i < n_layers - 1 else nn.Identity(), + nn.LayerNorm((out_dim,), epsilon=1e-5) if layer_norm and i == n_layers - 1 else nn.Identity()]) + self.layers.append(layer) + + def construct(self, x): + for layer in self.layers: + x = layer(x) + return x + + +class SpectralConv(nn.Cell): + """Base Class for Fourier Layer, including DFT, factorization, linear transform, and Inverse DFT""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, + fourier_weight, factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, filter_mode, + compute_dtype=mstype.float32): + super().__init__() + self.einsum_flag = tuple([int(s) for s in ms.__version__.split('.')]) >= (2, 5, 0) + self.in_channels = in_channels + self.out_channels = out_channels + if isinstance(n_modes, int): + n_modes = [n_modes] + self.n_modes = n_modes + if isinstance(resolutions, int): + resolutions = [resolutions] + self.resolutions = resolutions + if len(self.n_modes) != len(self.resolutions): + raise ValueError( + "The dimension of n_modes should be equal to that of resolutions, \ + but got dimension of n_modes {} and dimension of resolutions {}".format(len(self.n_modes), + len(self.resolutions))) + self.compute_dtype = compute_dtype + self.use_fork = use_fork + self.fourier_weight = fourier_weight + self.filter_mode = filter_mode + + if not self.fourier_weight: + param_list = [] + for i, n_mode in enumerate(self.n_modes): + weight_re = Tensor(ops.ones((in_channels, out_channels, n_mode)), mstype.float32) + weight_im = Tensor(ops.ones((in_channels, out_channels, n_mode)), mstype.float32) + + w_re = Parameter(initializer(XavierNormal(), weight_re.shape, mstype.float32), name=f'w_re_{i}', + requires_grad=True) + w_im = Parameter(initializer(XavierNormal(), weight_im.shape, mstype.float32), name=f'w_im_{i}', + requires_grad=True) + + param_list.append(w_re) + param_list.append(w_im) + + self.fourier_weight = ParameterTuple([param for param in param_list]) + + if use_fork: + self.forecast_ff = forecast_ff + if not self.forecast_ff: + self.forecast_ff = FeedForward( + out_channels, factor, ff_weight_norm, n_ff_layers, layer_norm, dropout) + + self.backcast_ff = backcast_ff + if not self.backcast_ff: + self.backcast_ff = FeedForward( + out_channels, factor, ff_weight_norm, n_ff_layers, layer_norm, dropout) + + self._positional_embedding, self._input_perm, self._output_perm = self._transpose(len(self.resolutions)) + + def construct(self, x: Tensor): + raise NotImplementedError() + + def _fourier_dimension(self, n, mode, n_dim): + """" n- shape - 3D: S1 S2 S3 / 2D: M N / 1D: C + mode - output length - n//2 +1 + dim - 3D: -1 -2 -3 / 2D: -1 -2 / 1D: -1 """ + dft_cell = dft1(shape=(n,), modes=mode, dim=(n_dim,), compute_dtype=self.compute_dtype) + idft_cell = idft1(shape=(n,), modes=mode, dim=(n_dim,), compute_dtype=self.compute_dtype) + + return dft_cell, idft_cell + + def _einsum(self, inputs, weights, dim): + """The Einstein multiplication function""" + res_len = len(self.resolutions) + + if res_len not in [1, 2, 3]: + raise ValueError( + "The length of input resolutions dimensions should be in [1, 2, 3], but got: {}".format(res_len)) + + if self.einsum_flag: + expressions = { + ('x', 1): 'bix,iox->box', + ('x', 2): 'bixy,iox->boxy', + ('y', 2): 'bixy,ioy->boxy', + ('x', 3): 'bixyz,iox->boxyz', + ('y', 3): 'bixyz,ioy->boxyz', + ('z', 3): 'bixyz,ioz->boxyz' + } + + key = (dim, res_len) + if key not in expressions: + raise ValueError(f"Unsupported type of the last dim of weight: {dim}") + + out = mint.einsum(expressions[key], inputs, weights) + + else: + _, weight_out, weight_dim = weights.shape + batch_size, inputs_in = inputs.shape[0], inputs.shape[1] + weights_perm = (2, 0, 1) + + if res_len == 1: + if dim == 'x': + input_perm = (2, 0, 1) + output_perm = (1, 2, 0) + else: + raise ValueError(f"Unsupported type of the last dim of weight: {dim}") + + inputs = ops.transpose(inputs, input_perm=input_perm) + weights = ops.transpose(weights, input_perm=weights_perm) + out = ops.bmm(inputs, weights) + out = ops.transpose(out, input_perm=output_perm) + elif res_len == 2: + if dim == 'y': + input_perm = (3, 0, 2, 1) + output_perm = (1, 3, 2, 0) + elif dim == 'x': + input_perm = (2, 0, 3, 1) + output_perm = (1, 3, 0, 2) + else: + raise ValueError(f"Unsupported type of the last dim of weight: {dim}") + + inputs = ops.transpose(inputs, input_perm=input_perm) + inputs = ops.reshape(inputs, (weight_dim, -1, inputs_in)) + weights = ops.transpose(weights, input_perm=weights_perm) + out = ops.bmm(inputs, weights) + out = ops.reshape(out, (weight_dim, batch_size, -1, weight_out)) + out = ops.transpose(out, input_perm=output_perm) + else: + input_dim1, input_dim2, input_dim3 = inputs.shape[2], inputs.shape[3], inputs.shape[4] + + if dim == 'z': + input_perm = (4, 0, 2, 3, 1) + output_perm = (1, 4, 2, 3, 0) + reshape_dim = input_dim1 + elif dim == 'y': + input_perm = (3, 0, 4, 2, 1) + output_perm = (1, 4, 3, 0, 2) + reshape_dim = input_dim3 + elif dim == 'x': + input_perm = (2, 0, 3, 4, 1) + output_perm = (1, 4, 0, 2, 3) + reshape_dim = input_dim2 + else: + raise ValueError(f"Unsupported type of the last dim of weight: {dim}") + + inputs = ops.transpose(inputs, input_perm=input_perm) + inputs = ops.reshape(inputs, (weight_dim, -1, inputs_in)) + weights = ops.transpose(weights, input_perm=weights_perm) + out = ops.bmm(inputs, weights) + out = ops.reshape(out, (weight_dim, batch_size, reshape_dim, -1, weight_out)) + out = ops.transpose(out, input_perm=output_perm) + + return out + + def _transpose(self, n_dim): + """transpose tensor""" + if n_dim == 1: + positional_embedding = Tensor(get_grid_1d(resolution=self.resolutions)) + input_perm = (0, 2, 1) + output_perm = (0, 2, 1) + elif n_dim == 2: + positional_embedding = Tensor(get_grid_2d(resolution=self.resolutions)) + input_perm = (0, 2, 3, 1) + output_perm = (0, 3, 1, 2) + elif n_dim == 3: + positional_embedding = Tensor(get_grid_3d(resolution=self.resolutions)) + input_perm = (0, 2, 3, 4, 1) + output_perm = (0, 4, 1, 2, 3) + else: + raise ValueError( + "The length of input resolutions dimensions should be in [1, 2, 3], but got: {}".format(n_dim)) + return positional_embedding, input_perm, output_perm + + def _complex_mul(self, input_re, input_im, weight_re, weight_im, dim): + """(a + bj) * (c + dj) = (ac - bd) + (ad + bc)j""" + out_re = self._einsum(input_re, weight_re, dim) - self._einsum(input_im, weight_im, dim) + out_im = self._einsum(input_re, weight_im, dim) + self._einsum(input_im, weight_re, dim) + + return out_re, out_im + + +class SpectralConv1d(SpectralConv): + """1D Fourier layer. It does DFT, factorization, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, + fourier_weight, factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, r_padding, + filter_mode, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, fourier_weight, + factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, filter_mode) + + self._dft1_x_cell, self._idft1_x_cell = self._fourier_dimension(resolutions[0] + r_padding, n_modes[0], -1) + + def construct(self, x: Tensor): + x = self.construct_fourier(x) + b = self.backcast_ff(x) + f = self.forecast_ff(x) if self.use_fork else None + + return b, f + + def construct_fourier(self, x): + """1D Fourier layer.""" + x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size + + x_ft_re = x + x_ft_im = ops.zeros_like(x_ft_re) + + x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + + x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0]] + x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0]] + + re0, re1, re2 = x_ftx_re.shape + im0, im1, im2 = x_ftx_im.shape + out_ftx_remain_re = ops.zeros((re0, re1, re2 - self.n_modes[0])) + out_ftx_remain_im = ops.zeros((im0, im1, im2 - self.n_modes[0])) + + if self.filter_mode == 'full': + ftx_re, ftx_im = self._complex_mul( + x_ftx_re_part, x_ftx_im_part, self.fourier_weight[0], self.fourier_weight[1], 'x') + out_ftx_re = ops.cat([ftx_re, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([ftx_im, out_ftx_remain_im], axis=2) + elif self.filter_mode == 'low_pass': + out_ftx_re = ops.cat([x_ftx_re_part, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([x_ftx_im_part, out_ftx_remain_im], axis=2) + else: + out_ftx_re = ops.zeros_like(x_ftx_re) + out_ftx_im = ops.zeros_like(x_ftx_im) + + x, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + x = ops.transpose(x, input_perm=self._input_perm) + + return x + + +class SpectralConv2d(SpectralConv): + """2D Fourier layer. It does DFT, factorization, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, + fourier_weight, factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, r_padding, + filter_mode, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, fourier_weight, + factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, filter_mode) + + self._dft1_x_cell, self._idft1_x_cell = self._fourier_dimension(resolutions[0] + r_padding, n_modes[0], -2) + self._dft1_y_cell, self._idft1_y_cell = self._fourier_dimension(resolutions[1] + r_padding, n_modes[1], -1) + + def construct(self, x: Tensor): + x = self.construct_fourier(x) + b = self.backcast_ff(x) + f = self.forecast_ff(x) if self.use_fork else None + + return b, f + + def construct_fourier(self, x): + """2D Fourier layer.""" + x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size, grid_size + + x_ft_re = x + x_ft_im = ops.zeros_like(x_ft_re) + + # Dimesion Y + x_fty_re, x_fty_im = self._dft1_y_cell((x_ft_re, x_ft_im)) + + x_fty_re_part = x_fty_re[:, :, :, :self.n_modes[1]] + x_fty_im_part = x_fty_im[:, :, :, :self.n_modes[1]] + + re0, re1, re2, re3 = x_fty_re.shape + im0, im1, im2, im3 = x_fty_im.shape + out_fty_remain_re = ops.zeros((re0, re1, re2, re3 - self.n_modes[1])) + out_fty_remain_im = ops.zeros((im0, im1, im2, im3 - self.n_modes[1])) + + if self.filter_mode == 'full': + fty_re, fty_im = self._complex_mul( + x_fty_re_part, x_fty_im_part, self.fourier_weight[2], self.fourier_weight[3], 'y') + out_fty_re = ops.cat([fty_re, out_fty_remain_re], axis=3) + out_fty_im = ops.cat([fty_im, out_fty_remain_im], axis=3) + elif self.filter_mode == 'low_pass': + out_fty_re = ops.cat([x_fty_re_part, out_fty_remain_re], axis=3) + out_fty_im = ops.cat([x_fty_im_part, out_fty_remain_im], axis=3) + else: + out_fty_re = ops.zeros_like(x_fty_re) + out_fty_im = ops.zeros_like(x_fty_im) + + xy, _ = self._idft1_y_cell((out_fty_re, out_fty_im)) + + # Dimesion X + x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + + x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0], :] + x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0], :] + + re0, re1, re2, re3 = x_ftx_re.shape + im0, im1, im2, im3 = x_ftx_im.shape + out_ftx_remain_re = ops.zeros((re0, re1, re2 - self.n_modes[0], re3)) + out_ftx_remain_im = ops.zeros((im0, im1, im2 - self.n_modes[0], im3)) + + if self.filter_mode == 'full': + ftx_re, ftx_im = self._complex_mul( + x_ftx_re_part, x_ftx_im_part, self.fourier_weight[0], self.fourier_weight[1], 'x') + out_ftx_re = ops.cat([ftx_re, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([ftx_im, out_ftx_remain_im], axis=2) + elif self.filter_mode == 'low_pass': + out_ftx_re = ops.cat([x_ftx_re_part, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([x_ftx_im_part, out_ftx_remain_im], axis=2) + else: + out_ftx_re = ops.zeros_like(x_ftx_re) + out_ftx_im = ops.zeros_like(x_ftx_im) + + xx, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + + # Combining Dimensions + x = xx + xy + + x = ops.transpose(x, input_perm=self._input_perm) + + return x + + +class SpectralConv3d(SpectralConv): + """3D Fourier layer. It does DFT, factorization, linear transform, and Inverse DFT.""" + + def __init__(self, in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, + fourier_weight, factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, r_padding, + filter_mode, compute_dtype=mstype.float32): + super().__init__(in_channels, out_channels, n_modes, resolutions, forecast_ff, backcast_ff, fourier_weight, + factor, ff_weight_norm, n_ff_layers, layer_norm, use_fork, dropout, filter_mode) + + self._dft1_x_cell, self._idft1_x_cell = self._fourier_dimension(resolutions[0] + r_padding, n_modes[0], -3) + self._dft1_y_cell, self._idft1_y_cell = self._fourier_dimension(resolutions[1] + r_padding, n_modes[1], -2) + self._dft1_z_cell, self._idft1_z_cell = self._fourier_dimension(resolutions[2] + r_padding, n_modes[2], -1) + + def construct(self, x: Tensor): + x = self.construct_fourier(x) + b = self.backcast_ff(x) + f = self.forecast_ff(x) if self.use_fork else None + + return b, f + + def construct_fourier(self, x): + """3D Fourier layer.""" + x = ops.transpose(x, input_perm=self._output_perm) # x shape: batch, in_dim, grid_size, grid_size, grid_size + + x_ft_re = x + x_ft_im = ops.zeros_like(x_ft_re) + + # Dimesion Z + x_ftz_re, x_ftz_im = self._dft1_z_cell((x_ft_re, x_ft_im)) + + x_ftz_re_part = x_ftz_re[:, :, :, :, :self.n_modes[2]] + x_ftz_im_part = x_ftz_im[:, :, :, :, :self.n_modes[2]] + + re0, re1, re2, re3, re4 = x_ftz_re.shape + im0, im1, im2, im3, im4 = x_ftz_im.shape + out_ftz_remain_re = ops.zeros((re0, re1, re2, re3, re4 - self.n_modes[2])) + out_ftz_remain_im = ops.zeros((im0, im1, im2, im3, im4 - self.n_modes[2])) + + if self.filter_mode == 'full': + ftz_re, ftz_im = self._complex_mul( + x_ftz_re_part, x_ftz_im_part, self.fourier_weight[4], self.fourier_weight[5], 'z') + out_ftz_re = ops.cat([ftz_re, out_ftz_remain_re], axis=4) + out_ftz_im = ops.cat([ftz_im, out_ftz_remain_im], axis=4) + elif self.filter_mode == 'low_pass': + out_ftz_re = ops.cat([x_ftz_re_part, out_ftz_remain_re], axis=4) + out_ftz_im = ops.cat([x_ftz_im_part, out_ftz_remain_im], axis=4) + else: + out_ftz_re = ops.zeros_like(x_ftz_re) + out_ftz_im = ops.zeros_like(x_ftz_im) + + xz, _ = self._idft1_z_cell((out_ftz_re, out_ftz_im)) + + # Dimesion Y + x_fty_re, x_fty_im = self._dft1_y_cell((x_ft_re, x_ft_im)) + + x_fty_re_part = x_fty_re[:, :, :, :self.n_modes[1], :] + x_fty_im_part = x_fty_im[:, :, :, :self.n_modes[1], :] + + re0, re1, re2, re3, re4 = x_fty_re.shape + im0, im1, im2, im3, im4 = x_fty_im.shape + out_fty_remain_re = ops.zeros((re0, re1, re2, re3 - self.n_modes[1], re4)) + out_fty_remain_im = ops.zeros((im0, im1, im2, im3 - self.n_modes[1], im4)) + + if self.filter_mode == 'full': + fty_re, fty_im = self._complex_mul( + x_fty_re_part, x_fty_im_part, self.fourier_weight[2], self.fourier_weight[3], 'y') + out_fty_re = ops.cat([fty_re, out_fty_remain_re], axis=3) + out_fty_im = ops.cat([fty_im, out_fty_remain_im], axis=3) + elif self.filter_mode == 'low_pass': + out_fty_re = ops.cat([x_fty_re_part, out_fty_remain_re], axis=3) + out_fty_im = ops.cat([x_fty_im_part, out_fty_remain_im], axis=3) + else: + out_fty_re = ops.zeros_like(x_fty_re) + out_fty_im = ops.zeros_like(x_fty_im) + + xy, _ = self._idft1_y_cell((out_fty_re, out_fty_im)) + + # Dimesion X + x_ftx_re, x_ftx_im = self._dft1_x_cell((x_ft_re, x_ft_im)) + + x_ftx_re_part = x_ftx_re[:, :, :self.n_modes[0], :, :] + x_ftx_im_part = x_ftx_im[:, :, :self.n_modes[0], :, :] + + re0, re1, re2, re3, re4 = x_ftx_re.shape + im0, im1, im2, im3, im4 = x_ftx_im.shape + out_ftx_remain_re = ops.zeros((re0, re1, re2 - self.n_modes[0], re3, re4)) + out_ftx_remain_im = ops.zeros((im0, im1, im2 - self.n_modes[0], im3, im4)) + + if self.filter_mode == 'full': + ftx_re, ftx_im = self._complex_mul( + x_ftx_re_part, x_ftx_im_part, self.fourier_weight[0], self.fourier_weight[1], 'x') + out_ftx_re = ops.cat([ftx_re, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([ftx_im, out_ftx_remain_im], axis=2) + elif self.filter_mode == 'low_pass': + out_ftx_re = ops.cat([x_ftx_re_part, out_ftx_remain_re], axis=2) + out_ftx_im = ops.cat([x_ftx_im_part, out_ftx_remain_im], axis=2) + else: + out_ftx_re = ops.zeros_like(x_ftx_re) + out_ftx_im = ops.zeros_like(x_ftx_im) + + xx, _ = self._idft1_x_cell((out_ftx_re, out_ftx_im)) + + # Combining Dimensions + x = xx + xy + xz + + x = ops.transpose(x, input_perm=self._input_perm) + + return x diff --git a/tests/st/mindflow/networks/ffno/test_ffno.py b/tests/st/mindflow/networks/ffno/test_ffno.py new file mode 100644 index 000000000..8cda98808 --- /dev/null +++ b/tests/st/mindflow/networks/ffno/test_ffno.py @@ -0,0 +1,381 @@ +# Copyright 2023 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""mindflow st testcase""" + +import os +import sys +import time + +import pytest +import numpy as np + +import mindspore as ms +from mindspore import nn, Tensor, set_seed, load_param_into_net, load_checkpoint +from mindspore import dtype as mstype + +from mindflow.cell import FFNO1D, FFNO2D, FFNO3D + +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../")) +sys.path.append(PROJECT_ROOT) + +# pylint: disable=wrong-import-position + +from common.cell.utils import compare_output +from common.cell import FP32_RTOL + +# pylint: enable=wrong-import-position + +set_seed(123456) +folder_path = "/home/workspace/mindspore_dataset/mindscience/ffno" + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno1d_output(mode): + """ + Feature: Test FFNO1D network in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model1d = FFNO1D(in_channels=2, + out_channels=2, + n_modes=[2], + resolutions=[6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data1d = Tensor(np.load(os.path.join(folder_path, "ffno_data1d.npy")), dtype=mstype.float32) + param1d = load_checkpoint(os.path.join(folder_path, "ffno1d.ckpt")) + load_param_into_net(model1d, param1d) + output1d = model1d(data1d) + target1d = np.load(os.path.join(folder_path, "ffno_target1d.npy")) + + assert output1d.shape == (2, 6, 2) + assert output1d.dtype == mstype.float32 + assert compare_output(output1d.asnumpy(), target1d, rtol=FP32_RTOL, atol=FP32_RTOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno1d_mse_loss_output(mode): + """ + Feature: Test FFNO1D MSE Loss in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model1d = FFNO1D(in_channels=2, + out_channels=2, + n_modes=[2], + resolutions=[6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data1d = Tensor(np.ones((2, 6, 2)), dtype=mstype.float32) + label_1d = Tensor(np.ones((2, 6, 2)), dtype=mstype.float32) + param1d = load_checkpoint(os.path.join(folder_path, "ffno1d.ckpt")) + load_param_into_net(model1d, param1d) + + loss_fn = nn.MSELoss() + optimizer_1d = nn.SGD(model1d.trainable_params(), learning_rate=0.01) + net_with_loss_1d = nn.WithLossCell(model1d, loss_fn) + train_step_1d = nn.TrainOneStepCell(net_with_loss_1d, optimizer_1d) + + # calculate two steps of loss + loss_1d = train_step_1d(data1d, label_1d) + target_loss_1_1d = 0.63846040 + assert compare_output(loss_1d.asnumpy(), target_loss_1_1d, rtol=FP32_RTOL, atol=FP32_RTOL) + + loss_1d = train_step_1d(data1d, label_1d) + target_loss_2_1d = 0.04462930 + assert compare_output(loss_1d.asnumpy(), target_loss_2_1d, rtol=FP32_RTOL, atol=FP32_RTOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno2d_output(mode): + """ + Feature: Test FFNO2D network in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model2d = FFNO2D(in_channels=2, + out_channels=2, + n_modes=[2, 2], + resolutions=[6, 6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data2d = Tensor(np.load(os.path.join(folder_path, "ffno_data2d.npy")), dtype=mstype.float32) + param2d = load_checkpoint(os.path.join(folder_path, "ffno2d.ckpt")) + load_param_into_net(model2d, param2d) + output2d = model2d(data2d) + target2d = np.load(os.path.join(folder_path, "ffno_target2d.npy")) + + assert output2d.shape == (2, 6, 6, 2) + assert output2d.dtype == mstype.float32 + assert compare_output(output2d.asnumpy(), target2d, rtol=FP32_RTOL, atol=FP32_RTOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno2d_mse_loss_output(mode): + """ + Feature: Test FFNO2D MSE Loss in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model2d = FFNO2D(in_channels=2, + out_channels=2, + n_modes=[2, 2], + resolutions=[6, 6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data2d = Tensor(np.ones((2, 6, 6, 2)), dtype=mstype.float32) + label_2d = Tensor(np.ones((2, 6, 6, 2)), dtype=mstype.float32) + param2d = load_checkpoint(os.path.join(folder_path, "ffno2d.ckpt")) + load_param_into_net(model2d, param2d) + + loss_fn = nn.MSELoss() + optimizer_2d = nn.SGD(model2d.trainable_params(), learning_rate=0.01) + net_with_loss_2d = nn.WithLossCell(model2d, loss_fn) + train_step_2d = nn.TrainOneStepCell(net_with_loss_2d, optimizer_2d) + + # calculate two steps of loss + loss_2d = train_step_2d(data2d, label_2d) + target_loss_1_2d = 1.70347130 + assert compare_output(loss_2d.asnumpy(), target_loss_1_2d, rtol=FP32_RTOL, atol=FP32_RTOL) + + loss_2d = train_step_2d(data2d, label_2d) + target_loss_2_2d = 0.28143430 + assert compare_output(loss_2d.asnumpy(), target_loss_2_2d, rtol=FP32_RTOL, atol=FP32_RTOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno3d_output(mode): + """ + Feature: Test FFNO3D network in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model3d = FFNO3D(in_channels=2, + out_channels=2, + n_modes=[2, 2, 2], + resolutions=[6, 6, 6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data3d = Tensor(np.load(os.path.join(folder_path, "ffno_data3d.npy")), dtype=mstype.float32) + param3d = load_checkpoint(os.path.join(folder_path, "ffno3d.ckpt")) + load_param_into_net(model3d, param3d) + output3d = model3d(data3d) + target3d = np.load(os.path.join(folder_path, "ffno_target3d.npy")) + + assert output3d.shape == (2, 6, 6, 6, 2) + assert output3d.dtype == mstype.float32 + assert compare_output(output3d.asnumpy(), target3d, rtol=FP32_RTOL, atol=FP32_RTOL) + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno3d_mse_loss_output(mode): + """ + Feature: Test FFNO3D MSE Loss in platform ascend. + Description: None. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model3d = FFNO3D(in_channels=2, + out_channels=2, + n_modes=[2, 2, 2], + resolutions=[6, 6, 6], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data3d = Tensor(np.ones((2, 6, 6, 6, 2)), dtype=mstype.float32) + label_3d = Tensor(np.ones((2, 6, 6, 6, 2)), dtype=mstype.float32) + param3d = load_checkpoint(os.path.join(folder_path, "ffno3d.ckpt")) + load_param_into_net(model3d, param3d) + + loss_fn = nn.MSELoss() + optimizer_3d = nn.SGD(model3d.trainable_params(), learning_rate=0.01) + net_with_loss_3d = nn.WithLossCell(model3d, loss_fn) + train_step_3d = nn.TrainOneStepCell(net_with_loss_3d, optimizer_3d) + + # calculate two steps of loss + loss_3d = train_step_3d(data3d, label_3d) + target_loss_1_3d = 1.94374371 + assert compare_output(loss_3d.asnumpy(), target_loss_1_3d, rtol=FP32_RTOL, atol=FP32_RTOL) + + loss_3d = train_step_3d(data3d, label_3d) + target_loss_2_3d = 0.24034855 + assert compare_output(loss_3d.asnumpy(), target_loss_2_3d, rtol=FP32_RTOL, atol=FP32_RTOL) + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno1d_speed(mode): + """ + Feature: Test FFNO1D training speed in platform ascend. + Description: The speed of each training step. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model1d = FFNO1D(in_channels=32, + out_channels=32, + n_modes=[16], + resolutions=[128], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data1d = Tensor(np.ones((32, 128, 32)), dtype=mstype.float32) + label_1d = Tensor(np.ones((32, 128, 32)), dtype=mstype.float32) + + loss_fn = nn.MSELoss() + optimizer_1d = nn.SGD(model1d.trainable_params(), learning_rate=0.01) + net_with_loss_1d = nn.WithLossCell(model1d, loss_fn) + train_step_1d = nn.TrainOneStepCell(net_with_loss_1d, optimizer_1d) + + steps = 10 + for _ in range(10): + train_step_1d(data1d, label_1d) + + start_time = time.time() + for _ in range(10): + train_step_1d(data1d, label_1d) + end_time = time.time() + + assert (end_time - start_time) / steps < 0.5 + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno2d_speed(mode): + """ + Feature: Test FFNO2D training speed in platform ascend. + Description: The speed of each training step. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model2d = FFNO2D(in_channels=32, + out_channels=32, + n_modes=[16, 16], + resolutions=[64, 64], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data2d = Tensor(np.ones((32, 64, 64, 32)), dtype=mstype.float32) + label_2d = Tensor(np.ones((32, 64, 64, 32)), dtype=mstype.float32) + + loss_fn = nn.MSELoss() + optimizer_2d = nn.SGD(model2d.trainable_params(), learning_rate=0.01) + net_with_loss_2d = nn.WithLossCell(model2d, loss_fn) + train_step_2d = nn.TrainOneStepCell(net_with_loss_2d, optimizer_2d) + + steps = 10 + for _ in range(steps): + train_step_2d(data2d, label_2d) + + start_time = time.time() + for _ in range(steps): + train_step_2d(data2d, label_2d) + end_time = time.time() + + assert (end_time - start_time) / steps < 1 + + +@pytest.mark.level0 +@pytest.mark.platform_arm_ascend910b_training +@pytest.mark.env_onecard +@pytest.mark.parametrize('mode', [ms.GRAPH_MODE, ms.PYNATIVE_MODE]) +def test_ffno3d_speed(mode): + """ + Feature: Test FFNO3D training speed in platform ascend. + Description: The speed of each training step. + Expectation: Success or throw AssertionError. + """ + ms.set_context(mode=mode) + model3d = FFNO3D(in_channels=2, + out_channels=2, + n_modes=[16, 16, 16], + resolutions=[32, 32, 32], + hidden_channels=2, + n_layers=2, + share_weight=True, + r_padding=8, + ffno_compute_dtype=mstype.float32) + + data3d = Tensor(np.ones((2, 32, 32, 32, 2)), dtype=mstype.float32) + label_3d = Tensor(np.ones((2, 32, 32, 32, 2)), dtype=mstype.float32) + + loss_fn = nn.MSELoss() + optimizer_3d = nn.SGD(model3d.trainable_params(), learning_rate=0.01) + net_with_loss_3d = nn.WithLossCell(model3d, loss_fn) + train_step_3d = nn.TrainOneStepCell(net_with_loss_3d, optimizer_3d) + + steps = 10 + for _ in range(steps): + train_step_3d(data3d, label_3d) + + start_time = time.time() + for _ in range(steps): + train_step_3d(data3d, label_3d) + end_time = time.time() + + assert (end_time - start_time) / steps < 3 -- Gitee From 7b77cb5def8e72268b2b747907e31c9e0b878960 Mon Sep 17 00:00:00 2001 From: xuhang Date: Fri, 18 Jul 2025 10:29:11 +0800 Subject: [PATCH 25/30] Fix: correct errors in the MEGAProtein.md file --- .../applications/model_cards/MEGAProtein.md | 4 +- .../test_megaprotein/test_megaprotein.py | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/st/mindsponge/test_megaprotein/test_megaprotein.py diff --git a/MindSPONGE/applications/model_cards/MEGAProtein.md b/MindSPONGE/applications/model_cards/MEGAProtein.md index dd00ed33e..1d9e74e7c 100644 --- a/MindSPONGE/applications/model_cards/MEGAProtein.md +++ b/MindSPONGE/applications/model_cards/MEGAProtein.md @@ -102,8 +102,8 @@ msa_feature['decoy_aatype'] = np.pad(aatype, (0, 256 - aatype.shape[0])) msa_feature['decoy_atom_positions'] = np.pad(final_atom_positions, ((0, 256 - final_atom_positions.shape[0]), (0, 0), (0, 0))) msa_feature['decoy_atom_mask'] = np.pad(final_atom_mask, ((0, 256 - final_atom_mask.shape[0]), (0, 0))) -res = protein_assessment.predict(msa_feature) -print("score is:", np.mean(res)) +res = protein_assessment.model.predict(msa_feature) +print("score is:", np.mean(res[:msa_feature['num_residues']])) ``` ### 使用场景 diff --git a/tests/st/mindsponge/test_megaprotein/test_megaprotein.py b/tests/st/mindsponge/test_megaprotein/test_megaprotein.py new file mode 100644 index 000000000..b8bce8d5f --- /dev/null +++ b/tests/st/mindsponge/test_megaprotein/test_megaprotein.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Huawei Technologies Co., Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ +"""Test MEGAFoldProtein examples.""" +import numpy as np +import mindspore as ms +from mindsponge import PipeLine + +ms.set_context(mode=ms.GRAPH_MODE) + +# MEGA-EvoGen推理获取蛋白质生成MSA后的特征 +fasta = "GYDKDLCEWSMTADQTEVETQIEADIMNIVKRDRPEMKAEVQKQLKSGGVMQYNYVLYCDKNFNNKNIIAEVVGE" +msa_generator = PipeLine(name="MEGAEvoGen") +msa_generator.set_device_id(0) +msa_generator.initialize(key="evogen_predict_256") +msa_generator.model.from_pretrained() +msa_feature = msa_generator.predict(fasta) + +# MEGA-Fold推理获取蛋白质结构信息 +fold_prediction = PipeLine(name="MEGAFold") +fold_prediction.set_device_id(0) +fold_prediction.initialize(key="predict_256") +fold_prediction.model.from_pretrained() +final_atom_positions, final_atom_mask, aatype, _, _ = fold_prediction.model.predict(msa_feature) + +# MEGA-Assessment对蛋白质结构进行评价 +protein_assessment = PipeLine(name="MEGAAssessment") +protein_assessment.set_device_id(0) +protein_assessment.initialize("predict_256") +protein_assessment.model.from_pretrained() +msa_feature['decoy_aatype'] = np.pad(aatype, (0, 256 - aatype.shape[0])) +msa_feature['decoy_atom_positions'] = np.pad(final_atom_positions, + ((0, 256 - final_atom_positions.shape[0]), (0, 0), (0, 0))) +msa_feature['decoy_atom_mask'] = np.pad(final_atom_mask, ((0, 256 - final_atom_mask.shape[0]), (0, 0))) + +res = protein_assessment.model.predict(msa_feature) +print("score is:", np.mean(res[:msa_feature['num_residues']])) -- Gitee From bd21c983bea5471c5e22258f13c603c61b770fdd Mon Sep 17 00:00:00 2001 From: l30062829 Date: Fri, 18 Jul 2025 11:39:21 +0800 Subject: [PATCH 26/30] =?UTF-8?q?=C2=B7fix=20assert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ensoforecast/src/utils.py | 6 +- .../earthquake/G-TEAM/src/models.py | 3 +- .../medium-range/koopman_vit/src/callback.py | 4 +- .../nowcasting/PreDiff/configs/diffusion.yaml | 193 ------------------ .../src/diffusion/cuboid_transformer.py | 24 ++- .../src/diffusion/cuboid_transformer_unet.py | 80 ++++++-- .../PreDiff/src/diffusion/latent_diffusion.py | 58 +++++- .../PreDiff/src/diffusion/solver.py | 6 +- .../PreDiff/src/diffusion/time_embed.py | 30 ++- .../src/knowledge_alignment/alignment.py | 50 ++++- .../nowcasting/PreDiff/src/sevir_dataset.py | 30 ++- .../nowcasting/PreDiff/src/utils.py | 45 +++- .../nowcasting/PreDiff/src/vae/resnet.py | 113 ++++++++-- .../nowcasting/PreDiff/src/visual.py | 21 +- 14 files changed, 389 insertions(+), 274 deletions(-) delete mode 100644 MindEarth/applications/nowcasting/PreDiff/configs/diffusion.yaml diff --git a/MindEarth/applications/climate-prediction/ensoforecast/src/utils.py b/MindEarth/applications/climate-prediction/ensoforecast/src/utils.py index d3c4f0cdb..e1c669d07 100644 --- a/MindEarth/applications/climate-prediction/ensoforecast/src/utils.py +++ b/MindEarth/applications/climate-prediction/ensoforecast/src/utils.py @@ -59,8 +59,10 @@ def init_dataloader(config): data_params = config.get('data') train_type = data_params.get('train_dataset') valid_type = data_params.get('valid_dataset') - assert train_type in ['CMIP5', 'Reanalysis'], 'Unexpected Data Type %s.' % train_type - assert valid_type in ['CMIP5', 'Reanalysis'], 'Unexpected Data Type %s.' % valid_type + if train_type not in ['CMIP5', 'Reanalysis']: + raise ValueError(f"Unexpected Data Type {train_type}.") + if valid_type not in ['CMIP5', 'Reanalysis']: + raise ValueError(f"Unexpected Data Type {valid_type}.") if train_type == 'CMIP5': train_dataset = CMIP5Data(data_params.get('root_dir'), data_params.get('train_period'), data_params.get('obs_time'), data_params.get('pred_time')) diff --git a/MindEarth/applications/earthquake/G-TEAM/src/models.py b/MindEarth/applications/earthquake/G-TEAM/src/models.py index 1c91673a6..57a230197 100644 --- a/MindEarth/applications/earthquake/G-TEAM/src/models.py +++ b/MindEarth/applications/earthquake/G-TEAM/src/models.py @@ -236,7 +236,8 @@ class PositionEmbedding(nn.Cell): min_lat, max_lat = wavelengths[0] min_lon, max_lon = wavelengths[1] min_depth, max_depth = wavelengths[2] - assert emb_dim % 10 == 0 + if emb_dim % 10 != 0: + raise ValueError(f"emb_dim must be divisible by 10, but got {emb_dim}") lat_dim = emb_dim // 5 lon_dim = emb_dim // 5 depth_dim = emb_dim // 10 diff --git a/MindEarth/applications/medium-range/koopman_vit/src/callback.py b/MindEarth/applications/medium-range/koopman_vit/src/callback.py index f8802248e..66b4f9320 100644 --- a/MindEarth/applications/medium-range/koopman_vit/src/callback.py +++ b/MindEarth/applications/medium-range/koopman_vit/src/callback.py @@ -184,8 +184,8 @@ class Lploss(nn.LossBase): def __init__(self, p=2, size_average=True, reduction=True): super(Lploss, self).__init__() # Dimension and Lp-norm type are positive - assert p > 0 - + if p <= 0: + raise ValueError(f"p must be positive, but got {p}") self.p = p self.reduction = reduction self.size_average = size_average diff --git a/MindEarth/applications/nowcasting/PreDiff/configs/diffusion.yaml b/MindEarth/applications/nowcasting/PreDiff/configs/diffusion.yaml deleted file mode 100644 index 408dfe6f0..000000000 --- a/MindEarth/applications/nowcasting/PreDiff/configs/diffusion.yaml +++ /dev/null @@ -1,193 +0,0 @@ -data: - dataset_name: "sevirlr" - seq_in: 13 - plot_stride: 1 - interval_real_time: 10 - raw_seq_len: 25 - sample_mode: "sequent" - stride: 6 - layout: "NTHWC" - start_date: null - train_val_split_date: [2019, 3, 19] - train_test_split_date: [2019, 6, 1] - end_date: null - val_ratio: 0.1 - metrics_mode: "0" - metrics_list: ['csi', 'pod', 'sucr', 'bias'] - threshold_list: [16, 74, 133, 160, 181, 219] - aug_mode: "0" - root_dir: "./dataset/sevir_lr" -layout: - t_in: 7 - t_out: 6 - data_channels: 1 - layout: "NTHWC" -optim: - total_batch_size: 64 - micro_batch_size: 2 - seed: 0 - float32_matmul_precision: "high" - method: "adamw" - lr: 1.0e-5 - betas: [0.9, 0.999] - gradient_clip_val: 1.0 - max_epochs: 2000 - loss_type: "l2" - warmup_percentage: 0.1 - lr_scheduler_mode: "cosine" - min_lr_ratio: 1.0e-3 - warmup_min_lr_ratio: 0.1 - monitor: "val/loss" - early_stop: false - early_stop_mode: "min" - early_stop_patience: 100 - save_top_k: 3 -logging: - logging_prefix: "PreDiff" - monitor_lr: true - monitor_device: false - track_grad_norm: -1 - use_wandb: false - profiler: null -trainer: - check_val_every_n_epoch: 50 - log_step_ratio: 0.001 - precision: 32 - find_unused_parameters: false -eval: - train_example_data_idx_list: [0, ] - val_example_data_idx_list: [0, 16, 32, 48, 64, 72, 96, 108, 128] - test_example_data_idx_list: [0, 16, 32, 48, 64, 72, 96, 108, 128] - eval_example_only: true - eval_aligned: true - eval_unaligned: true - num_samples_per_context: 1 - fs: 20 - label_offset: [-0.5, 0.5] - label_avg_int: false - fvd_features: 400 -model: - diffusion: - data_shape: [6, 128, 128, 1] - timesteps: 1000 - beta_schedule: "linear" - log_every_t: 100 - clip_denoised: false - linear_start: 1e-4 - linear_end: 2e-2 - cosine_s: 8e-3 - given_betas: null - original_elbo_weight: 0. - v_posterior: 0. - l_simple_weight: 1. - learn_logvar: false - logvar_init: 0. - latent_shape: [6, 16, 16, 64] - cond_stage_forward: null - scale_by_std: false - scale_factor: 1.0 - latent_cond_shape: [7, 16, 16, 64] - align: - alignment_type: "avg_x" - guide_scale: 50.0 - model_type: "cuboid" - model_args: - input_shape: [6, 16, 16, 64] - out_channels: 1 - base_units: 128 - scale_alpha: 1.0 - depth: [1, 1] - downsample: 2 - downsample_type: "patch_merge" - use_attn_pattern: true - num_heads: 4 - attn_drop: 0.1 - proj_drop: 0.1 - ffn_drop: 0.1 - ffn_activation: "gelu" - gated_ffn: false - norm_layer: "layer_norm" - use_inter_ffn: true - hierarchical_pos_embed: false - padding_type: "zeros" - use_relative_pos: true - self_attn_use_final_proj: true - num_global_vectors: 0 - use_global_vector_ffn: true - use_global_self_attn: false - separate_global_qkv: false - global_dim_ratio: 1 - attn_linear_init_mode: "0" - ffn_linear_init_mode: "0" - ffn2_linear_init_mode: "2" - attn_proj_linear_init_mode: "2" - conv_init_mode: "0" - down_linear_init_mode: "0" - global_proj_linear_init_mode: "2" - norm_init_mode: "0" - time_embed_channels_mult: 4 - time_embed_use_scale_shift_norm: false - time_embed_dropout: 0.0 - pool: "attention" - readout_seq: true - t_out: 6 - model_ckpt_path: "./ckpt/align.ckpt" - latent_model: - input_shape: [7, 16, 16, 64] - target_shape: [6, 16, 16, 64] - base_units: 256 - block_units: Null - scale_alpha: 1.0 - num_heads: 4 - attn_drop: 0.1 - proj_drop: 0.1 - ffn_drop: 0.1 - downsample: 2 - downsample_type: "patch_merge" - upsample_type: "upsample" - upsample_kernel_size: 3 - depth: [4, 4] - use_attn_pattern: true - num_global_vectors: 0 - use_global_vector_ffn: false - use_global_self_attn: true - separate_global_qkv: true - global_dim_ratio: 1 - ffn_activation: "gelu" - gated_ffn: false - norm_layer: "layer_norm" - padding_type: "zeros" - use_relative_pos: true - self_attn_use_final_proj: true - attn_linear_init_mode: "0" - ffn_linear_init_mode: "0" - ffn2_linear_init_mode: "2" - attn_proj_linear_init_mode: "2" - conv_init_mode: "0" - down_linear_init_mode: "0" - global_proj_linear_init_mode: "2" - norm_init_mode: "0" - time_embed_channels_mult: 4 - time_embed_use_scale_shift_norm: false - time_embed_dropout: 0.0 - unet_res_connect: true - vae: - pretrained_ckpt_path: "./ckpt/vae.ckpt" - data_channels: 1 - down_block_types: ['DownEncoderBlock2D', 'DownEncoderBlock2D', 'DownEncoderBlock2D', 'DownEncoderBlock2D'] - in_channels: 1 - block_out_channels: [128, 256, 512, 512] - act_fn: 'silu' - latent_channels: 64 - up_block_types: ['UpDecoderBlock2D', 'UpDecoderBlock2D', 'UpDecoderBlock2D', 'UpDecoderBlock2D'] - norm_num_groups: 32 - layers_per_block: 2 - out_channels: 1 -summary: - summary_dir: "./summary/prediff" - eval_interval: 10 - save_ckpt_epochs: 1 - keep_ckpt_max: 100 - ckpt_path: "./ckpt/diffusion.ckpt" - load_ckpt: false - diff --git a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer.py b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer.py index b51fb72cd..97e3e952b 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer.py @@ -335,7 +335,11 @@ class Upsample3DLayer(nn.Cell): def construct(self, x): """Forward pass of the 3D Upsampling layer.""" b, t, h, w, c = x.shape - assert self.target_size[0] == t + if self.target_size[0] != t: + raise ValueError( + f"Target size mismatch: expected first dimension to be {self.target_size[0]}, " + f"but got {t}. Please ensure consistent dimensions." + ) x = x.reshape(b * t, h, w, c).permute(0, 3, 1, 2) x = self.up(x) return ( @@ -645,7 +649,11 @@ class CuboidSelfAttentionLayer(nn.Cell): self.ffn_linear_init_mode = ffn_linear_init_mode self.norm_init_mode = norm_init_mode - assert dim % num_heads == 0 + if dim % num_heads != 0: + raise ValueError( + f"Dimension {dim} must be divisible by number of heads {num_heads}. " + f"Got dim={dim}, num_heads={num_heads}" + ) self.num_heads = num_heads self.dim = dim self.cuboid_size = cuboid_size @@ -659,7 +667,11 @@ class CuboidSelfAttentionLayer(nn.Cell): self.use_global_self_attn = use_global_self_attn self.separate_global_qkv = separate_global_qkv self.global_dim_ratio = global_dim_ratio - assert self.padding_type in ["ignore", "zeros", "nearest"] + if self.padding_type not in ["ignore", "zeros", "nearest"]: + raise ValueError( + f"Invalid padding_type: '{self.padding_type}'. " + f"Expected one of: ['ignore', 'zeros', 'nearest']" + ) head_dim = dim // num_heads self.scale = qk_scale or head_dim**-0.5 @@ -767,7 +779,11 @@ class CuboidSelfAttentionLayer(nn.Cell): """ x = self.norm(x) batch, time, height, width, channels = x.shape - assert channels == self.dim + if channels != self.dim: + raise ValueError( + f"Channel dimension mismatch: expected {self.dim}, got {channels}. " + f"Please ensure input channels match the layer's expected dimension." + ) cuboid_size, shift_size = update_cuboid_size_shift_size( (time, height, width), self.cuboid_size, self.shift_size, self.strategy ) diff --git a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer_unet.py b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer_unet.py index ae3292ceb..67929ae0c 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer_unet.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/cuboid_transformer_unet.py @@ -117,16 +117,29 @@ class CuboidTransformerUNet(nn.Cell): for i in range(self.num_blocks) ] else: - assert len(block_units) == self.num_blocks and block_units[0] == base_units + if len(block_units) != self.num_blocks: + raise ValueError( + f"Length of block_units ({len(block_units)}) does not match " + f"num_blocks ({self.num_blocks}). They must be equal." + ) + if block_units[0] != base_units: + raise ValueError( + f"First block_units value ({block_units[0]}) does not match " + f"base_units ({base_units}). The first unit must equal base_units." + ) self.block_units = block_units self.hierarchical_pos_embed = hierarchical_pos_embed self.num_global_vectors = num_global_vectors use_global_vector = num_global_vectors > 0 self.use_global_vector = use_global_vector if global_dim_ratio != 1: - assert ( - separate_global_qkv is True - ), f"Setting global_dim_ratio != 1 requires separate_global_qkv == True." + if not separate_global_qkv: + raise ValueError( + "Configuration conflict: When global_dim_ratio != 1, " + "separate_global_qkv must be set to True. " + f"Current values: global_dim_ratio={global_dim_ratio}, " + f"separate_global_qkv={separate_global_qkv}" + ) self.global_dim_ratio = global_dim_ratio self.use_global_vector_ffn = use_global_vector_ffn @@ -143,7 +156,19 @@ class CuboidTransformerUNet(nn.Cell): t_in, h_in, w_in, c_in = input_shape t_out, h_out, w_out, c_out = target_shape - assert h_in == h_out and w_in == w_out and c_in == c_out + if h_in != h_out or w_in != w_out or c_in != c_out: + mismatched_dims = [] + if h_in != h_out: + mismatched_dims.append(f"height ({h_in} vs {h_out})") + if w_in != w_out: + mismatched_dims.append(f"width ({w_in} vs {w_out})") + if c_in != c_out: + mismatched_dims.append(f"channels ({c_in} vs {c_out})") + raise ValueError( + f"Input and output dimensions mismatch. " + f"Mismatched dimensions: {', '.join(mismatched_dims)}. " + f"All dimensions must match for this operation." + ) self.t_in = t_in self.t_out = t_out self.first_proj = TimeEmbedResBlock( @@ -263,27 +288,37 @@ class CuboidTransformerUNet(nn.Cell): if not isinstance(block_cuboid_size[0][0], (list, tuple)): block_cuboid_size = [block_cuboid_size for _ in range(self.num_blocks)] else: - assert ( - len(block_cuboid_size) == self.num_blocks - ), f"Incorrect input format! Received block_cuboid_size={block_cuboid_size}" - + if len(block_cuboid_size) != self.num_blocks: + raise ValueError( + f"Block cuboid size dimension mismatch. Expected {self.num_blocks} blocks, " + f"but got {len(block_cuboid_size)}. Received block_cuboid_size={block_cuboid_size}. " + f"Please ensure the input matches the expected number of blocks." + ) if not isinstance(block_cuboid_strategy[0][0], (list, tuple)): block_cuboid_strategy = [ block_cuboid_strategy for _ in range(self.num_blocks) ] else: - assert ( - len(block_cuboid_strategy) == self.num_blocks - ), f"Incorrect input format! Received block_strategy={block_cuboid_strategy}" + if len(block_cuboid_strategy) != self.num_blocks: + raise ValueError( + f"Configuration error: Expected {self.num_blocks} block strategies, " + f"but got {len(block_cuboid_strategy)}. " + f"Received block_cuboid_strategy={block_cuboid_strategy}. " + f"Please ensure the strategy list matches the number of blocks." + ) if not isinstance(block_cuboid_shift_size[0][0], (list, tuple)): block_cuboid_shift_size = [ block_cuboid_shift_size for _ in range(self.num_blocks) ] else: - assert ( - len(block_cuboid_shift_size) == self.num_blocks - ), f"Incorrect input format! Received block_shift_size={block_cuboid_shift_size}" + if len(block_cuboid_shift_size) != self.num_blocks: + raise ValueError( + f"Block shift size configuration error: Expected {self.num_blocks} shift sizes, " + f"but received {len(block_cuboid_shift_size)}. " + f"Invalid configuration: block_cuboid_shift_size={block_cuboid_shift_size}. " + f"Please provide exactly {self.num_blocks} shift sizes in the list." + ) self.block_cuboid_size = block_cuboid_size self.block_cuboid_strategy = block_cuboid_strategy self.block_cuboid_shift_size = block_cuboid_shift_size @@ -442,7 +477,20 @@ class CuboidTransformerUNet(nn.Cell): if not hasattr(self, "_data_shape"): t_in, h_in, w_in, c_in = self.input_shape t_out, h_out, w_out, c_out = self.target_shape - assert h_in == h_out and w_in == w_out and c_in == c_out + if not (h_in == h_out and w_in == w_out and c_in == c_out): + mismatches = [] + if h_in != h_out: + mismatches.append(f"height ({h_in} vs {h_out})") + if w_in != w_out: + mismatches.append(f"width ({w_in} vs {w_out})") + if c_in != c_out: + mismatches.append(f"channels ({c_in} vs {c_out})") + raise ValueError( + f"Input-output dimension mismatch. Mismatched dimensions: {', '.join(mismatches)}. " + f"All dimensions must match for this operation. " + f"Input shape: (h={h_in}, w={w_in}, c={c_in}), " + f"Output shape: (h={h_out}, w={w_out}, c={c_out})" + ) self._data_shape = ( t_in + t_out, h_in, diff --git a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/latent_diffusion.py b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/latent_diffusion.py index 657dd45c4..040afb9cb 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/latent_diffusion.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/latent_diffusion.py @@ -211,9 +211,12 @@ class LatentDiffusion(nn.Cell): self.num_timesteps = int(timesteps) self.linear_start = linear_start self.linear_end = linear_end - assert ( - alphas_cumprod.shape[0] == self.num_timesteps - ), "alphas have to be defined for each timestep" + if alphas_cumprod.shape[0] != self.num_timesteps: + raise ValueError( + f"Timestep dimension mismatch: alphas_cumprod has {alphas_cumprod.shape[0]} timesteps, " + f"but expected {self.num_timesteps}. " + "The alpha values must be defined for each diffusion timestep." + ) to_mindspore = partial(Tensor, dtype=ms.float32) self.betas = Parameter(to_mindspore(betas), name="betas", requires_grad=False) @@ -287,7 +290,12 @@ class LatentDiffusion(nn.Cell): self.lvlb_weights = Parameter( lvlb_weights, name="lvlb_weights", requires_grad=False ) - assert not ops.isnan(self.lvlb_weights).all() + if ops.isnan(self.lvlb_weights).all(): + raise ValueError( + "All lvlb_weights are NaN (Not a Number). " + "This indicates a numerical instability or uninitialized weights. " + "Please check the weight initialization or training process." + ) def instantiate_first_stage(self, first_stage_model): """ @@ -298,8 +306,16 @@ class LatentDiffusion(nn.Cell): if isinstance(first_stage_model, nn.Cell): model = first_stage_model else: - assert first_stage_model is None - raise NotImplementedError("No default first_stage_model supported yet!") + if first_stage_model is not None: + raise ValueError( + "Custom first_stage_model is not currently supported. " + f"Received: {type(first_stage_model).__name__}. " + "This functionality is planned for future implementation." + ) + raise NotImplementedError( + "Automatic first_stage_model initialization is not yet implemented. " + "Please check for framework updates or consider contributing." + ) self.first_stage_model = model.set_train(False) self.first_stage_model.train = disabled_train for param in self.first_stage_model.trainable_params(): @@ -356,7 +372,14 @@ class LatentDiffusion(nn.Cell): def einops_spatial_layout(self): """Generates spatial Einops pattern for 2D/3D data handling.""" if not hasattr(self, "_einops_spatial_layout"): - assert len(self.layout) == 4 or len(self.layout) == 5 + if len(self.layout) not in (4, 5): + raise ValueError( + f"Invalid layout dimension: expected 4 or 5 dimensions, but got {len(self.layout)}. " + f"Current layout: {self.layout}\n" + "Possible solutions:\n" + "1. For 2D data: use [batch, channel, height, width]\n" + "2. For 3D data: use [batch, channel, depth, height, width]" + ) self._einops_spatial_layout = ( "(N T) C H W" if self.layout.find("T") else "N C H W" ) @@ -718,8 +741,19 @@ class LatentDiffusion(nn.Cell): ) if mask is not None: - assert x0 is not None - assert x0.shape[2:3] == mask.shape[2:3] # spatial size has to match + if x0 is None: + raise ValueError( + "Missing required input: x0 cannot be None. " + "Please provide valid input data." + ) + + if x0.shape[2:3] != mask.shape[2:3]: + raise ValueError( + f"Spatial dimension mismatch between input and mask. " + f"Input spatial size: {x0.shape[2:3]}, " + f"Mask spatial size: {mask.shape[2:3]}. " + "The height and width dimensions must match exactly." + ) for i in iterator: ts = ops.full((batch_size,), i, dtype=ms.int64) img = self.p_sample( @@ -781,7 +815,11 @@ class LatentDiffusion(nn.Cell): if shape is None: shape = self.get_batch_latent_shape(batch_size=batch_size) if self.cond_stage_model is not None: - assert cond is not None + if cond is None: + raise ValueError( + "Required condition is None. " + "This parameter must be provided with a valid value." + ) cond_tensor_slice = [ slice(None, None), ] * len(self.data_shape) diff --git a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/solver.py b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/solver.py index 103cf7064..bdbb5d4f8 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/solver.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/solver.py @@ -79,7 +79,11 @@ class DiffusionTrainer(nn.Cell): epoch_start = time.time() iterator = self.traindataset.create_dict_iterator() - assert iterator, "dataset is empty" + if not iterator: + raise ValueError( + "Empty dataset error: The provided dataset iterator contains no data. " + "Please verify your data loading pipeline and ensure the dataset is properly populated." + ) batch_idx = 0 for batch_idx, batch in enumerate(iterator): processed_data = self.datasetprocessing.process_data(batch["vil"]) diff --git a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/time_embed.py b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/time_embed.py index 1642d0c29..f052dc189 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/diffusion/time_embed.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/diffusion/time_embed.py @@ -40,7 +40,13 @@ class Upsample(nn.Cell): def construct(self, x): '''upsample forward''' - assert x.shape[1] == self.channels + if x.shape[1] != self.channels: + raise ValueError( + f"Channel dimension mismatch: input has {x.shape[1]} channels, " + f"but layer expects {self.channels} channels. " + f"Input shape: {x.shape}, expected channels dimension: {self.channels}. " + "Please adjust your input data or layer configuration." + ) if self.dims == 3: x = ops.interpolate( x, (x.shape[2], x.shape[3] * 2, x.shape[4] * 2), mode="nearest" @@ -78,11 +84,22 @@ class Downsample(nn.Cell): padding=padding, ) else: - assert self.channels == self.out_channels + if self.channels != self.out_channels: + raise ValueError( + f"Channel configuration mismatch: input channels ({self.channels}) " + f"must match output channels ({self.out_channels}) for this operation. " + "Please adjust either the input channels or the layer configuration." + ) self.op = avg_pool_nd(dims, kernel_size=stride, stride=stride) def construct(self, x): - assert x.shape[1] == self.channels + if x.shape[1] != self.channels: + raise ValueError( + f"Input channel mismatch: Expected {self.channels} channels, " + f"but received input with {x.shape[1]} channels. " + f"Full input shape: {x.shape}. " + "Please ensure your input data matches the layer's channel requirements." + ) return self.op(x) @@ -163,7 +180,12 @@ class TimeEmbedResBlock(nn.Cell): self.dropout = dropout self.use_embed = use_embed if use_embed: - assert isinstance(emb_channels, int) + if not isinstance(emb_channels, int): + raise TypeError( + f"Invalid type for emb_channels: expected integer, got {type(emb_channels).__name__}. " + f"Received value: {emb_channels}. " + "Please provide an integer value for the embedding channels." + ) self.emb_channels = emb_channels self.out_channels = out_channels or channels self.use_conv = use_conv diff --git a/MindEarth/applications/nowcasting/PreDiff/src/knowledge_alignment/alignment.py b/MindEarth/applications/nowcasting/PreDiff/src/knowledge_alignment/alignment.py index f1a6785b3..ec4b7f160 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/knowledge_alignment/alignment.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/knowledge_alignment/alignment.py @@ -53,7 +53,13 @@ class QKVAttention(nn.Cell): :return: an [N x (H * C) x T] tensor after attention. """ bs, width, length = qkv.shape - assert width % (3 * self.n_heads) == 0 + if width % (3 * self.n_heads) != 0: + raise ValueError( + f"Dimension mismatch: width ({width}) must be divisible by {3 * self.n_heads} " + f"(3 * n_heads), but got remainder {width % (3 * self.n_heads)}. " + f"Current configuration: n_heads={self.n_heads}. " + "Please adjust either the input width or the number of attention heads." + ) ch = width // (3 * self.n_heads) q, k, v = qkv.chunk(3, dim=1) scale = 1 / math.sqrt(math.sqrt(ch)) @@ -220,7 +226,17 @@ class NoisyCuboidTransformerEncoder(nn.Cell): for i in range(self.num_blocks) ] else: - assert len(block_units) == self.num_blocks and block_units[0] == base_units + if len(block_units) != self.num_blocks: + raise ValueError( + f"Block configuration mismatch: Expected {self.num_blocks} blocks, " + f"but got {len(block_units)}. Received block_units: {block_units}" + ) + + if block_units[0] != base_units: + raise ValueError( + f"First block units mismatch: Expected {base_units}, " + f"but got {block_units[0]}. The first block must match base_units." + ) self.block_units = block_units self.hierarchical_pos_embed = hierarchical_pos_embed self.num_global_vectors = num_global_vectors @@ -321,27 +337,39 @@ class NoisyCuboidTransformerEncoder(nn.Cell): if not isinstance(block_cuboid_size[0][0], (list, tuple)): block_cuboid_size = [block_cuboid_size for _ in range(self.num_blocks)] else: - assert ( - len(block_cuboid_size) == self.num_blocks - ), f"Incorrect input format! Received block_cuboid_size={block_cuboid_size}" + if len(block_cuboid_size) != self.num_blocks: + raise ValueError( + f"Block cuboid configuration error: Expected {self.num_blocks} blocks, " + f"but received {len(block_cuboid_size)} block configurations. " + f"Received block_cuboid_size: {block_cuboid_size}\n" + "Please ensure the number of block configurations matches num_blocks." + ) if not isinstance(block_cuboid_strategy[0][0], (list, tuple)): block_cuboid_strategy = [ block_cuboid_strategy for _ in range(self.num_blocks) ] else: - assert ( - len(block_cuboid_strategy) == self.num_blocks - ), f"Incorrect input format! Received block_strategy={block_cuboid_strategy}" + if len(block_cuboid_strategy) != self.num_blocks: + raise ValueError( + f"Block strategy configuration error: Expected {self.num_blocks} strategies (one per block), " + f"but received {len(block_cuboid_strategy)}. " + f"Received strategies: {block_cuboid_strategy}\n" + "Each cuboid block must have a corresponding processing strategy." + ) if not isinstance(block_cuboid_shift_size[0][0], (list, tuple)): block_cuboid_shift_size = [ block_cuboid_shift_size for _ in range(self.num_blocks) ] else: - assert ( - len(block_cuboid_shift_size) == self.num_blocks - ), f"Incorrect input format! Received block_shift_size={block_cuboid_shift_size}" + if len(block_cuboid_shift_size) != self.num_blocks: + raise ValueError( + f"Block shift configuration error: Expected {self.num_blocks} shift sizes (one per block), " + f"but received {len(block_cuboid_shift_size)}. " + f"Received shift sizes: {block_cuboid_shift_size}\n" + "Each cuboid block must have a corresponding shift size configuration." + ) self.block_cuboid_size = block_cuboid_size self.block_cuboid_strategy = block_cuboid_strategy self.block_cuboid_shift_size = block_cuboid_shift_size diff --git a/MindEarth/applications/nowcasting/PreDiff/src/sevir_dataset.py b/MindEarth/applications/nowcasting/PreDiff/src/sevir_dataset.py index d0fca3d1c..5b9c48128 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/sevir_dataset.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/sevir_dataset.py @@ -414,14 +414,22 @@ class SEVIRDataLoader: self.data_shape = SEVIR_DATA_SHAPE self.raw_seq_in = raw_seq_in - assert ( - seq_in <= self.raw_seq_in - ), f"seq_in must not be larger than raw_seq_in = {raw_seq_in}, got {seq_in}." + if seq_in > self.raw_seq_in: + raise ValueError( + f"Sequence length violation: Input sequence length ({seq_in}) " + f"exceeds maximum allowed length ({self.raw_seq_in}).\n" + f"Technical constraints: Processed sequence length must be ≤ original length.\n" + "Please check your sequence trimming/padding operations." + ) self.seq_in = seq_in - assert sample_mode in [ - "random", - "sequent", - ], f"Invalid sample_mode = {sample_mode}, must be 'random' or 'sequent'." + if sample_mode not in ["random", "sequent"]: + raise ValueError( + f"Invalid sampling mode: '{sample_mode}'. " + f"Allowed options are: {['random', 'sequent']}\n" + "Please specify either:\n" + "- 'random' for stochastic sampling\n" + "- 'sequent' for sequential sampling" + ) self.sample_mode = sample_mode self.stride = stride self.batch_size = batch_size @@ -921,7 +929,13 @@ class SEVIRDataset: if data_types is None: data_types = SEVIR_DATA_TYPES else: - assert set(data_types).issubset(SEVIR_DATA_TYPES) + if not set(data_types).issubset(SEVIR_DATA_TYPES): + invalid_types = set(data_types) - set(SEVIR_DATA_TYPES) + raise ValueError( + f"Invalid data type(s) detected: {sorted(invalid_types)}\n" + f"Allowed SEVIR data types are: {sorted(SEVIR_DATA_TYPES)}\n" + "Please remove or replace the unsupported data types." + ) self.layout = layout self.data_types = data_types diff --git a/MindEarth/applications/nowcasting/PreDiff/src/utils.py b/MindEarth/applications/nowcasting/PreDiff/src/utils.py index 5727573a7..1908b6a13 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/utils.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/utils.py @@ -179,7 +179,13 @@ class SEVIRSkillScore(Metric): ): super().__init__() self.layout = layout - assert preprocess_type == "sevir" or preprocess_type.startswith("sevir_pool") + if not (preprocess_type == "sevir" or preprocess_type.startswith("sevir_pool")): + raise ValueError( + f"Invalid preprocessing type: '{preprocess_type}'\n" + "Allowed options:\n" + "- 'sevir' for standard processing\n" + "- 'sevir_pool*' for pooled variants (e.g. 'sevir_pool4')" + ) self.preprocess_type = preprocess_type self.threshold_list = threshold_list self.metrics_list = metrics_list @@ -191,9 +197,11 @@ class SEVIRSkillScore(Metric): state_shape = (len(self.threshold_list),) elif mode in ("1", "2"): self.keep_seq_in_dim = True - assert isinstance( - self.seq_in, int - ), "seq_in must be provided when we need to keep seq_in dim." + if not isinstance(self.seq_in, int): + raise TypeError( + f"Invalid type for seq_in: expected integer, got {type(self.seq_in).__name__}. " + "This parameter is required when preserving the sequence dimension." + ) state_shape = (len(self.threshold_list), self.seq_in) else: @@ -572,8 +580,17 @@ def get_norm_layer( """ if isinstance(norm_type, str): if norm_type == "layer_norm": - assert in_channels > 0 - assert axis == -1 + if in_channels <= 0: + raise ValueError( + f"Invalid number of input channels: {in_channels}. " + "in_channels must be a positive integer." + ) + + if axis != -1: + raise ValueError( + f"Invalid axis specification: {axis}. " + "This operation only supports axis=-1 (last dimension)." + ) norm_layer = nn.LayerNorm( normalized_shape=[in_channels], epsilon=epsilon, **kwargs ) @@ -606,7 +623,15 @@ def generalize_padding(x, pad_t, pad_h, pad_w, padding_type, t_pad_left=False): if pad_t == 0 and pad_h == 0 and pad_w == 0: return x - assert padding_type in ["zeros", "ignore", "nearest"] + if padding_type not in ["zeros", "ignore", "nearest"]: + raise ValueError( + f"Invalid padding type: '{padding_type}'. " + f"Allowed options are: {['zeros', 'ignore', 'nearest']}\n" + "Please specify one of:\n" + "- 'zeros': pad with zero values\n" + "- 'ignore': maintain original values\n" + "- 'nearest': replicate edge values" + ) _, t, h, w, _ = x.shape if padding_type == "nearest": @@ -634,7 +659,11 @@ def generalize_unpadding(x, pad_t, pad_h, pad_w, padding_type): Raises: AssertionError: If invalid padding_type is provided. """ - assert padding_type in ["zeros", "ignore", "nearest"] + if padding_type not in ["zeros", "ignore", "nearest"]: + raise ValueError( + f"Invalid padding_type: '{padding_type}'. " + f"Supported padding types are: 'zeros', 'ignore', 'nearest'" + ) _, t, h, w, _ = x.shape if pad_t == 0 and pad_h == 0 and pad_w == 0: return x diff --git a/MindEarth/applications/nowcasting/PreDiff/src/vae/resnet.py b/MindEarth/applications/nowcasting/PreDiff/src/vae/resnet.py index 6a20d3b5f..b954f9d45 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/vae/resnet.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/vae/resnet.py @@ -120,7 +120,11 @@ class Upsample1D(nn.Cell): def construct(self, x): """forward""" - assert x.shape[1] == self.channels + if x.shape[1] != self.channels: + raise ValueError( + f"Input channels mismatch. Expected {self.channels} channels, " + f"but got {x.shape[1]} channels in dimension 1 of input tensor." + ) if self.use_conv_transpose: return self.conv(x) @@ -165,11 +169,14 @@ class Downsample1D(nn.Cell): has_bias=True, ) else: - assert self.channels == self.out_channels + if self.channels != self.out_channels: + raise RuntimeError( + f"Channels mismatch. Expected channels and out_channels to be equal, " + f"but got channels={self.channels}, out_channels={self.out_channels}." + ) self.conv = AvgPool1d(kernel_size=stride, stride=stride) def construct(self, x): - assert x.shape[1] == self.channels return self.conv(x) @@ -226,7 +233,14 @@ class Upsample2D(nn.Cell): def construct(self, hidden_states, output_size=None): """forward""" - assert hidden_states.shape[1] == self.channels + if hidden_states.shape[1] != self.channels: + raise ValueError( + f"Channel dimension mismatch in hidden states. " + f"Expected {self.channels} channels at dimension 1, " + f"but received {hidden_states.shape[1]} channels. " + f"Full shape: {tuple(hidden_states.shape)}" + ) + if self.use_conv_transpose: return self.conv(hidden_states) @@ -292,7 +306,13 @@ class Downsample2D(nn.Cell): has_bias=True, ) else: - assert self.channels == self.out_channels + if self.channels != self.out_channels: + raise RuntimeError( + f"Layer configuration conflict. channels ({self.channels}) " + f"must equal out_channels ({self.out_channels}). " + f"Check layer initialization parameters." + ) + conv = mint.nn.AvgPool2d(kernel_size=stride, stride=stride) if name == "conv": self.conv2d_0 = conv @@ -304,12 +324,28 @@ class Downsample2D(nn.Cell): def construct(self, hidden_states): """forward""" - assert hidden_states.shape[1] == self.channels + if hidden_states.shape[1] != self.channels: + raise ValueError( + f"Channel dimension mismatch in hidden states. Expected {self.channels} channels at dimension 1, " + f"but received tensor with {hidden_states.shape[1]} channels. " + f"Full shape: {tuple(hidden_states.shape)}" + ) + if self.use_conv and self.padding == 0: pad = (0, 1, 0, 1) hidden_states = ops.pad(hidden_states, pad, mode="constant", value=None) - assert hidden_states.shape[1] == self.channels + if hidden_states.shape[1] != self.channels: + raise ValueError( + f"Channel dimension mismatch in hidden states. " + f"Layer expects {self.channels} channels at dimension 1, " + f"but received {hidden_states.shape[1]} channels. " + f"Full tensor shape: {tuple(hidden_states.shape)}\n" + f"Possible solutions:\n" + f"1. Check input data pipeline\n" + f"2. Verify layer configuration (current channels: {self.channels})\n" + f"3. Inspect preceding layer's output channels" + ) hidden_states = self.conv(hidden_states) return hidden_states @@ -356,7 +392,17 @@ class FirUpsample2D(nn.Cell): Core upsampling operation with optional convolution and FIR filtering. """ - assert isinstance(factor, int) and factor >= 1 + if not isinstance(factor, int): + raise TypeError( + f"Invalid type for 'factor'. Expected integer, " + f"but got {type(factor).__name__}." + ) + + if factor < 1: + raise ValueError( + f"Invalid value for 'factor'. Must be >= 1, " + f"but got {factor}." + ) # Setup filter kernel. if kernel is None: @@ -387,7 +433,25 @@ class FirUpsample2D(nn.Cell): output_shape[0] - (hidden_states.shape[2] - 1) * stride[0] - convh, output_shape[1] - (hidden_states.shape[3] - 1) * stride[1] - convw, ) - assert output_padding[0] >= 0 and output_padding[1] >= 0 + if len(output_padding) < 2: + raise IndexError( + f"output_padding must have at least 2 elements, " + f"but got {len(output_padding)} elements" + ) + + errors = [] + if output_padding[0] < 0: + errors.append(f"output_padding[0] = {output_padding[0]} < 0") + if output_padding[1] < 0: + errors.append(f"output_padding[1] = {output_padding[1]} < 0") + + if errors: + raise ValueError( + f"Invalid output padding values:\n" + + "\n".join(errors) + + f"\nAll output padding values must be >= 0. " + f"Full output_padding: {output_padding}" + ) num_groups = hidden_states.shape[1] // in_c # Transpose weights. @@ -477,7 +541,17 @@ class FirDownsample2D(nn.Cell): """ Core downsampling operation with optional convolution and FIR filtering. """ - assert isinstance(factor, int) and factor >= 1 + if not isinstance(factor, int): + raise TypeError( + f"Invalid type for 'factor'. Expected integer, " + f"but got {type(factor).__name__} with value {repr(factor)}." + ) + + if factor < 1: + raise ValueError( + f"Invalid value for 'factor'. Must be a positive integer >= 1, " + f"but got {factor}." + ) if kernel is None: kernel = [1] * factor @@ -819,7 +893,17 @@ class ResidualTemporalBlock1D(nn.Cell): def upsample_2d(hidden_states, kernel=None, factor=2, gain=1): """Upsample2D a batch of 2D images with the given filter.""" - assert isinstance(factor, int) and factor >= 1 + if not isinstance(factor, int): + raise TypeError( + f"Invalid type for 'factor'. Expected integer, " + f"but got {type(factor).__name__} with value {repr(factor)}." + ) + + if factor < 1: + raise ValueError( + f"Invalid value for 'factor'. Must be a positive integer >= 1, " + f"but got {factor}." + ) if kernel is None: kernel = [1] * factor @@ -840,7 +924,12 @@ def upsample_2d(hidden_states, kernel=None, factor=2, gain=1): def downsample_2d(hidden_states, kernel=None, factor=2, gain=1): """Downsample2D a batch of 2D images with the given filter.""" - assert isinstance(factor, int) and factor >= 1 + if not isinstance(factor, int): + raise TypeError(f"factor must be an integer, got {type(factor).__name__}") + + if factor < 1: + raise ValueError(f"factor must be >= 1, got {factor}") + if kernel is None: kernel = [1] * factor diff --git a/MindEarth/applications/nowcasting/PreDiff/src/visual.py b/MindEarth/applications/nowcasting/PreDiff/src/visual.py index 343f3e17e..65d27e626 100644 --- a/MindEarth/applications/nowcasting/PreDiff/src/visual.py +++ b/MindEarth/applications/nowcasting/PreDiff/src/visual.py @@ -117,13 +117,30 @@ def vis_sevir_seq( if isinstance(seq, Sequence): seq_list = [ele.astype(np.float32) for ele in seq] - assert isinstance(label, Sequence) and len(label) == len(seq) + if not isinstance(label, Sequence): + raise TypeError( + f"label must be a Sequence (list, tuple, etc.), " + f"got {type(label).__name__}" + ) + + if len(label) != len(seq): + raise ValueError( + f"Length mismatch: label and seq must have same length\n" + f"• len(label) = {len(label)}\n" + f"• len(seq) = {len(seq)}" + ) label_list = label elif isinstance(seq, np.ndarray): seq_list = [ seq.astype(np.float32), ] - assert isinstance(label, str) + if not isinstance(label, str): + raise TypeError( + f"Invalid label type. Expected string, " + f"but got {type(label).__name__}. " + f"Value: {repr(label)}" + ) + label_list = [ label, ] -- Gitee From e19e80cd2e9d5c43da47973e277dfdfe9b1f88b8 Mon Sep 17 00:00:00 2001 From: goto Date: Wed, 23 Jul 2025 14:36:33 +0800 Subject: [PATCH 27/30] fix-mindflow-Fourier --- MindFlow/mindflow/core/fourier.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MindFlow/mindflow/core/fourier.py b/MindFlow/mindflow/core/fourier.py index 9425d32da..17c5c6467 100644 --- a/MindFlow/mindflow/core/fourier.py +++ b/MindFlow/mindflow/core/fourier.py @@ -626,8 +626,8 @@ class IDCT(nn.Cell): c = self.dft_cell(vr, vi) # (..., n) c1 = c[..., :(n + 1) // 2] c2 = self.fliper(c[..., (n + 1) // 2:], dims=-1) - d1 = ops.pad(c1[..., None], (0, 1)).reshape(*c1.shape[:-1], -1) - d2 = ops.pad(c2[..., None], (1, 0)).reshape(*c2.shape[:-1], -1) + d1 = ops.pad(c1.reshape(-1)[..., None], (0, 1)).reshape(*c1.shape[:-1], -1) + d2 = ops.pad(c2.reshape(-1)[..., None], (1, 0)).reshape(*c2.shape[:-1], -1) return d1 + d2 -- Gitee From 2425383e8a3109b0bf0ced2aad8e35099a36884b Mon Sep 17 00:00:00 2001 From: Yuheng Wang Date: Thu, 10 Jul 2025 14:34:48 +0800 Subject: [PATCH 28/30] add alphafold3 to MindSPONGE research jenkins gate codecheck filter update for AlphaFold3 pylint cleaning - lyy af3 codecheck lyy+ysy codecheck cleaning - cjh codecheck cleaning after cjh merge Update readme Update AF3 Readme --- .jenkins/check/config/filter_cppcheck.txt | 9 + .jenkins/check/config/filter_cpplint.txt | 19 + .jenkins/check/config/filter_linklint.txt | 6 +- .jenkins/check/config/filter_pylint.txt | 139 +- .jenkins/check/config/whitelizard.txt | 19 + .../research/AlphaFold3/CMakeLists.txt | 95 + .../applications/research/AlphaFold3/LICENSE | 437 +++ .../research/AlphaFold3/README.md | 258 ++ .../research/AlphaFold3/README_EN.md | 258 ++ .../research/AlphaFold3/example_input.json | 14 + .../AlphaFold3/image/af3_structure.jpg | Bin 0 -> 1669512 bytes .../research/AlphaFold3/requirements.txt | 6 + .../research/AlphaFold3/run_alphafold.py | 687 ++++ .../research/AlphaFold3/set_path.sh | 37 + .../AlphaFold3/src/alphafold3/__init__.py | 0 .../AlphaFold3/src/alphafold3/build_data.py | 45 + .../src/alphafold3/common/base_config.py | 151 + .../src/alphafold3/common/folding_input.py | 1115 ++++++ .../src/alphafold3/common/resources.py | 77 + .../src/alphafold3/common/testing/data.py | 70 + .../src/alphafold3/constants/atom_types.py | 262 ++ .../constants/chemical_component_sets.py | 38 + .../constants/chemical_components.py | 192 + .../constants/converters/ccd_pickle_gen.py | 53 + .../converters/chemical_component_sets_gen.py | 81 + .../src/alphafold3/constants/mmcif_names.py | 218 ++ .../alphafold3/constants/periodic_table.py | 399 +++ .../src/alphafold3/constants/residue_names.py | 421 +++ .../src/alphafold3/constants/side_chains.py | 112 + .../research/AlphaFold3/src/alphafold3/cpp.cc | 48 + .../alphafold3/data/cpp/msa_profile_pybind.cc | 79 + .../alphafold3/data/cpp/msa_profile_pybind.h | 25 + .../src/alphafold3/data/featurisation.py | 90 + .../AlphaFold3/src/alphafold3/data/msa.py | 346 ++ .../src/alphafold3/data/msa_config.py | 170 + .../src/alphafold3/data/msa_features.py | 204 ++ .../src/alphafold3/data/msa_identifiers.py | 86 + .../src/alphafold3/data/msa_store.py | 67 + .../AlphaFold3/src/alphafold3/data/parsers.py | 181 + .../src/alphafold3/data/pipeline.py | 543 +++ .../src/alphafold3/data/structure_stores.py | 102 + .../src/alphafold3/data/template_realign.py | 170 + .../src/alphafold3/data/template_store.py | 47 + .../src/alphafold3/data/templates.py | 974 +++++ .../src/alphafold3/data/tools/hmmalign.py | 144 + .../src/alphafold3/data/tools/hmmbuild.py | 148 + .../src/alphafold3/data/tools/hmmsearch.py | 152 + .../src/alphafold3/data/tools/jackhmmer.py | 137 + .../src/alphafold3/data/tools/msa_tool.py | 31 + .../src/alphafold3/data/tools/nhmmer.py | 175 + .../src/alphafold3/data/tools/rdkit_utils.py | 526 +++ .../alphafold3/data/tools/subprocess_utils.py | 108 + .../model/atom_layout/atom_layout.py | 1194 +++++++ .../src/alphafold3/model/base_config.py | 153 + .../alphafold3/model/components/base_model.py | 51 + .../model/components/base_modules.py | 148 + .../alphafold3/model/components/mapping.py | 353 ++ .../src/alphafold3/model/components/utils.py | 65 + .../src/alphafold3/model/confidence_types.py | 310 ++ .../src/alphafold3/model/confidences.py | 665 ++++ .../AlphaFold3/src/alphafold3/model/data3.py | 127 + .../src/alphafold3/model/data_constants.py | 27 + .../model/diffusion/atom_cross_attention.py | 466 +++ .../model/diffusion/confidence_head.py | 289 ++ .../model/diffusion/diffusion_head.py | 331 ++ .../model/diffusion/diffusion_transformer.py | 488 +++ .../model/diffusion/distogram_head.py | 88 + .../model/diffusion/featurization.py | 212 ++ .../alphafold3/model/diffusion/load_ckpt.py | 579 +++ .../src/alphafold3/model/diffusion/model.py | 758 ++++ .../src/alphafold3/model/diffusion/modules.py | 562 +++ .../model/diffusion/random/bias.npy | Bin 0 -> 1152 bytes .../model/diffusion/random/weight.npy | Bin 0 -> 1152 bytes .../model/diffusion/template_modules.py | 326 ++ .../alphafold3/model/diffusion/triangle.py | 262 ++ .../src/alphafold3/model/feat_batch.py | 180 + .../src/alphafold3/model/features.py | 2103 +++++++++++ .../src/alphafold3/model/load_batch.py | 22 + .../src/alphafold3/model/merging_features.py | 92 + .../src/alphafold3/model/mkdssp_pybind.cc | 63 + .../src/alphafold3/model/mkdssp_pybind.h | 26 + .../src/alphafold3/model/mmcif_metadata.py | 202 ++ .../src/alphafold3/model/model_config.py | 32 + .../src/alphafold3/model/msa_pairing.py | 316 ++ .../AlphaFold3/src/alphafold3/model/params.py | 218 ++ .../model/pipeline/inter_chain_bonds.py | 348 ++ .../src/alphafold3/model/pipeline/pipeline.py | 446 +++ .../model/pipeline/structure_cleaning.py | 370 ++ .../src/alphafold3/model/post_processing.py | 114 + .../model/protein_data_processing.py | 128 + .../src/alphafold3/model/scoring/alignment.py | 146 + .../model/scoring/covalent_bond_cleaning.py | 265 ++ .../src/alphafold3/model/scoring/scoring.py | 67 + .../src/alphafold3/parsers/cpp/cif_dict.pyi | 125 + .../alphafold3/parsers/cpp/cif_dict_lib.cc | 648 ++++ .../src/alphafold3/parsers/cpp/cif_dict_lib.h | 149 + .../alphafold3/parsers/cpp/cif_dict_pybind.cc | 652 ++++ .../alphafold3/parsers/cpp/cif_dict_pybind.h | 24 + .../alphafold3/parsers/cpp/fasta_iterator.pyi | 22 + .../parsers/cpp/fasta_iterator_lib.cc | 121 + .../parsers/cpp/fasta_iterator_lib.h | 94 + .../parsers/cpp/fasta_iterator_pybind.cc | 127 + .../parsers/cpp/fasta_iterator_pybind.h | 24 + .../alphafold3/parsers/cpp/msa_conversion.pyi | 26 + .../parsers/cpp/msa_conversion_pybind.cc | 162 + .../parsers/cpp/msa_conversion_pybind.h | 24 + .../src/alphafold3/structure/__init__.py | 46 + .../src/alphafold3/structure/bioassemblies.py | 333 ++ .../src/alphafold3/structure/bonds.py | 237 ++ .../structure/chemical_components.py | 286 ++ .../alphafold3/structure/cpp/aggregation.pyi | 13 + .../structure/cpp/aggregation_pybind.cc | 54 + .../structure/cpp/aggregation_pybind.h | 24 + .../alphafold3/structure/cpp/membership.pyi | 18 + .../structure/cpp/membership_pybind.cc | 82 + .../structure/cpp/membership_pybind.h | 24 + .../alphafold3/structure/cpp/mmcif_altlocs.cc | 249 ++ .../alphafold3/structure/cpp/mmcif_altlocs.h | 51 + .../structure/cpp/mmcif_atom_site.pyi | 23 + .../structure/cpp/mmcif_atom_site_pybind.cc | 83 + .../structure/cpp/mmcif_atom_site_pybind.h | 24 + .../alphafold3/structure/cpp/mmcif_layout.h | 146 + .../alphafold3/structure/cpp/mmcif_layout.pyi | 26 + .../structure/cpp/mmcif_layout_lib.cc | 213 ++ .../structure/cpp/mmcif_layout_pybind.cc | 49 + .../structure/cpp/mmcif_layout_pybind.h | 24 + .../structure/cpp/mmcif_struct_conn.h | 34 + .../structure/cpp/mmcif_struct_conn.pyi | 13 + .../structure/cpp/mmcif_struct_conn_lib.cc | 380 ++ .../structure/cpp/mmcif_struct_conn_pybind.cc | 68 + .../structure/cpp/mmcif_struct_conn_pybind.h | 24 + .../alphafold3/structure/cpp/mmcif_utils.pyi | 71 + .../structure/cpp/mmcif_utils_pybind.cc | 787 ++++ .../structure/cpp/mmcif_utils_pybind.h | 24 + .../alphafold3/structure/cpp/string_array.pyi | 50 + .../structure/cpp/string_array_pybind.cc | 329 ++ .../structure/cpp/string_array_pybind.h | 24 + .../src/alphafold3/structure/mmcif.py | 333 ++ .../src/alphafold3/structure/parsing.py | 1805 ++++++++++ .../src/alphafold3/structure/sterics.py | 142 + .../src/alphafold3/structure/structure.py | 3180 +++++++++++++++++ .../alphafold3/structure/structure_tables.py | 842 +++++ .../src/alphafold3/structure/table.py | 565 +++ .../src/alphafold3/structure/test_utils.py | 358 ++ .../alphafold3/utils/attention/attention.py | 77 + .../utils/attention/attention_base.py | 269 ++ .../attention/attention_call_arg_specs.py | 61 + .../utils/attention/ms_attention.py | 96 + .../src/alphafold3/utils/common/precision.py | 91 + .../gated_linear_unit/gated_linear_unit.py | 66 + .../gated_linear_unit_base.py | 84 + .../src/alphafold3/utils/geometry/__init__.py | 28 + .../utils/geometry/rigid_matrix_vector.py | 194 + .../utils/geometry/rotation_matrix.py | 255 ++ .../utils/geometry/struct_of_array.py | 280 ++ .../src/alphafold3/utils/geometry/utils.py | 149 + .../src/alphafold3/utils/geometry/vector.py | 255 ++ .../mindchemistry.cell.orb.EnergyHead.rst | 2 +- 158 files changed, 37499 insertions(+), 3 deletions(-) create mode 100644 MindSPONGE/applications/research/AlphaFold3/CMakeLists.txt create mode 100644 MindSPONGE/applications/research/AlphaFold3/LICENSE create mode 100644 MindSPONGE/applications/research/AlphaFold3/README.md create mode 100644 MindSPONGE/applications/research/AlphaFold3/README_EN.md create mode 100644 MindSPONGE/applications/research/AlphaFold3/example_input.json create mode 100644 MindSPONGE/applications/research/AlphaFold3/image/af3_structure.jpg create mode 100644 MindSPONGE/applications/research/AlphaFold3/requirements.txt create mode 100644 MindSPONGE/applications/research/AlphaFold3/run_alphafold.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/set_path.sh create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/__init__.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/build_data.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/base_config.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/atom_types.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_component_sets.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/ccd_pickle_gen.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/converters/chemical_component_sets_gen.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/mmcif_names.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/periodic_table.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/residue_names.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/side_chains.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/cpp.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/featurisation.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_config.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_features.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_identifiers.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_store.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/parsers.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/structure_stores.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_realign.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_store.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/templates.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmalign.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmbuild.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/jackhmmer.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/msa_tool.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/nhmmer.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/rdkit_utils.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/subprocess_utils.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/atom_layout/atom_layout.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/base_config.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_model.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/utils.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidence_types.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidences.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data3.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/data_constants.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/confidence_head.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_transformer.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/distogram_head.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/bias.npy create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/random/weight.npy create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/triangle.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/feat_batch.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/features.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/load_batch.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/merging_features.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/model_config.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/msa_pairing.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/params.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/inter_chain_bonds.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/pipeline.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/structure_cleaning.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/protein_data_processing.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/alignment.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/covalent_bond_cleaning.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_lib.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/__init__.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bioassemblies.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bonds.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/chemical_components.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array.pyi create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.h create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/sterics.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure_tables.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/table.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/test_utils.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_base.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_call_arg_specs.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/common/precision.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rigid_matrix_vector.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rotation_matrix.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/utils.py create mode 100644 MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py diff --git a/.jenkins/check/config/filter_cppcheck.txt b/.jenkins/check/config/filter_cppcheck.txt index 5ceb4144c..ba103f4eb 100644 --- a/.jenkins/check/config/filter_cppcheck.txt +++ b/.jenkins/check/config/filter_cppcheck.txt @@ -2,3 +2,12 @@ "mindscience/MindElec/mindelec/ccsrc/api/python/pybind_register.cc" "syntaxError" "mindscience/MindElec/mindelec/ccsrc/scientific_compute/pointcloud/material_analyse.cc" "useStlAlgorithm" "mindscience/MindElec/mindelec/ccsrc/scientific_compute/pointcloud/tensor_initializer.cc" "useStlAlgorithm" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc" "shadowFunction" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc" "useStlAlgorithm" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc" "variableScope" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc" "shadowVariable" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc" "useStlAlgorithm" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "unsignedLessThanZero" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc" "shadowVariable" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc" "useStlAlgorithm" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/string_array_pybind.cc" "pointerSize" diff --git a/.jenkins/check/config/filter_cpplint.txt b/.jenkins/check/config/filter_cpplint.txt index 9ceab3d1e..205e7a725 100644 --- a/.jenkins/check/config/filter_cpplint.txt +++ b/.jenkins/check/config/filter_cpplint.txt @@ -597,3 +597,22 @@ "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/barostats/MC_barostat.cu" "whitespace/parens" "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/thermostats/Andersen_thermostat.cu" "whitespace/parens" "mindscience/MindSPONGE/mindsponge/ccsrc/molecular_dynamics/common.cuh" "build/include_subdir" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "whitespace/parens" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc" "runtime/references" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc" "build/include_order" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mkdssp_pybind.cc" "whitespace/ending_newline" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/msa_conversion_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "whitespace/braces" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "whitespace/parens" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/cpp/msa_profile_pybind.cc" "whitespace/ending_newline" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_atom_site_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/aggregation_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc" "whitespace/braces" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/fasta_iterator_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/membership_pybind.cc" "build/include" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_altlocs.cc" "runtime/references" \ No newline at end of file diff --git a/.jenkins/check/config/filter_linklint.txt b/.jenkins/check/config/filter_linklint.txt index cea87ddfc..20e8fe38e 100644 --- a/.jenkins/check/config/filter_linklint.txt +++ b/.jenkins/check/config/filter_linklint.txt @@ -3,4 +3,8 @@ https://api.colabfold.com https://a3m.mmseqs.com -https://www.mindspore.cn/community/SIG/detail/?name=mindflow+SIG \ No newline at end of file +https://www.mindspore.cn/community/SIG/detail/?name=mindflow+SIG + +https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Items/_entity.type.html* +https://doi.org/10.1002/prot.340200303* +https://arxiv.org/pdf/2006.14616.pdf* \ No newline at end of file diff --git a/.jenkins/check/config/filter_pylint.txt b/.jenkins/check/config/filter_pylint.txt index d680aadf0..9b3177782 100644 --- a/.jenkins/check/config/filter_pylint.txt +++ b/.jenkins/check/config/filter_pylint.txt @@ -161,4 +161,141 @@ "mindscience/MindSPONGE/tutorials/basic/tutorial_p05.py" "wrong-import-position" "mindscience/tests/st/mindflow/cell/test_dft.py" "wrong-import-position" "mindscience/tests/st/mindflow/cell/test_fno1d.py" "wrong-import-position" -"mindscience/tests/st/mindflow/cell/attention/test_attention.py" "wrong-import-position" +"mindscience/tests/st/mindflow/cell/attention/test_attention.py" "wrong-import-position" + +# DeepMind AlphaFold3 +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/nhmmer.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/structure_cleaning.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py" "multiple-statements" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/base_config.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/__init__.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/table.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py" "assigning-non-slot" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py" "unused-variable" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmalign.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/jackhmmer.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_store.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/covalent_bond_cleaning.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_features.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_identifiers.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/base_config.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/bonds.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_store.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py" "pointless-statement" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/parsers.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/template_realign.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py" "function-redefined" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/chemical_components.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/protein_data_processing.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/merging_features.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/confidence_types.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/scoring.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/templates.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/pipeline.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/chemical_components.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/featurisation.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/resources.py" "pointless-statement" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/structure_stores.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/msa_config.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/subprocess_utils.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/scoring/alignment.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/mmcif.py" "invalid-name" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/test_utils.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/pipeline/inter_chain_bonds.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py" "function-redefined" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmsearch.py" "useless-object-inheritance" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/post_processing.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/periodic_table.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/structure_tables.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/hmmbuild.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/constants/mmcif_names.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/testing/data.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/tools/msa_tool.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention_base.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/feat_batch.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "invalid-name" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "pointless-statement" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/atom_layout/atom_layout.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/base_modules.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/params.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/ms_attention.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/attention/attention.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/utils.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/atom_cross_attention.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/common/precision.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py" "invalid-name" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold_data_test.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py" "cell-var-from-loop" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/vector.py" "invalid-name" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_head.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "unexpected-keyword-arg" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold_test_v2.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "function-redefined" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "len-as-condition" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "unsupported-membership-test" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/featurization.py" "redefined-argument-from-local" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/distogram_head.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "unsupported-assignment-operation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/struct_of_array.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/__init__.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "multiple-statements" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "no-else-return" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "too-many-function-args" +"mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/mapping.py" "unused-variable" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/features.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/confidence_head.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/diffusion_transformer.py" "syntax-error" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/components/utils.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py" "bad-continuation" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/template_modules.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py" "unused-import" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/geometry/rigid_matrix_vector.py" "bad-whitespace" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py" "no-value-for-parameter" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py" "pointless-statement" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/modules.py" "missing-docstring" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/load_ckpt.py" "protected-access" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py" "redefined-argument-from-local" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/triangle.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/utils/gated_linear_unit/gated_linear_unit_base.py" "unused-argument" +"mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/mmcif_metadata.py" "unused-argument" \ No newline at end of file diff --git a/.jenkins/check/config/whitelizard.txt b/.jenkins/check/config/whitelizard.txt index 1d50c2b87..7503472fd 100644 --- a/.jenkins/check/config/whitelizard.txt +++ b/.jenkins/check/config/whitelizard.txt @@ -13,3 +13,22 @@ mindscience/MindChemistry/mindchemistry/so2_conv/wigner.py:wigner_D mindscience/MindSPONGE/src/sponge/system/modelling/mol2_parser.py:mol2parser mindscience/MindSPONGE/src/sponge/system/modelling/hadder.py:add_hydrogen mindscience/MindSPONGE/src/sponge/system/molecule/molecule.py:build_system +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py:from_alphafoldserver_fold_job +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py:from_json +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::GetEscapeQuote +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::CifDict::ToString +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::Gather +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::CifDictGetArray +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::RegisterModuleCifDict +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_utils_pybind.cc:alphafold3::FixArginine::Fix +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc:alphafold3::MmcifLayout::Create +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc:alphafold3::GetBondAtomIndices +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/data/pipeline.py:__init__ +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/common/folding_input.py:from_json +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_lib.cc:alphafold3::CifDict::ToString +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/parsers/cpp/cif_dict_pybind.cc:alphafold3::RegisterModuleCifDict +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_layout_lib.cc:alphafold3::MmcifLayout::Create +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/cpp/mmcif_struct_conn_lib.cc:alphafold3::GetBondAtomIndices +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/structure/parsing.py:from_res_arrays +mindscience/MindSPONGE/applications/research/AlphaFold3/src/alphafold3/model/diffusion/model.py:get_inference_result +mindscience/MindSPONGE/applications/research/AlphaFold3/run_alphafold_test_v2.py:test_inference diff --git a/MindSPONGE/applications/research/AlphaFold3/CMakeLists.txt b/MindSPONGE/applications/research/AlphaFold3/CMakeLists.txt new file mode 100644 index 000000000..81162722e --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/CMakeLists.txt @@ -0,0 +1,95 @@ +# Copyright 2024 DeepMind Technologies Limited +# +# AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of +# this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ +# +# To request access to the AlphaFold 3 model parameters, follow the process set +# out at https://github.com/google-deepmind/alphafold3. You may only use these +# if received directly from Google. Use is subject to terms of use available at +# https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md + +cmake_minimum_required(VERSION 3.28) +project( + "${SKBUILD_PROJECT_NAME}" + LANGUAGES CXX + VERSION "${SKBUILD_PROJECT_VERSION}") + +include(FetchContent) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) +set(ABSL_PROPAGATE_CXX_STD ON) + +# Remove support for scan deps, which is only useful when using C++ modules. +unset(CMAKE_CXX_SCANDEP_SOURCE) + +FetchContent_Declare( + abseil-cpp + GIT_REPOSITORY https://github.com/abseil/abseil-cpp + GIT_TAG d7aaad83b488fd62bd51c81ecf16cd938532cc0a # 20240116.2 + EXCLUDE_FROM_ALL) + +FetchContent_Declare( + pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11 + GIT_TAG 2e0815278cb899b20870a67ca8205996ef47e70f # v2.12.0 + EXCLUDE_FROM_ALL) + +FetchContent_Declare( + pybind11_abseil + GIT_REPOSITORY https://github.com/pybind/pybind11_abseil + GIT_TAG bddf30141f9fec8e577f515313caec45f559d319 # HEAD @ 2024-08-07 + EXCLUDE_FROM_ALL) + +FetchContent_Declare( + cifpp + GIT_REPOSITORY https://github.com/pdb-redo/libcifpp + GIT_TAG ac98531a2fc8daf21131faa0c3d73766efa46180 # v7.0.3 + # Don't `EXCLUDE_FROM_ALL` as necessary for build_data. +) + +FetchContent_Declare( + dssp + GIT_REPOSITORY https://github.com/PDB-REDO/dssp + GIT_TAG 57560472b4260dc41f457706bc45fc6ef0bc0f10 # v4.4.7 + EXCLUDE_FROM_ALL) + +FetchContent_MakeAvailable(pybind11 abseil-cpp pybind11_abseil cifpp dssp) + +find_package( + Python3 + COMPONENTS Interpreter Development NumPy + REQUIRED) + +include_directories(${PYTHON_INCLUDE_DIRS}) +include_directories(src/) + +file(GLOB_RECURSE cpp_srcs src/alphafold3/*.cc) +list(FILTER cpp_srcs EXCLUDE REGEX ".*\(_test\|_main\|_benchmark\).cc$") + +add_compile_definitions(NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION) + +pybind11_add_module(cpp ${cpp_srcs}) + +target_link_libraries( + cpp + PRIVATE absl::check + absl::flat_hash_map + absl::node_hash_map + absl::strings + absl::status + absl::statusor + absl::log + pybind11_abseil::absl_casters + Python3::NumPy + dssp::dssp + cifpp::cifpp) + +target_compile_definitions(cpp PRIVATE VERSION_INFO=${PROJECT_VERSION}) +install(TARGETS cpp LIBRARY DESTINATION alphafold3) +install( + FILES LICENSE + OUTPUT_TERMS_OF_USE.md + WEIGHTS_PROHIBITED_USE_POLICY.md + WEIGHTS_TERMS_OF_USE.md + DESTINATION alphafold3) diff --git a/MindSPONGE/applications/research/AlphaFold3/LICENSE b/MindSPONGE/applications/research/AlphaFold3/LICENSE new file mode 100644 index 000000000..bfef380bf --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/LICENSE @@ -0,0 +1,437 @@ +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/README.md b/MindSPONGE/applications/research/AlphaFold3/README.md new file mode 100644 index 000000000..6e23913ab --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/README.md @@ -0,0 +1,258 @@ +# AlphaFold3-MindSpore + +[**MindSpore版 AlphaFold3实现**] 一个基于MindSpore深度学习框架的AlphaFold3推理网络结构实现。 + +> 📖 **语言版本**: [中文](README.md) | [English](README_EN.md) + +## 📑 目录 + +- [项目简介](#项目简介) +- [安装](#安装) +- [快速开始](#快速开始) +- [详细使用说明](#详细使用说明) +- [许可证](#许可证) +- [致谢](#致谢) +- [参考文献](#参考文献) + +## 项目简介 + +**项目背景**: +AlphaFold3是DeepMind在2024年发布的革命性生物分子结构预测模型,能够预测蛋白质、DNA、RNA等生物大分子的三维结构。本项目基于Ascend NPU和MindSpore框架,实现了AlphaFold3的推理功能。 + +AlphaFold3 的模型结构如下图所示: + +![AlphaFold3 模型结构](image/af3_structure.jpg) + +- **推理流程**:首先输入的蛋白,核酸,配体等序列信息,经过模板搜索(Template Search)、多序列比对(Multiple Sequence Alignment, MSA)等预处理步骤,然后通过embeding部分对输入信息进行编码,之后通过Pairformer模块,获取序列及结构的关系,接着进入扩散模块生成三维结构,最后通过置信度模块给出预测的置信度评分 +- **生物分子结构预测**: 基于AlphaFold3算法的生物分子结构预测模型,支持包括蛋白质,DNA,RNA,小分子在内的多种输入形式;支持多链输入,预测相互作用和相对位置 +- **MindSpore支持**: 基于MindSpore对模型推理功能进行适配 + +### 硬件要求 + +- Atlas 800T A2 + +### 软件要求 + +- Python >= 3.11 +- MindSpore >= 2.5.0 +- CANN = 8.0.0 +- cmake >= 3.28.1 + +## 安装 + +### 1. 克隆仓库 + +```bash +git clone https://gitee.com/mindspore/mindscience +cd mindsience/MindSPONGE/application/research/AlphaFold3 +``` + +### 2. 安装依赖 + +```bash +pip install -r requirements.txt +#`{PATH}` 为当前目录 +export PYTHONPATH={PATH}/mindscience/MindSPONGE/src +export PYTHONPATH={PATH}/mindscience/MindChemistry +``` + +### 3. 安装软件包 + +[hmmer](http://eddylab.org/software/hmmer/) 在链接处下载安装包,如 `hmmer-3.4.tar.gz`,并放置在当前目录下,然后执行以下命令: + +```bash +mkdir /path/to/hmmer_build /path/to/hmmer && \ +mv ./hmmer-3.4.tar.gz /path/to/hmmer_build && \ +cd /path/to/hmmer_build && tar -zxf hmmer-3.4.tar.gz && rm hmmer-3.4.tar.gz && \ +cd /path/to/hmmer_build/hmmer-3.4 && ./configure --prefix=/path/to/hmmer && \ +make -j8 && make install && \ +cd /path/to/hmmer_build/hmmer-3.4/easel && make install && \ +rm -rf /path/to/hmmer_build +export PATH=/hmmer/bin:$PATH +which jackhmmer +``` + +如果出现`/path/to/hmmer/bin/jackhmmer`则安装成功 + +### 4. 编译 + +```bash +cd {PATH}/mindscience/MindSPONGE/applications/research/AlphaFold3 +mkdir build +cd build +cmake .. +make +cp ./cpp.cpython-311-aarch64-linux-gnu.so ../src/alphafold +cd .. +``` + +生成数据文件: + +```bash +python ./src/alphafold3/build_data.py +``` + +如出现报错找不到components.cif,可以去[wwpdb](https://files.wwpdb.org/pub/pdb/data/monomers/components.cif)下载components.cif文件,放置在conda环境中`{CONDA_ENV_DIR}/lib/python3.11/site-packages/share/libcifpp`文件夹下。如不存在`share/libcifpp`文件夹,则需要手动创建。 + +### 5. 下载数据库 + +可以从DeepMind官网下载测试用小数据库[miniature_databases](https://github.com/google-deepmind/alphafold3/tree/main/src/alphafold3/test_data/miniature_databases)(影响推理结果,仅测试使用!) +下载后放置在统一文件夹中并修改文件名如下所示(如统一放置在`/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`可省略`--db_dir=/PATH/TO/DB_DIR`): + +```txt +miniature_databases + └─ mmcif_files + │ bfd-first_non_consensus_sequences.fasta + │ mgy_clusters_2022_05.fa + │ pdb_seqres_2022_09_28.fasta + │ uniprot_all_2021_04.fa + │ uniref90_2022_05.fa + │ nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta + │ rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta + │ rnacentral_active_seq_id_90_cov_80_linclust.fasta +``` + +如果想要搜索完整的数据库,请从以下链接下载数据库,放置到同一文件夹中(如统一放置在`/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`可省略`--db_dir=/PATH/TO/DB_DIR`): + +- [mmcif](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_2022_09_28_mmcif_files.tar.zst) +- [BFD](https://storage.googleapis.com/alphafold-databases/v3.0/bfd-first_non_consensus_sequences.fasta.zst) +- [MGnify](https://storage.googleapis.com/alphafold-databases/v3.0/mgy_clusters_2022_05.fa.zst) +- [PDB seqres](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_seqres_2022_09_28.fasta.zst) +- [UniProt](https://storage.googleapis.com/alphafold-databases/v3.0/uniprot_all_2021_04.fa.zst) +- [uniref90](https://storage.googleapis.com/alphafold-databases/v3.0/uniref90_2022_05.fa.zst) +- [NT](https://storage.googleapis.com/alphafold-databases/v3.0/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst) +- [RFam](https://storage.googleapis.com/alphafold-databases/v3.0/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst) +- [RNACentral](https://storage.googleapis.com/alphafold-databases/v3.0/rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst) + +请确保磁盘中有足够空间: +| DataBase | Compressed Size | Uncompressed Size| +|--------------|---------------------|------------------| +| mmcif | 233G | 233G | +| BFD | 9.2G | 16.9G | +| MGnify | 64.5G | 119G | +| PDB seqres| 25.3M | 217M | +| UniProt | 45.3G | 101G | +| uniref90 | 30.9G | 66.8G | +| NT | 15.8G | 75.4G | +| RFam | 53.9M | 217M | +| RNACentral| 3.27G | 12.9G | +| total | 402G | 534G | + +解压下载的数据文件: + +```bash +cd /PATH/TO/YOUR/DATA_DIR +tar –use-compress-program=unzstd -xf pdb_2022_09_28_mmcif_files.tar.zst +zstd -d bfd-first_non_consensus_sequences.fasta.zst +zstd -d mgy_clusters_2022_05.fa.zst +zstd -d pdb_seqres_2022_09_28.fasta.zst +zstd -d uniprot_all_2021_04.fa.zst +zstd -d uniref90_2022_05.fa.zst +zstd -d nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst +zstd -d rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst +zstd -d rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst +``` + +如统一放置在`/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`可在运行时省略`--db_dir=/PATH/TO/DB_DIR` + +## 快速开始 + +### 输入数据格式 + +示例输入JSON: + +```json +{ + "name": "5tgy", + "sequences": [ + { + "protein": { + "id": "A", + "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" + } + } + ], + "modelSeeds": [1], + "dialect": "alphafold3", + "version": 1 +} +``` + +### 运行流程 + +使用以下命令运行模型(计算精度float32): + +```bash +source set_path.sh +python run_alphafold.py \ + --json_path=example_input.json \ + --output_dir=output \ + --run_data_pipeline=true \ + --run_inference=true \ + --db_dir=/PATH/TO/DB_DIR \ + --model_dir=/PATH/TO/MODEL_DIR\ + --buckets=256 +``` + +### 参数说明 + +- `--json_path`输入文件名称 +- `--output_dir`: 输出文件路径 +- `--run_data_pipeline`: 是否运行数据处理模块 +- `--run_inference`: 是否运行推理模块 +- `--db_dir`: 数据库存放路径, 默认 `{HOME}/public_databases` +- `--model_dir`: 模型文件路径, 默认 `{HOME}/ckpt` +- `--buckets`: 设定序列长度,如不设置会将序列长度padding到256的倍数,如传入则使用传入值作为序列长度 + +### 输入与输出文件说明 + +- **JSON格式数据输入**: 包含蛋白质核酸等的序列信息。当前支持输入种类与DeepMind版本相同,支持蛋白质,DNA,RNA及Ligand作为输入,当前推理版本为单卡版本支持序列长度不超过1000 + +- **输出文件**: 5个标准的蛋白质结构文件,及置信度信息 + +```txt +└─name_in_your_json + └─ seed-1_sample-0 # 第一个生成样本 + │ confidence.json # 第一个样本的详细置信度文件 + │ model.cif # 第一个样本的结构文件 + │ summary_confidence.json # 第一个样本的总体置信度文件 + └─ seed-1_sample-1 # 第二个生成样本 + └─ seed-1_sample-2 # 第三个生成样本 + └─ seed-1_sample-3 # 第四个生成样本 + └─ seed-1_sample-4 # 第五个生成样本 + │ {name}_confidences.json # 最优样本的详细置信度文件 + │ {name}_data.json # 数据处理后的数据文件 + │ {name}_model.cif # 最优样本的结构文件 + │ {name}_summary_confidence.json # 最优样本的总体置信度文件 + │ ranking_scores.csv # 五个样本的ranking score;ranking score越高,表明置信度越高 +``` + +### 推理完成 + +当看到如下日志,表明推理正常结束: + +```txt +=======write output to /PATH/TO/OUTPUT/DIR/name_of_your_input========== +Done processing fold input name_of_your_input. +Done processing 1 fold inputs. +``` + +## 许可证 + +详情请参阅 [LICENSE](LICENSE) 文件。 + +## 致谢 + +- `data`,`structure`,`common`,`constant`等模块使用了[DeepMind](https://deepmind.com/)实现 +- `model`,`utils`等模块基于[MindSpore](https://www.mindspore.cn/)实现 + +## 联系我们 + +如果您在使用过程中遇到任何问题或有任何建议,请通过以下方式与我们联系: + +- **Gitee仓库**:[AlphaFold3](https://gitee.com/mindspore/mindscience/tree/main/MindSPONGE/applications/research/AlphaFold3) +- **问题跟踪**:[问题单跟踪](https://gitee.com/mindspore/mindscience/issues) + +## 参考文献 + +- Abramson J, Adler J, Dunger J, et al. Accurate structure prediction of biomolecular interactions with AlphaFold 3[J]. Nature, 2024, 630(8016): 493-500. \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/README_EN.md b/MindSPONGE/applications/research/AlphaFold3/README_EN.md new file mode 100644 index 000000000..a2ab03ff8 --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/README_EN.md @@ -0,0 +1,258 @@ +# AlphaFold3-MindSpore + +[**MindSpore Implementation of AlphaFold3**] A MindSpore-based deep learning framework implementation of AlphaFold3 inference network architecture. + +> 📖 **Language**: [中文](README.md) | [English](README_EN.md) + +## 📑 Table of Contents + +- [Project Overview](#project-overview) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [License](#license) +- [Acknowledgments](#acknowledgments) +- [Reference](#reference) + +## Project Overview + +**Project Background**: +AlphaFold3 is a revolutionary biomolecular structure prediction model released by DeepMind in 2024, capable of predicting the three-dimensional structures of proteins, DNA, RNA, and other biological macromolecules. This project implements AlphaFold3's inference functionality based on Ascend NPU and MindSpore framework. + +Model Architecture is shown below: + +![AlphaFold3 Model Structure](image/af3_structure.jpg) + +- **Inference Pipeline**:The workflow begins with the provision of sequence information for proteins, DNA, RNA, and ligands. This data undergoes preprocessing steps, including template search and multiple sequence alignment, before being fed into the model. Next, an embedding module encodes the input information. Subsequently, the Pairformer cycles analyze the relationships between the sequences and their structures. Following this, a diffusion module generates the 3D structures. Finally, a confidence module assigns a confidence score to the predictions, providing a measure of their reliability. +- **Biomolecular Structure Prediction**: A biomolecular structure prediction model based on the AlphaFold3 algorithm, supporting various input forms including proteins, DNA, RNA, and small molecules; enabling multi-chain inputs and predicting interactions and relative positions. +- **MindSpore Support**: Model Inference adaptation based on MindSpore. + +### Hardware Requirements + +- Atlas 800T A2 + +### Software Requirements + +- Python >= 3.11 +- MindSpore >= 2.5.0 +- CANN = 8.0.0 +- cmake >= 3.28.1 + +## Installation + +### 1. Clone Repository + +```bash +git clone https://gitee.com/mindspore/mindscience +cd mindsience/MindSPONGE/application/research/AlphaFold3 +``` + +### 2. Install Dependencies + +```bash +pip install -r requirements.txt +#`{PATH}` is the current path +export PYTHONPATH={PATH}/mindscience/MindSPONGE/src +export PYTHONPATH={PATH}/mindscience/MindChemistry +``` + +### 3. Installing the Software Package + +Download the installation package from the link [hmmer](http://eddylab.org/software/hmmer/) , such as hmmer-3.4.tar.gz, and place it in the current directory. + +```bash +mkdir /path/to/hmmer_build /path/to/hmmer && \ +mv ./hmmer-3.4.tar.gz /path/to/hmmer_build && \ +cd /path/to/hmmer_build && tar -zxf hmmer-3.4.tar.gz && rm hmmer-3.4.tar.gz && \ +cd /path/to/hmmer_build/hmmer-3.4 && ./configure --prefix=/path/to/hmmer && \ +make -j8 && make install && \ +cd /path/to/hmmer_build/hmmer-3.4/easel && make install && \ +rm -rf /path/to/hmmer_build +export PATH=/hmmer/bin:$PATH +which jackhmmer +``` + +If the file `/path/to/hmmer/bin/jackhmmer` appears, the installation is successful. + +### 4. Compile + +```bash +cd {PATH}/mindscience/MindSPONGE/applications/research/AlphaFold3 +mkdir build +cd build +cmake .. +make +cp ./cpp.cpython-311-aarch64-linux-gnu.so ../src/alphafold +cd .. +``` + +Then, we need to generate data file: + +```bash +python ./src/alphafold3/build_data.py +``` + +if you see the error 'counld not find components.cif', download the file from [wwpdb](https://files.wwpdb.org/pub/pdb/data/monomers/components.cif),then put this file in your conda environment, `{CONDA_ENV_DIR}/lib/python3.11/site-packages/share/libcifpp`. If there is no `share/libcifpp` direction, create the direction by yourself. + +### 5. Download DataBase + +You can download a small test database from DeepMind [miniature_databases](https://github.com/google-deepmind/alphafold3/tree/main/src/alphafold3/test_data/miniature_databases)(Only for test,have influence to inference result!) +Download and put all the files in the same direction (No need to set `--db_dir=/PATH/TO/DB_DIR` if all the database are put in `/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`) and rename the file like the example below: + +```txt +miniature_databases + └─ mmcif_files + │ bfd-first_non_consensus_sequences.fasta + │ mgy_clusters_2022_05.fa + │ pdb_seqres_2022_09_28.fasta + │ uniprot_all_2021_04.fa + │ uniref90_2022_05.fa + │ nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta + │ rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta + │ rnacentral_active_seq_id_90_cov_80_linclust.fasta +``` + +If you want to seearch the full database, download the following database, and put them in the same direction(No need to set `--db_dir=/PATH/TO/DB_DIR` if all the database are put in `/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`): + +- [mmcif](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_2022_09_28_mmcif_files.tar.zst) +- [BFD small](https://storage.googleapis.com/alphafold-databases/v3.0/bfd-first_non_consensus_sequences.fasta.zst) +- [MGnify](https://storage.googleapis.com/alphafold-databases/v3.0/mgy_clusters_2022_05.fa.zst) +- [PDB seqres](https://storage.googleapis.com/alphafold-databases/v3.0/pdb_seqres_2022_09_28.fasta.zst) +- [UniProt](https://storage.googleapis.com/alphafold-databases/v3.0/uniprot_all_2021_04.fa.zst) +- [uniref90](https://storage.googleapis.com/alphafold-databases/v3.0/uniref90_2022_05.fa.zst) +- [NT](https://storage.googleapis.com/alphafold-databases/v3.0/nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst) +- [RFam](https://storage.googleapis.com/alphafold-databases/v3.0/rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst) +- [RNACentral](https://storage.googleapis.com/alphafold-databases/v3.0/rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst) + +Make sure having enough space on disk: + +| DataBase | Compressed Size | Uncompressed Size| +|--------------|---------------------|------------------| +| mmcif | 233G | 233G | +| BFD | 9.2G | 16.9G | +| MGnify | 64.5G | 119G | +| PDB seqres| 25.3M | 217M | +| UniProt | 45.3G | 101G | +| uniref90 | 30.9G | 66.8G | +| NT | 15.8G | 75.4G | +| RFam | 53.9M | 217M | +| RNACentral| 3.27G | 12.9G | +| total | 402G | 534G | + +Uncompressing the following database file: + +```bash +cd /PATH/TO/YOUR/DATA_DIR +tar –use-compress-program=unzstd -xf pdb_2022_09_28_mmcif_files.tar.zst +zstd -d bfd-first_non_consensus_sequences.fasta.zst +zstd -d mgy_clusters_2022_05.fa.zst +zstd -d pdb_seqres_2022_09_28.fasta.zst +zstd -d uniprot_all_2021_04.fa.zst +zstd -d uniref90_2022_05.fa.zst +zstd -d nt_rna_2023_02_23_clust_seq_id_90_cov_80_rep_seq.fasta.zst +zstd -d rfam_14_9_clust_seq_id_90_cov_80_rep_seq.fasta.zst +zstd -d rnacentral_active_seq_id_90_cov_80_linclust.fasta.zst +``` + +If all the files are put under`/mindscience/MindSPONGE/applications/research/AlphaFold3/public_databases`, the setting `--db_dir=/PATH/TO/DB_DIR` can be ignored. + +## Quick Start + +### Input Structure + +Example Input JSON: + +```json +{ + "name": "5tgy", + "sequences": [ + { + "protein": { + "id": "A", + "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" + } + } + ], + "modelSeeds": [1], + "dialect": "alphafold3", + "version": 1 +} +``` + +### Running Pipeline + +AlphaFold3 can be run with the following command(Precision: float32). + +```bash +source set_path.sh +python run_alphafold.py \ + --json_path=example_input.json \ + --output_dir=output \ + --run_data_pipeline=true \ + --run_inference=true \ + --db_dir=/PATH/TO/DB_DIR \ + --model_dir=/PATH/TO/MODEL_DIR \ + --buckets=256 +``` + +### Parameter Introduction + +- `--json_path`: Name of input json +- `--output_dir`: Output direction +- `--run_data_pipeline`: run data-pipeline or not +- `--run_inference`: run inference or not +- `--db_dir`: path to database, default `{HOME}/public_databases` +- `--model_dir`: Path to ckpt, default `{HOME}/ckpt` +- `--buckets`: setting the sequence length,Default:padding to N * 256 + +### Input & Output + +- **JSON Input**: Contains sequence information of proteins and other molecules. Support the following types of input (same as DeepMind version): Protein, DNA, RNA, Ligand, etc. Currently, only single NPU version and the max sequence length should be smaller than 1000. + +- **CIF Output**: 5 Standard protein structure files and confidence info. + +```txt +└─name_in_your_json + └─ seed-{random_seed}_sample-0 # First Sample + │ confidence.json # Confidence of the first sample + │ model.cif # Predicted structure of the first sample + │ summary_confidence.json # Summary confidence of the first sample + └─ seed-{random_seed}_sample-1 # Second Sample + └─ seed-{random_seed}_sample-2 # Third Sample + └─ seed-{random_seed}_sample-3 # Forth Sample + └─ seed-{random_seed}_sample-4 # Fifth Sample + │ {name}_confidences.json # Confidence of the best sample + │ {name}_data.json # Data json file after data-processing + │ {name}_model.cif # Predicted structure of the best sample + │ {name}_summary_confidence.json # Summary confidence of the best sample + │ ranking_scores.csv # Ranking Score of all five samples, the higher of the ranking score, the higher of the confidence of the sample +``` + +### End of Inference + +When you see the following log,the inference finished correctly: + +```text +=======write output to /PATH/TO/OUTPUT/DIR/name_of_your_input========== +Done processing fold input name_of_your_input. +Done processing 1 fold inputs. +``` + +## License + +See the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- The implementation of Modules including: data,structure,common, constant refers to [DeepMind](https://github.com/google-deepmind/alphafold3). +- The implementation of Modules including: model,utils are based on [MindScience](https://gitee.com/mindspore/mindscience/) + +## 联系我们 + +If you encounter any issues or have any suggestions during use, please contact us through the following methods: + +- **Gitee Repository**: [AlphaFold3](https://gitee.com/mindspore/mindscience/tree/main/MindSPONGE/applications/research/AlphaFold3) +- **Issue Tracking**: [Issue Tracking](https://gitee.com/mindspore/mindscience/issues) + +## Reference + +- Abramson J, Adler J, Dunger J, et al. Accurate structure prediction of biomolecular interactions with AlphaFold 3[J]. Nature, 2024, 630(8016): 493-500. diff --git a/MindSPONGE/applications/research/AlphaFold3/example_input.json b/MindSPONGE/applications/research/AlphaFold3/example_input.json new file mode 100644 index 000000000..2e31369cc --- /dev/null +++ b/MindSPONGE/applications/research/AlphaFold3/example_input.json @@ -0,0 +1,14 @@ +{ + "name": "5tgy", + "sequences": [ + { + "protein": { + "id": "A", + "sequence": "SEFEKLRQTGDELVQAFQRLREIFDKGDDDSLEQVLEEIEELIQKHRQLFDNRQEAADTEAAKQGDQWVQLFQRFREAIDKGDKDSLEQLLEELEQALQKIRELAEKKN" + } + } + ], + "modelSeeds": [1], + "dialect": "alphafold3", + "version": 1 + } \ No newline at end of file diff --git a/MindSPONGE/applications/research/AlphaFold3/image/af3_structure.jpg b/MindSPONGE/applications/research/AlphaFold3/image/af3_structure.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8e383f913b2ed2b3cec8f11c30203e65d8b197b GIT binary patch literal 1669512 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�RbEP}unJ|$36JZgQalMf+!Nn+=UNKG35T4) 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 s{Z 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 D%h}+So3xMJ@XRxOU7*+tE#+`8 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 N3sW4n}nU>2*dN)PbC(5X)AKSeye*t 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 z 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

  • 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�opo(AO>@0QsLf0U~sSN?Q;d&Owe90O|#*R+x?? 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