# FunctionGraph

## TODO What functionalities of FunctionGraphs are needed where?

Need to draw a map of where the different FunctionGraph functionalities could be used.

## Misc

From the documentation:

A FunctionGraph represents a subgraph bound by a set of input variables and a set of output variables, ie a subgraph that specifies an Aesara function. The inputs list should contain all the inputs on which the outputs depend. Variable\s of type Constant are not counted as inputs.

The FunctionGraph supports the replace operation which allows to replace a variable in the subgraph by another, e.g. replace (x + x).out by (2 * x).out. This is the basis for optimization in Aesara.

This class is also responsible for verifying that a graph is valid (ie, all the dtypes and broadcast patterns are compatible with the way the Variable\s are used) and for tracking the Variable\s with a :attr:FunctionGraph.clients dict that specifies which Apply nodes use the Variable. The :attr:FunctionGraph.clients field, combined with the :attr:Variable.owner and each :attr:Apply.inputs, allows the graph to be traversed in both directions.

It can also be extended with new features using :meth:FunctionGraph.attachfeature. See Feature for event types and documentation. Extra features allow the FunctionGraph to verify new properties of a graph as it is optimized.

What I am wondering is: when is a FunctionGraph needed? When does it become cumbersome?

import aesara
from aesara.graph.fg import FunctionGraph
import aesara.tensor as at

x = at.vector('x')
z = at.log(at.exp(x + x)) + x + x

fg = FunctionGraph(outputs=[z])

aesara.dprint(fg)

print(fg.clients)


## TODO What is the clients dictionary used for?

It looks like this is what allows to traverse the graph?