Source code for aidsorb.modules.voxels

# This file is part of AIdsorb.
# Copyright (C) 2026 Antonios P. Sarikas

# AIdsorb is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

r"""
:class:`torch.nn.Module`'s for voxels processing.

References
----------

.. [IntelliPore] A. P. Sarikas, K. Gkagkas, and G. E. Froudakis, β€œIntelliPore: A
                 Foundation Model for Gas Adsorption in Porous Materials"
                 ADD IT IN IEEE FORMAT
.. [RetNeXt] Sarikas, A. P., Gkagkas, K., & Froudakis, G. E. (2026). RetNeXt: A
             Pretrained Model for Transfer Learning Across the MOF Adsorption Space. Journal
             of Chemical Information and Modeling, 66(4), 2110–2116.
             https://doi.org/10.1021/acs.jcim.5c02698 
"""

from collections import OrderedDict

import torch
from torch import nn, Tensor

from .._torch_utils import get_activation


[docs] def conv3d_block( in_channels: int, out_channels: int, config_activation: dict[str, str | dict] | None = None, **kwargs ): r""" Return a 3D convolutional block. The block has the following form:: block = nn.Sequential( conv_layer, nn.BatchNorm3d(out_channels), activation_fn ) Parameters ---------- in_channels : int out_channels : int config_activation : dict, default=None Dictionary for configuring activation function. If :obj:`None`, the :class:`~torch.nn.modules.activation.ReLU` activation is used. * ``'name'`` activation's class name :class:`str` * ``'hparams'`` activation's hyperparameters :class:`dict` **kwargs Valid keyword arguments for :class:`~torch.nn.Conv3d`. Returns ------- block : torch.nn.Sequential Examples -------- >>> inp, out = 4, 8 >>> x = torch.randn(2, inp, 3, 3, 3) # Shape (B, in_channels, H, W, D). >>> config_afn = {'name': 'LeakyReLU', 'hparams': {'negative_slope': 0.9}} >>> # Default activation function (ReLU). >>> block = conv3d_block(inp, out, kernel_size=3) >>> block(x).shape torch.Size([2, 8, 1, 1, 1]) >>> block[2] ReLU() >>> # Custom activation function. >>> block = conv3d_block(inp, out, config_afn, kernel_size=3) >>> block(x).shape torch.Size([2, 8, 1, 1, 1]) >>> block[2] LeakyReLU(negative_slope=0.9) """ return nn.Sequential( nn.Conv3d(in_channels, out_channels, **kwargs), nn.BatchNorm3d(out_channels), get_activation(config_activation), )
[docs] class RetNeXt(nn.Module): r""" Architecture from the [RetNeXt]_ paper. .. note:: ``pretrained=True`` is only compatible with ``in_channels=1``, since the pretrained backbone was trained on single-channel images. Parameters ---------- in_channels : int, default=1 n_outputs : int or None, default=1 Number of output units. If ``None``, no linear head is added and the model acts as feature extractor. pretrained : bool, default=False Whether to use pretrained weights for the backbone. Examples -------- >>> model = RetNeXt(3, 100) >>> x = torch.randn(8, 3, 32, 32, 32) >>> model(x).shape # Outputs torch.Size([8, 100]) >>> model.backbone(x).shape # Embeddings torch.Size([8, 128]) >>> # Works with different grid sizes (adaptive pooling). >>> x = torch.randn(16, 3, 25, 25, 25) >>> model(x).shape torch.Size([16, 100]) >>> # Acts as feature extractor. >>> model = IntelliPore(in_channels=3, n_outputs=None) >>> x = torch.randn(4, 3, 32, 32, 32) >>> model(x).shape torch.Size([4, 128]) >>> # Pretrained weights for the backbone. >>> model = RetNeXt(pretrained=True) # doctest: +SKIP """ def __init__( self, in_channels: int = 1, n_outputs: int | None = 1, *, pretrained: bool = False, ): super().__init__() self.backbone = nn.Sequential( nn.BatchNorm3d(in_channels, affine=False, momentum=None), conv3d_block(in_channels, 32, kernel_size=3, bias=False, padding='same'), conv3d_block(32, 32, kernel_size=3, bias=False, padding='same'), nn.MaxPool3d(kernel_size=2), # 1st pooling layer conv3d_block(32, 64, kernel_size=3, bias=False, padding='same'), conv3d_block(64, 64, kernel_size=3, bias=False, padding='same'), nn.MaxPool3d(kernel_size=2), # 2nd pooling layer conv3d_block(64, 128, kernel_size=3, bias=False), conv3d_block(128, 128, kernel_size=3, bias=False), nn.AdaptiveAvgPool3d(1), nn.Flatten(), ) if pretrained: self.backbone.load_state_dict(self.get_pretrained_weights()) self.head = torch.nn.Identity() if n_outputs is None else nn.Linear(128, n_outputs)
[docs] def forward(self, x: Tensor) -> Tensor: r""" Run the forward pass. Parameters ---------- x : tensor of shape (B, C, H, W, D) Returns ------- out : tensor If ``n_outputs=None`` return the embeddings of shape ``(B, 128)``, else the model outputs of shape ``(B, n_outputs)``. """ return self.head(self.backbone(x))
[docs] def get_pretrained_weights(self) -> OrderedDict: r""" Return the state dict of the pretrained backbone. """ url = 'https://raw.githubusercontent.com/adosar/retnext-paper/master/pretrained_weights/retnext_cubic_boltzmann_final_all.pt' return torch.hub.load_state_dict_from_url(url)
[docs] class IntelliPore(nn.Module): r""" Architecture from the [IntelliPore]_ paper. .. note:: ``pretrained=True`` is only compatible with ``in_channels=1``, since the pretrained backbone was trained on single-channel images. Parameters ---------- in_channels : int, default=1 n_outputs : int or None, default=1 Number of output units. If ``None``, no linear head is added and the model acts as feature extractor. pretrained : bool, default=False Whether to use pretrained weights for the backbone. Examples -------- >>> model = IntelliPore(3, 100) >>> x = torch.randn(4, 3, 32, 32, 32) >>> model(x).shape # Outputs torch.Size([4, 100]) >>> model.backbone(x).shape # Embeddings torch.Size([4, 128]) >>> # Works with different grid sizes (adaptive pooling). >>> x = torch.randn(8, 3, 24, 24, 24) >>> model(x).shape torch.Size([8, 100]) >>> # Acts as feature extractor. >>> model = IntelliPore(in_channels=3, n_outputs=None) >>> x = torch.randn(4, 3, 32, 32, 32) >>> model(x).shape torch.Size([4, 128]) >>> # Pretrained weights for the backbone. >>> model = IntelliPore(pretrained=True) # doctest: +SKIP """ def __init__( self, in_channels: int = 1, n_outputs: int | None = 1, *, pretrained: bool = False, ): super().__init__() self.backbone = nn.Sequential( conv3d_block(in_channels, 32, kernel_size=3, padding='same'), conv3d_block(32, 32, kernel_size=3, padding='same'), nn.MaxPool3d(kernel_size=2), # 1st pooling layer conv3d_block(32, 64, kernel_size=3, padding='same'), conv3d_block(64, 64, kernel_size=3, padding='same'), nn.MaxPool3d(kernel_size=2), # 2nd pooling layer conv3d_block(64, 128, kernel_size=3), conv3d_block(128, 128, kernel_size=3), nn.AdaptiveAvgPool3d(1), nn.Flatten() ) if pretrained: self.backbone.load_state_dict(self.get_pretrained_weights()) self.head = torch.nn.Identity() if n_outputs is None else nn.Linear(128, n_outputs)
[docs] def forward(self, x: Tensor) -> Tensor: r""" Run the forward pass. Parameters ---------- x : tensor of shape (B, C, H, W, D) Returns ------- out : tensor If ``n_outputs=None`` return the embeddings of shape ``(B, 128)``, else the model outputs of shape ``(B, n_outputs)``. """ return self.head(self.backbone(x))
[docs] def get_pretrained_weights(self) -> OrderedDict: r""" Return the state dict of the pretrained backbone. """ #url = 'https://raw.githubusercontent.com/adosar/intellipore/master/pretrained_weights/intellipore_backbone_pretrained.pt' # Temporary needs to be changed url = 'https://raw.githubusercontent.com/adosar/trial/master/pretrained_weights/intellipore_backbone_pretrained.pt' return torch.hub.load_state_dict_from_url(url)