Symbolic Events

Symbolic events are introduced for the user to subscribe to updates of symbolic objects:

Method

Event

_on_init

The object has been created.

_on_change

The object has changed.

_on_bound

The object has been created or changed.

_on_parent_change

The parent of the object has changed.

_on_path_change

The location of the object has changed.

Tip

For all events, always call super().<event_method> first.

_on_init

For native symbolic classes, since __init__ is generated by the pg.Object base class, _on_init is a good place for performing additional argument validation and setting up the internal states for the class.

For example:

@pg.members([
    ('x', pg.typing.Int(max_value=1)),
    ('y', pg.typing.Int(max_value=1))
])
class Foo(pg.Object):

  def _on_init(self):
    super()._on_init()
    if self.x + self.y > 1:
      raise ValueError()
    self.z = self.x ** 2

foo = Foo(-2, 1)
assert foo.z == 4

Tip

Always consider _on_bound first as it covers both state initialization and reset.

_on_change

However, since symbolic arguments can be manipulated at runtime, the symbolic class needs to respond to the updates to ensure the consistency of its internal states. For example, foo.z needs to be updated automatically when foo.x has changed.

This can be done via subscribing the _on_change event, which is triggered when any of the symbolic arguments are updated via rebind:

@pg.members([
    ('x', pg.typing.Int(max_value=1)),
    ('y', pg.typing.Int(max_value=1))
])
class Foo(pg.Object):

  def _on_init(self):
    super()._on_init()
    self._validate_and_reset()

  def _on_change(self, updates):
    super()._on_change(updates)
    self._validate_and_reset()

  def _validate_and_reset(self)
    if self.x + self.y > 1:
      raise ValueError()
    self._z = self.x ** 2

The _on_change event takes a updates argument, which is a dict of pg.KeyPath to pg.FieldUpdate objects in case the user want to cherrypick the internal states to recompute based on the updates. For example:

def _on_change(self, updates):
    if 'x' in updates:
       self._z == self.x ** 2

Notification Rules

The chain of notification: When a symbolic object is changed, its containing object is considered changed. This chain of change propagates upward till there is no further containing object. Correspondingly, a chain of notifications will be triggered for each of the impacted objects. PyGlove invokes the _on_change method in a bottom-up way - the immediate updated object first, then its parent, so on and so forth, up to the root of tree.

Fire just once: If there are multiple symbolic arguments get updated, the containing object is guarenteed to receive the notification just once.

Do I Really Need _on_change?

In most circumstances, we can simply recompute all the internal states when any of the arguments changes. In such cases, _on_change usually has the same implementation as _on_init, which can be replaced by _on_bound.

_on_bound

_on_bound is the only event that is needed for most symbolic classes, for it handles both the internal state setup and reset:

@pg.members([
    ('x', pg.typing.Int()),
    ('y', pg.typing.Float())
])
class Foo(pg.Object):

  def _on_bound(self):
    super()._on_bound()
    self.z = x ** 2

Note

_on_init / _on_change will take precedence over _on_bound if both are subscribed.

_on_parent_change

Sometimes, a symbolic object need to respond to parent changes, e.g., when an object is firstly added to a symbolic tree or when it is removed. This can be done by subscribing the _on_parent_change event:

@pg.members([
    ('x', pg.typing.Int()),
    ('y', pg.typing.Float())
])
class Foo(pg.Object):

  def _on_parent_change(self, old_parent, new_parent):
    super()._on_parent_change(old_parent, new_parent)
    print('Parent changes from %r to %r' % (old_parent, new_parent))

foo = Foo(1, 2.0)

# Assigning `foo` to a key of a symbolic dict will
# trigger the `_on_parent_change` event with a None `old_parent`
# and the symbolic dict as the `new_parent`.
a = pg.Dict(x=foo)

# This will again trigger the `on_parent_change` event on `foo`,
# with the `old_parent` set to the symbolic dict and the `new_parent`
# set to None.
a.x = 1

_on_path_change

Similar as _on_parent_change, when the users need to subscribe symbolic object path change, they can override the _on_path_change method.

The _on_path_change event is triggered when a symbolic object’s location has changed, the location is represented by a path from the root of its containing symbolic tree to the object itself. For example:

@pg.members([
    ('x', pg.typing.Int()),
    ('y', pg.typing.Float())
])
class Foo(pg.Object):

  def _on_path_change(self, old_path, new_path):
    super()._on_path_change(old_path, new_path)
    print('Symbolic location has changed from %r to %r'
          % (old_path, new_path))

foo = Foo(1, 2.0)

# `foo`'s path will change from '' to 'x',
#  which trigger the `_on_path_change` event.
a = pg.Dict(x=foo)

# `foo`'s path will change from 'x' to '[0].x',
# triggering the event again.
b = pg.List([a])

# `foo`'s path will change  from '[0].x' to '',
# triggering the event for the third time.
a.x = 1