Custom Attributes#

Newton’s simulation model uses flat buffer arrays to represent physical properties and simulation state. These arrays can be extended with user-defined custom attributes to store application-specific data alongside the standard physics quantities.

Use Cases#

Custom attributes enable a wide range of simulation extensions:

  • Per-body properties: Store thermal properties, material composition, sensor IDs, or hardware specifications

  • Advanced control: Store PD gains, velocity limits, control modes, or actuator parameters per-joint or per-DOF

  • Visualization: Attach colors, labels, rendering properties, or UI metadata to simulation entities

  • Multi-physics coupling: Store quantities like surface stress, temperature fields, or electromagnetic properties

  • Reinforcement learning: Store observation buffers, reward weights, optimization parameters, or policy-specific data directly on entities

  • Solver-specific data: Store contact pair parameters, tendon properties, or other solver-specific entity types

Custom attributes follow Newton’s flat array indexing scheme, enabling efficient GPU-parallel access while maintaining flexibility for domain-specific extensions.

Overview#

Newton organizes simulation data into four primary objects, each containing flat arrays indexed by simulation entities:

  • Model Object (Model) - Static configuration and physical properties that remain constant during simulation

  • State Object (State) - Dynamic quantities that evolve during simulation

  • Control Object (Control) - Control inputs and actuator commands

  • Contact Object (Contacts) - Contact-specific properties

Custom attributes extend these objects with user-defined arrays that follow the same indexing scheme as Newton’s built-in attributes. The CONTACT assignment attaches attributes to the Contacts object created during collision detection.

Declaring Custom Attributes#

Custom attributes must be declared before use via the newton.ModelBuilder.add_custom_attribute() method. Each declaration specifies:

  • name: Attribute name

  • frequency: Determines array size and indexing—either a AttributeFrequency enum value (e.g., BODY, SHAPE, JOINT, JOINT_DOF, JOINT_COORD, ARTICULATION, ONCE) or a string for custom frequencies

  • dtype: Warp data type (wp.float32, wp.vec3, wp.quat, etc.) or str for string attributes stored as Python lists

  • assignment: Which simulation object owns the attribute (MODEL, STATE, CONTROL, CONTACT)

  • default (optional): Default value for unspecified entities. When omitted, a sensible zero-value is derived from the dtype (0 for scalars, identity for quaternions, False for booleans, "" for strings)

  • namespace (optional): Hierarchical organization for grouping related attributes

  • references (optional): For multi-world merging, specifies how values are transformed (e.g., "body", "shape", "world", or a custom frequency key)

  • values (optional): Pre-populated values — dict[int, Any] for enum frequencies or list[Any] for custom string frequencies

When no namespace is specified, attributes are added directly to their assignment object (e.g., model.temperature). When a namespace is provided, Newton creates a namespace container (e.g., model.mujoco.damping).

from newton import Model, ModelBuilder
import warp as wp

builder = ModelBuilder()

# Default namespace attributes - added directly to assignment objects
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="temperature",
        frequency=Model.AttributeFrequency.BODY,
        dtype=wp.float32,
        default=20.0,  # Explicit default value
        assignment=Model.AttributeAssignment.MODEL
    )
)
# → Accessible as: model.temperature

builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="velocity_limit",
        frequency=Model.AttributeFrequency.BODY,
        dtype=wp.vec3,
        default=(1.0, 1.0, 1.0),  # Default vector value
        assignment=Model.AttributeAssignment.STATE
    )
)
# → Accessible as: state.velocity_limit

# Namespaced attributes - organized under namespace containers
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="float_attr",
        frequency=Model.AttributeFrequency.BODY,
        dtype=wp.float32,
        default=0.5,
        assignment=Model.AttributeAssignment.MODEL,
        namespace="namespace_a"
    )
)
# → Accessible as: model.namespace_a.float_attr

builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="bool_attr",
        frequency=Model.AttributeFrequency.SHAPE,
        dtype=wp.bool,
        default=False,
        assignment=Model.AttributeAssignment.MODEL,
        namespace="namespace_a"
    )
)
# → Accessible as: model.namespace_a.bool_attr

