@@ -519,8 +519,8 @@ def bind(self, mjcf_elements):
519519 ```
520520
521521 Note that the binding takes into account the type of element. This allows us
522- to remove prefixes for from certain common attributes in order to unify
523- access. For example, we can use:
522+ to remove prefixes from certain common attributes in order to unify access.
523+ For example, we can use:
524524
525525 ```python
526526 physics.bind(geom_element).pos = [1, 2, 3]
@@ -544,22 +544,42 @@ def bind(self, mjcf_elements):
544544 done lazily when an updated value is required, so repeated value
545545 modifications do not incur a performance penalty.
546546
547- It is also possible to bind a sequence containing multiple elements,
548- provided they are all of the same type. However, note that for a sequence of
549- elements the returned array will always be a copy, so writing into it will
550- not affect the underlying `Physics` instance:
547+ It is also possible to bind a sequence containing one or more elements,
548+ provided they are all of the same type. In this case the binding exposes
549+ `_SynchronizingArrayWrapper`s, which are array-like objects that provide
550+ writeable views onto the corresponding memory addresses in MuJoCo. Writing
551+ into a `_SynchronizingArrayWrapper` causes the underlying values in MuJoCo
552+ to be updated, and if necessary causes derived values to be recalculated.
553+ Note that in order to trigger recalculation it is necessary to reference a
554+ derived attribute of a binding.
551555
552556 ```python
553- physics.bind([geom1, geom2]).pos # Returns a copy.
554- physics.bind([geom1, geom2]).pos[:] = [[1, 2, 3], [4, 5, 6]] # No effect!
557+ bound_joints = physics.bind([joint1, joint2])
558+ bound_bodies = physics.bind([body1, body2])
559+ # `qpos_view` and `xpos_view` are `_SynchronizingArrayWrapper`s providing
560+ # views onto `physics.data.qpos` and `physics.data.xpos` respectively.
561+ qpos_view = bound_joints.qpos
562+ xpos_view = bound_bodies.xpos
563+ # This updates the corresponding values in `physics.data.qpos`, and marks
564+ # derived values (such as `physics.data.xpos`) as needing recalculation.
565+ qpos_view[0] += 1.
566+ # Note: at this point `xpos_view` still contains the old values, since we
567+ # need to actually read the value of a derived attribute in order to
568+ # trigger recalculation.
569+ another_xpos_view = bound_bodies.xpos # Triggers recalculation of `xpos`.
570+ # Now both `xpos_view` and `another_xpos_view` will contain the updated
571+ # values.
555572 ```
556573
557- To allow for assignment into multiple elements, bindings also support
558- numpy-style square bracket indexing. The first element in the indexing
559- expression should be an attribute name, and the second element (if present)
560- is used to index into the columns of the underlying array. Named indexing
561- into columns is also allowed, provided that the corresponding field in
562- `physics.named` supports it.
574+ Warning: it is unsafe to copy or serialize a `_SynchronizingArrayWrapper`.
575+ We also do not recommend holding references to them - instead hold a
576+ reference to the binding object, or call `physics.bind` again.
577+
578+ Bindings also support numpy-style square bracket indexing. The first element
579+ in the indexing expression should be an attribute name, and the second
580+ element (if present) is used to index into the columns of the underlying
581+ array. Named indexing into columns is also allowed, provided that the
582+ corresponding field in `physics.named` supports it.
563583
564584 ```python
565585 physics.bind([geom1, geom2])['pos'] = [[1, 2, 3], [4, 5, 6]]
0 commit comments