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 - Static configuration and physical properties that remain constant during simulation
State Object - Dynamic quantities that evolve during simulation
Control Object - Control inputs and actuator commands
Contact Object - Contact-specific properties
Custom attributes extend these objects with user-defined arrays that follow the same indexing scheme as Newton’s built-in attributes.
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 (BODY,SHAPE,JOINT,JOINT_DOF,JOINT_COORD,ARTICULATION) or a string for custom frequenciesdtype: Warp data type (
wp.float32,wp.vec3,wp.quat, etc.)assignment: Which simulation object owns the attribute (
MODEL,STATE,CONTROL,CONTACT)default (optional): Default value for unspecified entities
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)
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
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.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")
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 = model.namespace_a.float_attr.numpy()
namespace_b_floats = state.namespace_b.float_attr.numpy()
control_floats = control.namespace_a.float_attr_dof.numpy()
For more information about USD integration and the schema resolver system, see USD Parsing and Schema Resolver System.
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.
Declaring Custom Frequencies#
Pass a string instead of an enum for the frequency parameter:
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_geom1",
frequency="pair", # Custom frequency (string)
dtype=wp.int32,
namespace="mujoco",
)
)
# → Frequency resolves to "mujoco:pair" via namespace
When a namespace is provided, it is automatically prepended to the frequency string, matching how attribute keys work.
Adding Values#
Custom frequency values are appended using add_custom_values():
# Declare attributes sharing a frequency
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(name="item_id", frequency="item", dtype=wp.int32, namespace="myns")
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(name="item_value", frequency="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,
})
model = builder.finalize()
print(model.myns.item_id.numpy()) # [100, 101]
print(model.myns.item_value.numpy()) # [2.5, 3.0]
Validation: All attributes sharing a custom frequency must have the same count at finalize() time. This catches synchronization bugs early.
Multi-World Merging#
When using add_world() to create multi-world simulations, the references field specifies how attribute values should be transformed:
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_world",
frequency="pair",
dtype=wp.int32,
namespace="mujoco",
references="world", # Replaced with current_world during merge
)
)
builder.add_custom_attribute(
ModelBuilder.CustomAttribute(
name="pair_geom1",
frequency="pair",
dtype=wp.int32,
namespace="mujoco",
references="shape", # Offset by shape count during merge
)
)
Supported reference types:
"body","shape","joint","joint_dof","joint_coord","articulation"— offset by entity count"world"— replaced withcurrent_worldCustom 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):
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 resolved frequency key with namespace prefix (e.g., "mujoco:pair"), not the raw string used in the declaration ("pair"). This matches how attribute keys work: model.get_attribute_frequency("mujoco:condim") for a namespaced attribute.
ArticulationView Limitations#
Custom frequency attributes are not accessible via ArticulationView because they represent entity types that aren’t tied to articulation structure. For per-articulation data, use enum frequencies like ARTICULATION, JOINT, or BODY.