# Articulation frequency attributes - one value per articulation
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="articulation_stiffness",
        frequency=Model.AttributeFrequency.ARTICULATION,
        dtype=wp.float32,
        default=100.0,
        assignment=Model.AttributeAssignment.MODEL
    )
)
# → Accessible as: model.articulation_stiffness

# ONCE frequency attributes - a single global value
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="gravity_scale",
        frequency=Model.AttributeFrequency.ONCE,
        dtype=wp.float32,
        default=1.0,
        assignment=Model.AttributeAssignment.MODEL
    )
)
# → Accessible as: model.gravity_scale (array of length 1)

# String dtype attributes - stored as Python lists, not Warp arrays
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="body_description",
        frequency=Model.AttributeFrequency.BODY,
        dtype=str,
        default="unnamed",
        assignment=Model.AttributeAssignment.MODEL
    )
)
# → Accessible as: model.body_description (Python list[str])

Default Value Behavior:

When entities don’t explicitly specify custom attribute values, the default value is used:

# First body uses the default value (20.0)
body1 = builder.add_body(mass=1.0)

# Second body overrides with explicit value
body2 = builder.add_body(
    mass=1.0,
    custom_attributes={"temperature": 37.5}
)

# Articulation attributes: create articulations with custom values
# Each add_articulation creates one articulation at the next index
for i in range(3):
    base = builder.add_link(mass=1.0)
    joint = builder.add_joint_free(child=base)
    builder.add_articulation(
        joints=[joint],
        custom_attributes={
            "articulation_stiffness": 100.0 + float(i) * 50.0  # 100, 150, 200
        }
    )

# After finalization, access attributes
model = builder.finalize()
temps = model.temperature.numpy()
arctic_stiff = model.articulation_stiffness.numpy()

print(f"Body 1: {temps[body1]}")  # 20.0 (default)
print(f"Body 2: {temps[body2]}")  # 37.5 (authored)
# Articulation indices reflect all articulations in the model
# (including any implicit ones from add_body)
print(f"Articulations: {len(arctic_stiff)}")
print(f"Last articulation stiffness: {arctic_stiff[-1]}")  # 200.0
Body 1: 20.0
Body 2: 37.5
Articulations: 5
Last articulation stiffness: 200.0

Note

Uniqueness is determined by the full identifier (namespace + name):

  • model.float_attr (key: "float_attr") and model.namespace_a.float_attr (key: "namespace_a:float_attr") can coexist

  • model.float_attr (key: "float_attr") and state.namespace_a.float_attr (key: "namespace_a:float_attr") can coexist

  • model.float_attr (key: "float_attr") and state.float_attr (key: "float_attr") cannot coexist - same key

  • model.namespace_a.float_attr and state.namespace_a.float_attr cannot coexist - same key "namespace_a:float_attr"

Registering Solver Attributes:

Before loading assets, register solver-specific attributes:

from newton import ModelBuilder
from newton.solvers import SolverMuJoCo

builder_mujoco = ModelBuilder()
SolverMuJoCo.register_custom_attributes(builder_mujoco)

# Now build your scene...
body = builder_mujoco.add_link()
joint = builder_mujoco.add_joint_free(body)
builder_mujoco.add_articulation([joint])
shape = builder_mujoco.add_shape_box(body=body, hx=0.1, hy=0.1, hz=0.1)

model_mujoco = builder_mujoco.finalize()
assert hasattr(model_mujoco, "mujoco")
assert hasattr(model_mujoco.mujoco, "condim")

MuJoCo boolean custom attributes use a parse_bool transformer (registered by register_custom_attributes()) that handles strings ("true"/"false"), integers, and native booleans.

Authoring Custom Attributes#

After declaration, values are assigned through the standard entity creation API (add_body, add_shape, add_joint). For default namespace attributes, use the attribute name directly. For namespaced attributes, use the format "namespace:attr_name".

# Create a body with both default and namespaced attributes
body_id = builder.add_body(
    mass=1.0,
    custom_attributes={
        "temperature": 37.5,                  # default → model.temperature
        "velocity_limit": [2.0, 2.0, 2.0],    # default → state.velocity_limit
        "namespace_a:float_attr": 0.5,        # namespaced → model.namespace_a.float_attr
    }
)

# Create a shape with a namespaced attribute
shape_id = builder.add_shape_box(
    body=body_id,
    hx=0.1, hy=0.1, hz=0.1,
    custom_attributes={
        "namespace_a:bool_attr": True,  # → model.namespace_a.bool_attr
    }
)

