Source code for protomotions.agents.amp.model

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""AMP model components including discriminator network.

This module implements the AMP-specific neural networks, particularly the
discriminator that distinguishes between agent and reference motion data.

Key Classes:
    - Discriminator: Binary classifier for agent vs. reference motions
    - AMPModel: PPO model extended with discriminator
"""

import torch
from torch import nn
from typing import List
from tensordict import TensorDict
from protomotions.utils.hydra_replacement import get_class
from protomotions.agents.ppo.model import PPOModel
from protomotions.agents.amp.config import DiscriminatorConfig, AMPModelConfig
from protomotions.agents.common.common import ModuleContainer, ObsProcessor
from protomotions.agents.common.mlp import MLPWithConcat
from protomotions.agents.base_agent.model import ProtoMotionsTensorDictModule


[docs] class Discriminator(ProtoMotionsTensorDictModule): """Discriminator network for AMP style rewards. Binary classifier that distinguishes between agent-generated and reference motion data. Uses ModuleContainer structure - just chains models together. Args: config: DiscriminatorConfig (extends ModuleContainerConfig). Attributes: models: ModuleContainer list of modules. in_keys: Input keys from config. out_keys: Output keys from config. """ config: DiscriminatorConfig
[docs] def __init__(self, config: DiscriminatorConfig): super().__init__() self.config = config # Build sequential modules models = [] for input_model in config.models: model = get_class(input_model._target_)(config=input_model) models.append(model) self.models = nn.ModuleList(models) # Set TensorDict keys from config self.in_keys = self.config.in_keys self.out_keys = self.config.out_keys # Discover gradient penalty targets once at init self._grad_penalty_keys = self._find_grad_penalty_keys() if not self._grad_penalty_keys: self._grad_penalty_keys = list(self.config.in_keys)
def _find_grad_penalty_keys(self) -> List[str]: """Discover tensordict keys to use as gradient penalty targets. Returns keys for the effective inputs to the discriminator's learned function — after preprocessing but before learned layers: - ObsProcessor outputs (normalizing or not — they preprocess raw obs) - MLPWithConcat internal norm_ keys (when normalize_obs=True) - Raw pass-through keys that bypass all preprocessing Some keys (e.g. masks) may not be in the autograd graph — the caller handles this via allow_unused=True and filters None gradients. """ transformed_keys = [] consumed_raw_keys: set = set() for model in self.models: cfg = getattr(model, "config", None) if cfg is None: continue if isinstance(model, ObsProcessor): consumed_raw_keys.update(model.in_keys) transformed_keys.extend(model.out_keys) elif isinstance(model, MLPWithConcat) and getattr( cfg, "normalize_obs", False ): consumed_raw_keys.update(model.in_keys) transformed_keys.append(f"norm_{model.config.in_keys[0]}") passthrough_keys = [ k for k in self.config.in_keys if k not in consumed_raw_keys ] return transformed_keys + passthrough_keys
[docs] def forward( self, tensordict: TensorDict, log_internals: bool = False, ) -> TensorDict: """Forward pass through discriminator. Args: tensordict: TensorDict containing observations. Returns: TensorDict with discriminator output added. """ # Chain through all modules for model in self.models: if isinstance(model, ProtoMotionsTensorDictModule): tensordict = model(tensordict, log_internals=log_internals) else: tensordict = model(tensordict) return tensordict
[docs] def compute_disc_reward( self, disc_logits: torch.Tensor, eps: float = 1e-4 ) -> torch.Tensor: """Compute style reward from discriminator logits. Converts discriminator logits to reward using negative log probability. Higher reward means motion is more similar to reference data. Args: disc_logits: Discriminator logits. eps: Small constant for numerical stability. Returns: Style rewards for each sample (higher = more reference-like). """ prob = 1 / (1 + torch.exp(-disc_logits)) reward = -torch.log(torch.clamp(1 - prob, min=eps)) return reward
[docs] def all_discriminator_weights(self): """Get all discriminator weight matrices (works with LazyLinear). Returns: List of weight parameters from all linear layers in discriminator. """ weights: List[nn.Parameter] = [] for mod in self.modules(): if hasattr(mod, "weight") and isinstance(mod.weight, nn.Parameter): weights.append(mod.weight) return weights
[docs] def logit_weights(self) -> List[nn.Parameter]: """Get the final layer weights (logit layer). Returns: List containing the output layer weight parameter. """ last_module = self.models[-1] if hasattr(last_module, "mlp"): last_module = last_module.mlp[-1] return [last_module.weight]
[docs] class AMPModelComponentsMixin: """Adds AMP discriminator modules to a host model.""" def _build_amp_model_components(self, config): DiscriminatorClass = get_class(config.discriminator._target_) self._discriminator: Discriminator = DiscriminatorClass( config=config.discriminator ) DiscCriticClass = get_class(config.disc_critic._target_) self._disc_critic: ModuleContainer = DiscCriticClass( config=config.disc_critic ) self._validate_amp_model_keys(config) def _validate_amp_model_keys(self, config): discriminator_in_keys = list( set(self._discriminator.in_keys + self._disc_critic.in_keys) ) discriminator_out_keys = list( set(self._discriminator.out_keys + self._disc_critic.out_keys) ) for key in discriminator_out_keys: assert ( key in config.out_keys ), f"Discriminator output key {key} not in out_keys {config.out_keys}" for key in discriminator_in_keys: assert ( key in config.in_keys ), f"Discriminator input key {key} not in in_keys {config.in_keys}" def _forward_amp_model_components( self, tensordict: TensorDict, log_internals: bool = False, ) -> TensorDict: tensordict = self._discriminator(tensordict, log_internals=log_internals) tensordict = self._disc_critic(tensordict, log_internals=log_internals) return tensordict
[docs] class AMPModel(AMPModelComponentsMixin, PPOModel): """AMP model with actor, task critic, disc critic, and discriminator networks. Extends PPOModel by adding a discriminator network that provides style rewards and a separate critic for estimating discriminator reward values. Args: config: AMPModelConfig specifying all networks. Attributes: _actor: Policy network. _critic: Task value network. _disc_critic: Discriminator reward value network. _discriminator: Style discriminator network. """ config: AMPModelConfig
[docs] def __init__(self, config: AMPModelConfig): super().__init__(config) self._build_amp_model_components(self.config)
[docs] def forward( self, tensordict: TensorDict, log_internals: bool = False, ) -> TensorDict: """Forward pass through PPO, and discriminator. Args: tensordict: TensorDict containing observations. Returns: TensorDict with all model outputs added. """ tensordict = super().forward(tensordict, log_internals=log_internals) return self._forward_amp_model_components( tensordict, log_internals=log_internals )