Symbolic Events¶
Symbolic events are introduced for the user to subscribe to updates of symbolic objects:
Method |
Event |
---|---|
The object has been created. |
|
The object has changed. |
|
The object has been created or changed. |
|
The parent of the object has changed. |
|
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