Joint Frequency Types:

For joints, Newton provides three frequency types to store different granularities of data:

  • JOINT frequency → One value per joint

  • JOINT_DOF frequency → Values per degree of freedom (list, dict, or scalar for single-DOF joints)

  • JOINT_COORD frequency → Values per position coordinate (list, dict, or scalar for single-coordinate joints)

For JOINT_DOF and JOINT_COORD frequencies, values can be provided in three formats:

  1. List format: Explicit values for all DOFs/coordinates (e.g., [100.0, 200.0] for 2-DOF joint)

  2. Dict format: Sparse specification mapping indices to values (e.g., {0: 100.0, 2: 300.0} sets only DOF 0 and 2)

  3. Scalar format: Single value for single-DOF/single-coordinate joints, automatically expanded to a list

# Declare joint attributes with different frequencies
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="int_attr",
        frequency=Model.AttributeFrequency.JOINT,
        dtype=wp.int32
    )
)
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="float_attr_dof",
        frequency=Model.AttributeFrequency.JOINT_DOF,
        dtype=wp.float32
    )
)
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="float_attr_coord",
        frequency=Model.AttributeFrequency.JOINT_COORD,
        dtype=wp.float32
    )
)

# Create a D6 joint with 2 DOFs (1 linear + 1 angular) and 2 coordinates
parent = builder.add_link(mass=1.0)
child = builder.add_link(mass=1.0)

cfg = ModelBuilder.JointDofConfig
joint_id = builder.add_joint_d6(
    parent=parent,
    child=child,
    linear_axes=[cfg(axis=[1, 0, 0])],      # 1 linear DOF
    angular_axes=[cfg(axis=[0, 0, 1])],     # 1 angular DOF
    custom_attributes={
        "int_attr": 5,                      # JOINT frequency: single value
        "float_attr_dof": [100.0, 200.0],   # JOINT_DOF frequency: list with 2 values (one per DOF)
        "float_attr_coord": [0.5, 0.7],     # JOINT_COORD frequency: list with 2 values (one per coordinate)
    }
)
builder.add_articulation([joint_id])

# Scalar format for single-DOF joints (automatically expanded to list)
parent2 = builder.add_link(mass=1.0)
child2 = builder.add_link(mass=1.0)
revolute_joint = builder.add_joint_revolute(
    parent=parent2,
    child=child2,
    axis=[0, 0, 1],
    custom_attributes={
        "float_attr_dof": 150.0,    # Scalar for 1-DOF joint (expanded to [150.0])
        "float_attr_coord": 0.8,    # Scalar for 1-coord joint (expanded to [0.8])
    }
)
builder.add_articulation([revolute_joint])

# Dict format for sparse specification (only set specific DOF/coord indices)
parent3 = builder.add_link(mass=1.0)
child3 = builder.add_link(mass=1.0)
d6_joint = builder.add_joint_d6(
    parent=parent3,
    child=child3,
    linear_axes=[cfg(axis=[1, 0, 0]), cfg(axis=[0, 1, 0])],  # 2 linear DOFs
    angular_axes=[cfg(axis=[0, 0, 1])],                      # 1 angular DOF
    custom_attributes={
        "float_attr_dof": {0: 100.0, 2: 300.0},  # Dict: only DOF 0 and 2 specified
    }
)
builder.add_articulation([d6_joint])

Accessing Custom Attributes#

After finalization, custom attributes become accessible as Warp arrays. Default namespace attributes are accessed directly on their assignment object, while namespaced attributes are accessed through their namespace container.

# Finalize the model
model = builder.finalize()
state = model.state()

# Access default namespace attributes (direct access on assignment objects)
temperatures = model.temperature.numpy()
velocity_limits = state.velocity_limit.numpy()

print(f"Temperature: {temperatures[body_id]}")
print(f"Velocity limit: {velocity_limits[body_id]}")

# Access namespaced attributes (via namespace containers)
namespace_a_body_floats = model.namespace_a.float_attr.numpy()
namespace_a_shape_bools = model.namespace_a.bool_attr.numpy()

