Source code for pimm.models.polarmae.polarmae

"""
Self-contained PoLAr-MAE for pimm.

Reimplements the PoLAr-MAE self-supervised model (Chamfer reconstruction +
energy infilling) natively within pimm, with no dependency on the external
PoLAr-MAE library.  Transforms (centering, scaling, rotation) are expected
to be applied in the dataset pipeline rather than inside the model.
"""

from __future__ import annotations

from math import sqrt
from typing import Dict, Literal, Optional

import torch
import torch.nn as nn
import torch.nn.functional as F

from pimm.models.builder import MODELS
from pimm.models.losses.chamfer import chamfer_distance
from pimm.models.modules import PointModel
from pimm.models.polarmae.data import packed_to_batched
from pimm.models.polarmae.layers import (
    LearnedPositionalEncoder,
    MaskedMiniPointNet,
    PointcloudGrouping,
    TransformerOutput,
    VariablePointcloudMasking,
    VIT_CONFIGS,
    make_transformer,
)
from pimm.utils.logger import get_logger

logger = get_logger(__name__)

# Default tokenizer configs keyed by (arch, voxel_size)
_SCALE = 768 * sqrt(3) / 2  # ≈ 665.1076

_TOKENIZER_PRESETS = {
    ("vit_small", 5): dict(
        num_groups=2048, context_length=512, group_max_points=32,
        group_radius=5 / _SCALE, group_upscale_points=256, overlap_factor=0.72,
    ),
    ("vit_small", 2.5): dict(
        num_groups=2048, context_length=1024, group_max_points=24,
        group_radius=2.5 / _SCALE, group_upscale_points=64, overlap_factor=0.75,
    ),
    ("vit_small", 25): dict(
        num_groups=256, context_length=128, group_max_points=128,
        group_radius=25 / _SCALE, group_upscale_points=2048, overlap_factor=0.72,
    ),
    ("vit_tiny", 5): dict(
        num_groups=2048, context_length=512, group_max_points=32,
        group_radius=5 / _SCALE, group_upscale_points=256, overlap_factor=0.72,
    ),
    ("vit_tiny", 2.5): dict(
        num_groups=2048, context_length=1024, group_max_points=24,
        group_radius=2.5 / _SCALE, group_upscale_points=64, overlap_factor=0.75,
    ),
    ("vit_base", 5): dict(
        num_groups=2048, context_length=512, group_max_points=32,
        group_radius=5 / _SCALE, group_upscale_points=256, overlap_factor=0.72,
    ),
}


