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 simulationState Object (
State) - Dynamic quantities that evolve during simulationControl Object (
Control) - Control inputs and actuator commandsContact 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
AttributeFrequencyenum value (e.g.,BODY,SHAPE,JOINT,JOINT_DOF,JOINT_COORD,ARTICULATION,ONCE) or a string for custom frequenciesdtype: Warp data type (
wp.float32,wp.vec3,wp.quat, etc.) orstrfor string attributes stored as Python listsassignment: 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 (
0for scalars, identity for quaternions,Falsefor 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 orlist[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") andmodel.namespace_a.float_attr(key:"namespace_a:float_attr") can coexistmodel.float_attr(key:"float_attr") andstate.namespace_a.float_attr(key:"namespace_a:float_attr") can coexistmodel.float_attr(key:"float_attr") andstate.float_attr(key:"float_attr") cannot coexist - same keymodel.namespace_a.float_attrandstate.namespace_a.float_attrcannot 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.
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 attributename)mjcf_value_transformer— callable that converts the XML string value to the target dtypeurdf_attribute_name— name of the XML attribute to read (defaults to the attributename)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 (raisesAttributeErrorotherwise)Each attribute must be used with entities matching its declared frequency (raises
ValueErrorotherwise)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},
])
Validation: All attributes sharing a custom frequency must have the same count at finalize() time. This catches synchronization bugs early.
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_filterselects 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:
Parses standard entities (bodies, shapes, joints, etc.).
Collects custom frequencies that define
usd_prim_filter.Traverses prims once (including instance proxies via
Usd.TraverseInstanceProxies()).For each prim, evaluate matching frequencies in registration order:
If
usd_entry_expanderis 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)andusd_entry_expander(prim, context)receive the same context shape.contextis a small dictionary:prim: current USD prim (same object as theprimargument)builder: currentModelBuilderinstanceresult: dictionary returned byadd_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
CustomAttributeUSD parsing path is skipped for that frequency/prim. In other words,usd_attribute_nameandusd_value_transformerare not evaluated for those rows.Example: if frequency
"mujoco:tendon_joint"has an expander and attributeCustomAttribute(name="tendon_coef", frequency="mujoco:tendon_joint", ...), thentendon_coefis populated only from keys returned by the expander rows. If a row omits"mujoco:tendon_coef", the value is treated asNoneand 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": TheCustomAttributebeing evaluated.When called from
add_usd()custom-frequency parsing, context also includes"result"(theadd_usdreturn dictionary) and"builder"(the currentModelBuilder).
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-managedcurrent_worldfor the active merge contextCustom 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 generally not accessible via ArticulationView because they represent entity types that aren’t tied to articulation structure. The one exception is the mujoco:tendon frequency, which is supported. For per-articulation data, use enum frequencies like ARTICULATION, JOINT, or BODY.