print(f"Namespace A body float: {namespace_a_body_floats[body_id]}")
print(f"Namespace A shape bool: {bool(namespace_a_shape_bools[shape_id])}")
Temperature: 37.5
Velocity limit: [2. 2. 2.]
Namespace A body float: 0.5
Namespace A shape bool: True

Custom attributes follow the same GPU/CPU synchronization rules as built-in attributes and can be modified during simulation.

USD Integration#

Custom attributes can be authored in USD files using a declaration-first pattern, similar to the Python API. Declarations are placed on the PhysicsScene prim, and individual prims can then assign values to these attributes.

Declaration Format (on PhysicsScene prim):

def PhysicsScene "physicsScene" {
    # Default namespace attributes
    custom float newton:float_attr = 0.0 (
        customData = {
            string assignment = "model"
            string frequency = "body"
        }
    )
    custom float3 newton:vec3_attr = (0.0, 0.0, 0.0) (
        customData = {
            string assignment = "state"
            string frequency = "body"
        }
    )

    # ARTICULATION frequency attribute
    custom float newton:articulation_stiffness = 100.0 (
        customData = {
            string assignment = "model"
            string frequency = "articulation"
        }
    )

    # Custom namespace attributes
    custom float newton:namespace_a:some_attrib = 150.0 (
        customData = {
            string assignment = "control"
            string frequency = "joint_dof"
        }
    )
    custom bool newton:namespace_a:bool_attr = false (
        customData = {
            string assignment = "model"
            string frequency = "shape"
        }
    )
}

Assignment Format (on individual prims):

def Xform "robot_arm" (
    prepend apiSchemas = ["PhysicsRigidBodyAPI"]
) {
    # Override declared attributes with custom values
    custom float newton:float_attr = 850.0
    custom float3 newton:vec3_attr = (1.0, 0.5, 0.3)
    custom float newton:namespace_a:some_attrib = 250.0
}

def Mesh "gripper" (
    prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI"]
) {
    custom bool newton:namespace_a:bool_attr = true
}

After importing the USD file, attributes are accessible following the same patterns as programmatically declared attributes:

from newton import ModelBuilder

builder_usd = ModelBuilder()
builder_usd.add_usd("robot_arm.usda")

model = builder_usd.finalize()
state = model.state()
control = model.control()

# Access default namespace attributes
float_values = model.float_attr.numpy()
vec3_values = state.vec3_attr.numpy()

# Access namespaced attributes
namespace_a_floats = control.namespace_a.some_attrib.numpy()
namespace_a_bools = model.namespace_a.bool_attr.numpy()

For more information about USD integration and the schema resolver system, see USD Parsing and Schema Resolver System.

MJCF and URDF Integration#

Custom attributes can also be parsed from MJCF and URDF files. Each CustomAttribute has optional fields for controlling how values are extracted from these formats:

  • mjcf_attribute_name — name of the XML attribute to read (defaults to the attribute name)

  • mjcf_value_transformer — callable that converts the XML string value to the target dtype

  • urdf_attribute_name — name of the XML attribute to read (defaults to the attribute name)

  • urdf_value_transformer — callable that converts the XML string value to the target dtype

These are primarily used by solver integrations (e.g., register_custom_attributes() registers MJCF transformers for MuJoCo-specific attributes like condim, priority, and solref). When no transformer is provided, values are parsed using a generic string-to-Warp converter.

# Example: register an attribute that reads "damping" from MJCF joint elements
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="custom_damping",
        frequency=Model.AttributeFrequency.JOINT_DOF,
        dtype=wp.float32,
        default=0.0,
        namespace="myns",
        mjcf_attribute_name="damping",  # reads <joint damping="..."/>
    )
)

Validation and Constraints#

The custom attribute system enforces several constraints to ensure correctness:

  • Attributes must be declared via add_custom_attribute() before use (raises AttributeError otherwise)

  • Each attribute must be used with entities matching its declared frequency (raises ValueError otherwise)

  • Each full attribute identifier (namespace + name) can only be declared once with a specific assignment, frequency, and dtype

  • The same attribute name can exist in different namespaces because they create different full identifiers

Custom Frequencies#

While enum frequencies (BODY, SHAPE, JOINT, etc.) cover most use cases, some data structures have counts independent of built-in entity types. Custom frequencies address this by allowing a string instead of an enum for the frequency parameter.