[docs] @MODELS.register_module("PoLAr-MAE") class PoLArMAE(PointModel): """Self-contained PoLAr-MAE for pimm's DefaultTrainer.""" def __init__( self, arch: Literal["vit_tiny", "vit_small", "vit_base"] = "vit_small", num_channels: int = 4, voxel_size: float = 5, masking_ratio: float = 0.6, masking_type: Literal["rand"] = "rand", decoder_arch: Literal["vit_tiny", "vit_small", "vit_base"] = "vit_small", decoder_kwargs: Optional[dict] = None, mae_prediction: Literal["full", "pos"] = "full", loss_weights: Optional[Dict[str, float]] = None, transformer_kwargs: Optional[dict] = None, tokenizer_kwargs: Optional[dict] = None, decoder_use_kv: bool = True, patch_encoder_kwargs: Optional[dict] = None, ): super().__init__() loss_weights = loss_weights or {"chamfer": 1.0, "energy": 1.0} transformer_kwargs = dict(transformer_kwargs or {}) decoder_kwargs = dict(decoder_kwargs or {}) embed_dim = VIT_CONFIGS[arch]["embed_dim"] self.mae_channels = 4 if mae_prediction == "full" else 3 self.loss_weights = loss_weights # --- Tokenizer (grouping + embedding + masking) --- tok_cfg = dict(_TOKENIZER_PRESETS.get((arch, voxel_size), _TOKENIZER_PRESETS[("vit_small", 5)])) if tokenizer_kwargs: tok_cfg.update(tokenizer_kwargs) group_max_points = tok_cfg["group_max_points"] self.grouping = PointcloudGrouping(reduction_method="fps", **tok_cfg) self.embedding = MaskedMiniPointNet(num_channels, embed_dim) self.masking = VariablePointcloudMasking(ratio=masking_ratio) self.pos_embed = LearnedPositionalEncoder(embed_dim) # --- Encoder & Decoder --- self.encoder = make_transformer(arch, use_kv=False, **transformer_kwargs) self.decoder = make_transformer( decoder_arch, use_kv=decoder_use_kv, **decoder_kwargs.get("transformer_kwargs", {}), ) # --- Loss heads --- self.mask_token = nn.Parameter(torch.zeros(embed_dim)) nn.init.trunc_normal_(self.mask_token, std=0.02, a=-0.02, b=0.02) self.increase_dim = nn.Conv1d(embed_dim, self.mae_channels * group_max_points, 1) # Equivariant patch encoder for the energy-infilling head. Defaults match # pimm-native; `patch_encoder_kwargs` (feature_dim/hidden1/hidden2/ # pos_enc_style) lets a config reproduce the original PoLAr-MAE checkpoint. pe_cfg = dict(channels=3, feature_dim=96, hidden1=64, hidden2=64, equivariant=True, pos_enc_style="mlp") if patch_encoder_kwargs: pe_cfg.update(patch_encoder_kwargs) self.equivariant_patch_encoder = MaskedMiniPointNet(**pe_cfg) self.energy_decoder = nn.Conv1d(embed_dim + pe_cfg["feature_dim"], group_max_points, 1) logger.info( f"PoLAr-MAE-Native: arch={arch}, voxel={voxel_size}, " f"mask={masking_ratio}, pred={mae_prediction}, weights={loss_weights}" ) # ---------------------------------------------------------------
[docs] def forward(self, data_dict): feat = data_dict["feat"] # (N_total, C) offset = data_dict["offset"] # (B,) # 1. Packed → padded points, lengths = packed_to_batched(feat, offset) # 2. Grouping g = self.grouping(points, lengths) groups, centers = g["groups"], g["centers"] emb_mask, point_mask = g["embedding_mask"], g["point_mask"] # 3. Masking m_idx, m_mask, um_idx, um_mask = self.masking(emb_mask.sum(-1)) _gather = lambda x, idx: torch.gather( x, 1, idx.unsqueeze(-1).expand(-1, -1, x.shape[2])) _lgather = lambda x, idx: torch.gather( x, 1, idx.unsqueeze(-1).unsqueeze(-1).expand(-1, -1, x.shape[2], x.shape[3])) # Unmasked tokens um_groups = _lgather(groups, um_idx) um_pmask = _gather(point_mask, um_idx) um_emb_mask = um_mask # Embed only valid unmasked tokens with torch.amp.autocast(device_type=feat.device.type, dtype=torch.float32): flat_tok = self.embedding(um_groups[um_emb_mask], um_pmask[um_emb_mask].unsqueeze(1)) tokens = um_groups.new_zeros(um_groups.shape[0], um_groups.shape[1], flat_tok.shape[-1]) tokens[um_emb_mask] = flat_tok um_centers = _gather(centers, um_idx) um_pos = self.pos_embed(um_centers) # Masked metadata m_centers = _gather(centers, m_idx) m_pos = self.pos_embed(m_centers) m_groups = _lgather(groups, m_idx) m_pmask = _gather(point_mask, m_idx) * m_mask.unsqueeze(-1) # 4. Encode unmasked tokens enc_out = self.encoder(tokens, um_pos, um_mask).last_hidden_state # 5. Decode masked tokens tok_m = self.mask_token.expand(m_mask.shape[0], m_mask.shape[1], -1) dec_out = self.decoder(tok_m, m_pos, m_mask, kv=enc_out, pos_kv=um_pos, kv_mask=um_mask).last_hidden_state # Flatten to valid masked tokens only masked_output = dec_out[m_mask] masked_groups = m_groups[m_mask] flat_pmask = m_pmask[m_mask] point_lengths = flat_pmask.sum(-1) # 6. Chamfer loss with torch.amp.autocast(device_type=feat.device.type, dtype=torch.float32): up = self.increase_dim(masked_output.unsqueeze(-1)).squeeze(-1) up = up.reshape(up.shape[0], -1, self.mae_channels) chamfer_loss, _, _ = chamfer_distance( up.float(), masked_groups[..., :self.mae_channels].float(), x_lengths=point_lengths, y_lengths=point_lengths, ) # 7. Energy loss pmask_1 = flat_pmask.unsqueeze(1) with torch.amp.autocast(device_type=feat.device.type, dtype=torch.float32): eq_enc = self.equivariant_patch_encoder(masked_groups[..., :3], pmask_1) dec_inp = torch.cat([eq_enc, masked_output], dim=1) energy_pred = self.energy_decoder(dec_inp.unsqueeze(-1)).squeeze(-1) energy_loss = F.mse_loss( energy_pred[flat_pmask].float(), masked_groups[flat_pmask][..., -1].float(), ) loss = sum(self.loss_weights.get(k, 1.0) * v for k, v in [("chamfer", chamfer_loss), ("energy", energy_loss)]) return { "loss": loss, "chamfer_loss": chamfer_loss, "energy_loss": energy_loss, "mean_points": lengths.float().mean(), "mean_groups": emb_mask.sum(-1).float().mean(), }