x
is an input to the FunctionGraph
, and the checks surrounding that are fairly strict
x
as an input that's distinct from an output
exp(x)
(λx.P)[x := N] == λz.(P'[x := N])
where P'
has y
renamed to z
λy.yx[x := xy]
would have the free y
in x := xy
bound/captured by the lambda
def create_fgraph():
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
e = at.log(d); e.name = 'e'
f = e - 3; f.name = 'f'
nodes = dict(a=a, b=b, c=c, d=d, e=e, f=f)
fg = FunctionGraph(inputs=[a], outputs=[f], clone=False)
return nodes, fg
# Can always replace one variable by an earlier one
nodes, fg = create_fgraph()
fg.replace_validate(nodes['c'], nodes['b'])
nodes, fg = create_fgraph()
fg.replace_validate(nodes['e'], nodes['c'])
# Can never replace one variable by a later one
nodes, fg = create_fgraph()
# This would enter an inifinite loop!
# fg.replace_validate(nodes['b'], nodes['c'])
# Unless it is the output variable, but then it gives
# an invalid cyclical graph
nodes, fg = create_fgraph()
fg.replace_validate(nodes['e'], nodes['f'])
aesara.dprint(fg)
# Elemwise{sub,no_inplace} [id A] 'f' 0
# |Elemwise{sub,no_inplace} [id A] 'f' 0
# |TensorConstant{3} [id B]
:point_up: Edit: ```python
def create_fgraph():
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
e = at.log(d); e.name = 'e'
f = e - 3; f.name = 'f'
nodes = dict(a=a, b=b, c=c, d=d, e=e, f=f)
fg = FunctionGraph(inputs=[a], outputs=[f], clone=False)
return nodes, fg
nodes, fg = create_fgraph()
fg.replace(nodes['c'], nodes['b'])
nodes, fg = create_fgraph()
fg.replace(nodes['e'], nodes['c'])
nodes, fg = create_fgraph()
nodes, fg = create_fgraph()
fg.replace_validate(nodes['e'], nodes['f'])
aesara.dprint(fg)
```
:point_up: Edit: ```python
def create_fgraph():
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
e = at.log(d); e.name = 'e'
f = e - 3; f.name = 'f'
nodes = dict(a=a, b=b, c=c, d=d, e=e, f=f)
fg = FunctionGraph(inputs=[a], outputs=[f], clone=False)
return nodes, fg
nodes, fg = create_fgraph()
fg.replace(nodes['c'], nodes['b'])
nodes, fg = create_fgraph()
fg.replace(nodes['e'], nodes['c'])
nodes, fg = create_fgraph()
fg.replace(nodes['b'], nodes['c'])
nodes, fg = create_fgraph()
fg.replace(nodes['e'], nodes['f'])
aesara.dprint(fg.outputs)
```
Last code dump I promise
#%%
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[c], clone=False)
fg.replace(b, d)
aesara.dprint(fg.outputs)
# Elemwise{log,no_inplace} [id A] 'c'
# |Elemwise{add,no_inplace} [id B] 'd'
# |Elemwise{log,no_inplace} [id A] 'c' <-- CYCLICAL
# |TensorConstant{5} [id C]
#%%
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[b], clone=False)
fg.replace(b, d)
aesara.dprint(fg.outputs)
# Elemwise{add,no_inplace} [id A] 'd'
# |Elemwise{log,no_inplace} [id B] 'c'
# | |Elemwise{exp,no_inplace} [id C] 'b'
# | |a [id D]
# |TensorConstant{5} [id E]
Does it make sense that the replacement of b -> d works when b is the output of the FunctionGraph (second half) but not when it is not (first half)?
:point_up: Edit: Last code dump I promise
#%%
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[c], clone=False)
fg.replace(b, d)
aesara.dprint(fg.outputs)
# Elemwise{log,no_inplace} [id A] 'c'
# |Elemwise{add,no_inplace} [id B] 'd'
# |Elemwise{log,no_inplace} [id A] 'c' <-- CYCLICAL
# |TensorConstant{5} [id C]
#%%
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[b], clone=False)
fg.replace(b, d)
aesara.dprint(fg.outputs)
# Elemwise{add,no_inplace} [id A] 'd'
# |Elemwise{log,no_inplace} [id B] 'c'
# | |Elemwise{exp,no_inplace} [id C] 'b'
# | |a [id D]
# |TensorConstant{5} [id E]
Does it make sense that the replacement of b -> d "works" when b is the output of the FunctionGraph (second half) but not when it is not (first half)?
By works I mean that it produces an acyclical graph
b
in the latter case, it basically cleared the entire FunctionGraph
and replaced it with another graph/output that also happened to reference b
FunctionGraph.outputs
FunctionGraph
output, like in the former case, an Apply
node is updated in-place
Apply.inputs
that's updated in-place
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[d], clone=False)
# Cannot do this
# fg.replace(b, c)
# But can do this
new_c = c.owner.op(*c.owner.inputs); new_c.name = 'new_c'
fg.replace(b, new_c)
aesara.dprint(fg.outputs)
fg.replace(b, c)
should behave differently than fg.replace(b, new_c)
?
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[d], clone=False)
# Cannot do this
# fg.replace(b, c)
# But can do this
new_c = c.owner.op(*c.owner.inputs); new_c.name = 'new_c'
fg.replace(b, new_c)
aesara.dprint(fg.outputs)
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[d], clone=False)
# Cannot do this
# fg.replace(b, c)
# But can do this
new_c = c.owner.op(*c.owner.inputs); new_c.name = 'new_c'
fg.replace(b, new_c)
aesara.dprint(fg.outputs)
# Elemwise{add,no_inplace} [id A] 'd'
# |Elemwise{log,no_inplace} [id B] 'c'
# | |Elemwise{log,no_inplace} [id C] 'new_c'
# | |Elemwise{exp,no_inplace} [id D] 'b'
# | |a [id E]
# |TensorConstant{5} [id F]
a = at.scalar('a')
b = at.exp(a); b.name = 'b'
c = at.log(b); c.name = 'c'
d = c + 5; d.name = 'd'
fg = FunctionGraph(inputs=[a], outputs=[d], clone=False)
# Cannot do this
# fg.replace(b, c)
# But can do this
# new_c = at.log(b); new_c.name = 'new_c'
new_c = c.owner.op(*c.owner.inputs); new_c.name = 'new_c'
fg.replace(b, new_c)
aesara.dprint(fg.outputs)
# Elemwise{add,no_inplace} [id A] 'd'
# |Elemwise{log,no_inplace} [id B] 'c'
# | |Elemwise{log,no_inplace} [id C] 'new_c'
# | |Elemwise{exp,no_inplace} [id D] 'b'
# | |a [id E]
# |TensorConstant{5} [id F]
a = at.scalar('a')
b = a + 1; b.name = 'b'
c = b + 1; c.name = 'c'
d = c + 2; d.name = 'd'
e = d + 2; e.name = 'e'
f = e + 1; f.name = 'f'
out = aesara.clone_replace(f, replace={b: at.cos(a), d: at.sin(c)})
aesara.dprint(out)
# Elemwise{add,no_inplace} [id A] 'f'
# |Elemwise{add,no_inplace} [id B] 'e'
# | |Elemwise{sin,no_inplace} [id C] ''
# | | |Elemwise{add,no_inplace} [id D] 'c'
# | | |Elemwise{add,no_inplace} [id E] 'b'
# | | | |a [id F]
# | | | |TensorConstant{1} [id G]
# | | |TensorConstant{1} [id H]
# | |TensorConstant{2} [id I]
# |TensorConstant{1} [id J]
d: at.sin(c)
erases the first update, but then clone_replace with multiple updates only works in very specific cases where the replacements are not nested at all
FunctionGraph.replace
is an in-place update of Apply.inputs
that operates on lambdas-like objects and has some lambda-relevant considerations