Example use case: MuJoCo’s <contact><pair> elements define contact pairs between geometries. These pairs have their own count independent of bodies or shapes, and their indices must be remapped when merging worlds.

Registering Custom Frequencies#

Custom frequencies must be registered before use via add_custom_frequency() using a CustomFrequency object. This explicit registration ensures clarity about which entity types exist and enables optional USD parsing support.

# Register a custom frequency
builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(
        name="item",
        namespace="myns",
    )
)

The frequency key follows the same namespace rules as attribute keys: if a namespace is provided, it is prepended to the name (e.g., "mujoco:pair"). When declaring a custom attribute, the frequency string must match this full key.

Declaring Custom Frequency Attributes#

Once a custom frequency is registered, pass a string instead of an enum for the frequency parameter when adding attributes:

# First register the custom frequency
builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(name="pair", namespace="mujoco")
)

# Then add attributes using that frequency
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="pair_geom1",
        frequency="mujoco:pair",  # Custom frequency (string)
        dtype=wp.int32,
        namespace="mujoco",
    )
)

Note

Attempting to add an attribute with an unregistered custom frequency will raise a ValueError.

Adding Values#

Custom frequency values are appended using add_custom_values():

# Declare attributes sharing the "myns:item" frequency
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(name="item_id", frequency="myns:item", dtype=wp.int32, namespace="myns")
)
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(name="item_value", frequency="myns:item", dtype=wp.float32, namespace="myns")
)

# Append values together
builder.add_custom_values(**{
    "myns:item_id": 100,
    "myns:item_value": 2.5,
})
builder.add_custom_values(**{
    "myns:item_id": 101,
    "myns:item_value": 3.0,
})

# Finalize (requires at least one articulation)
_body = builder.add_link()
_joint = builder.add_joint_free(_body)
builder.add_articulation([_joint])
model = builder.finalize()

print(model.myns.item_id.numpy())
print(model.myns.item_value.numpy())
[100 101]
[2.5 3. ]

For convenience, add_custom_values_batch() appends multiple rows in a single call:

builder.add_custom_values_batch([
    {"myns:item_id": 100, "myns:item_value": 2.5},
    {"myns:item_id": 101, "myns:item_value": 3.0},
])

Here, a row is one indexed element of a custom frequency: the values at that index across every attribute using the frequency describe the same entity.

Validation: All attributes sharing a custom frequency must have the same count at finalize() time. This catches synchronization bugs early.

Articulation-Scoped Custom Frequencies#

A custom frequency can declare that each of its rows belongs to one articulation. This lets ArticulationView expose every model, state, or control attribute using that frequency without adding frequency-specific logic to the view.

Set articulation_owner_attribute to the full key <frequency>_articulation of an integer owner attribute. The owner attribute must use the same custom frequency, be assigned to Model, and declare references="articulation". An optional label_attribute identifies a string attribute named <frequency>_label containing row labels. Builder merging and replication apply their label prefixes to this declared label attribute.

When owners can be derived from other model data, set articulation_owner_resolver instead of populating the owner attribute at every import or construction site. The resolver receives the completed builder and returns one articulation index per frequency row. It runs before merge and finalization; its results are stored in the declared owner attribute and remapped like ordinary articulation references.

import newton
import warp as wp

builder = newton.ModelBuilder()
body = builder.add_link(mass=1.0)
joint = builder.add_joint_revolute(parent=-1, child=body)
builder.add_articulation([joint], label="robot")

def resolve_sensor_owners(builder):
    sensor_joints = builder.custom_attributes["demo:sensor_joint"].values
    return [builder.joint_articulation[joint] for joint in sensor_joints]

builder.add_custom_frequency(
    newton.ModelBuilder.CustomFrequency(
        name="sensor",
        namespace="demo",
        articulation_owner_attribute="demo:sensor_articulation",
        articulation_owner_resolver=resolve_sensor_owners,
        label_attribute="demo:sensor_label",
    )
)
builder.add_custom_attribute(
    newton.ModelBuilder.CustomAttribute(
        name="sensor_articulation",
        namespace="demo",
        frequency="demo:sensor",
        dtype=wp.int32,
        references="articulation",
    )
)
builder.add_custom_attribute(
    newton.ModelBuilder.CustomAttribute(
        name="sensor_joint",
        namespace="demo",
        frequency="demo:sensor",
        dtype=wp.int32,
        references="joint",
    )
)
builder.add_custom_attribute(
    newton.ModelBuilder.CustomAttribute(
        name="sensor_label",
        namespace="demo",
        frequency="demo:sensor",
        dtype=str,
    )
)
builder.add_custom_attribute(
    newton.ModelBuilder.CustomAttribute(
        name="sensor_value",
        namespace="demo",
        frequency="demo:sensor",
        dtype=wp.float32,
    )
)
builder.add_custom_values(**{
    "demo:sensor_joint": joint,
    "demo:sensor_label": "force_sensor",
    "demo:sensor_value": 0.0,
})

model = builder.finalize()
view = newton.selection.ArticulationView(model, "robot")
sensor_values = view.get_attribute("demo.sensor_value", model)

In this example, sensor_values has shape (world_count, articulation_count_per_world, sensor_count_per_articulation). Owner indices are remapped automatically by add_builder(), add_world(), and replicate().

Frequencies that do not declare articulation ownership remain unavailable through ArticulationView. This is appropriate for global rows or relationships that can span multiple articulations, such as contact pairs.

USD Parsing Support#

Custom frequencies can support automatic USD parsing:

In this section, a row means one appended set of values for a custom frequency (that is, one index entry across all attributes in that frequency, equivalent to one call to add_custom_values()).

  • usd_prim_filter selects which prims should emit rows.

  • usd_entry_expander (optional) expands one prim into multiple rows.

def is_actuator_prim(prim, context):
    """Return True for prims with type name ``MjcActuator``."""
    return prim.GetTypeName() == "MjcActuator"

builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(
        name="actuator",
        namespace="mujoco",
        usd_prim_filter=is_actuator_prim,
    )
)

For one-to-many mappings (one prim -> many rows):

def is_tendon_prim(prim, context):
    return prim.GetTypeName() == "MjcTendon"

def expand_joint_rows(prim, context):
    return [
        {"mujoco:tendon_joint": 4, "mujoco:tendon_coef": 0.5},
        {"mujoco:tendon_joint": 8, "mujoco:tendon_coef": 0.5},
    ]

builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(
        name="tendon_joint",
        namespace="mujoco",
        usd_prim_filter=is_tendon_prim,
        usd_entry_expander=expand_joint_rows,
    )
)

When add_usd() runs:

  1. Parses standard entities (bodies, shapes, joints, etc.).

  2. Collects custom frequencies that define usd_prim_filter.

  3. Traverses prims under the requested root_path once (including instance proxies via Usd.TraverseInstanceProxies()).

  4. For each prim, evaluate matching frequencies in registration order:

    • If usd_entry_expander is set, one row is appended per emitted dictionary, and default per-attribute USD extraction for that frequency is skipped for that prim.

    • Otherwise, one row is appended from the frequency’s declared attributes.

Callback inputs:

  • usd_prim_filter(prim, context) and usd_entry_expander(prim, context) receive the same context shape.

  • context is a small dictionary:

    • prim: current USD prim (same object as the prim argument)

    • builder: current ModelBuilder instance

    • result: dictionary returned by add_usd()

Note

Important behavior:

  • Frequency callbacks are evaluated in deterministic registration order for each visited prim.

  • If a frequency defines usd_entry_expander, then for every matched prim in that frequency, the expander output is the only source of row values.

  • In that expander code path, the normal CustomAttribute USD parsing path is skipped for that frequency/prim. In other words, usd_attribute_name and usd_value_transformer are not evaluated for those rows.

  • Example: if frequency "mujoco:tendon_joint" has an expander and attribute CustomAttribute(name="tendon_coef", frequency="mujoco:tendon_joint", ...), then tendon_coef is populated only from keys returned by the expander rows. If a row omits "mujoco:tendon_coef", the value is treated as None and the attribute default is applied at finalize time.

This mechanism lets solvers such as MuJoCo define USD-native schemas and parse them automatically during model import.

Deriving Values from Prim Data (Wildcard Attribute)#

By default, each custom attribute reads its value from a specific USD attribute on the prim (e.g., newton:myns:my_attr). Sometimes, however, you want to compute an attribute value from arbitrary prim data rather than reading a single named attribute. This is what setting usd_attribute_name to "*" is for.

When usd_attribute_name is set to "*", the attribute’s usd_value_transformer is called for every prim matching the attribute’s frequency — regardless of which USD attributes exist on that prim. The transformer receives None as the value (since there is no specific attribute to read) and a context dictionary containing the prim and the attribute definition.

A usd_value_transformer must be provided when using "*"; omitting it raises a ValueError.

Example: Suppose your USD stage contains “sensor” prims, each with an arbitrary sensor:position attribute. You want to store the distance from the origin as a custom attribute, computed at parse time:

import warp as wp
import numpy as np

# 1. Register the custom frequency with a filter that selects sensor prims
def is_sensor(prim, context):
    return prim.GetName().startswith("Sensor")

builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(
        name="sensor",
        namespace="myns",
        usd_prim_filter=is_sensor,
    )
)

# 2. Define a transformer that computes the distance from prim data
def compute_distance(value, context):
    pos = context["prim"].GetAttribute("sensor:position").Get()
    return wp.float32(float(np.linalg.norm(pos)))

# 3. Register the attribute with usd_attribute_name="*"
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="distance",
        frequency="myns:sensor",
        dtype=wp.float32,
        default=0.0,
        namespace="myns",
        usd_attribute_name="*",
        usd_value_transformer=compute_distance,
    )
)

# 4. Parse the USD stage (assuming `stage` is an existing Usd.Stage)
builder.add_usd(stage)
model = builder.finalize()

# Access the computed values
distances = model.myns.distance.numpy()

The transformer context dictionary contains:

  • "prim": The current USD prim.

  • "attr": The CustomAttribute being evaluated.

  • When called from add_usd() custom-frequency parsing, context also includes "result" (the add_usd return dictionary) and "builder" (the current ModelBuilder).

This pattern is useful when:

  • The value you need doesn’t exist as a single USD attribute (it must be derived from multiple attributes, prim metadata, or relationships).

  • You want to run the same computation for every prim of a given frequency without requiring an authored attribute on each prim.

  • You need to look up related entities (for example, resolving a prim relationship to a body index through context["result"]["path_body_map"]).

Multi-World Merging#

When using add_builder(), add_world(), or replicate() in multi-world simulations, the references field specifies how attribute values should be transformed:

from newton import Model, ModelBuilder

builder = ModelBuilder()
builder.add_custom_frequency(
    ModelBuilder.CustomFrequency(name="pair", namespace="mujoco")
)
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="pair_world",
        frequency="mujoco:pair",
        dtype=wp.int32,
        namespace="mujoco",
        references="world",  # Replaced with the builder-managed current world during merge
    )
)
builder.add_custom_attribute(
    ModelBuilder.CustomAttribute(
        name="pair_geom1",
        frequency="mujoco:pair",
        dtype=wp.int32,
        namespace="mujoco",
        references="shape",  # Offset by shape count during merge
    )
)

Supported reference types:

  • Any built-in entity type (e.g., "body", "shape", "joint", "joint_dof", "joint_coord", "articulation") — offset by entity count

  • "world" — replaced with the builder-managed current_world for the active merge context

  • Custom frequency keys (e.g., "mujoco:pair") — offset by that frequency’s count

Querying Counts#

Use get_custom_frequency_count() to get the count for a custom frequency (raises KeyError if unknown):

# Finalize (requires at least one articulation)
_body = builder.add_link()
_joint = builder.add_joint_free(_body)
builder.add_articulation([_joint])
model = builder.finalize()

pair_count = model.get_custom_frequency_count("mujoco:pair")

# Or check directly without raising:
pair_count = model.custom_frequency_counts.get("mujoco:pair", 0)

Note

When querying, use the full frequency key with namespace prefix (e.g., "mujoco:pair"). This matches how attribute keys work: model.get_attribute_frequency("mujoco:condim") for a namespaced attribute.

ArticulationView Limitations#

Custom frequency attributes are accessible through ArticulationView only when their frequency declares articulation ownership. MuJoCo declares ownership for its actuator, tendon, tendon-joint, and tendon-wrap frequencies. Unscoped custom frequencies remain unavailable because their rows are not tied to the articulation structure. For other per-articulation data, use enum frequencies such as ARTICULATION, JOINT, or BODY.