A CAD program like Antimony has to keep track of many small
pieces of data. For example, a 2D point has
One of the major design decisions in Antimony is that every datum is a piece of code. This means we can put arbitrary expressions into the point's coordinates.
Object properties are also free to refer to each other, with the graph acting
as a local namespace. Given a point named
p with properties
a datum could refer to
This brings us to the challenge: how do we build a system to represent this set of datums-as-code, tracking dependencies and re-evaluating data values as needed?
This writeup gives a high-level overview of the Antimony graph engine, which is a general-purpose library for building this kind of software infrastructure.
First, a basic level of functionality: how do we make an expression evaluate itself?
Antimony's chosen scripting language is Python, which has a friendly C API that allows it to be embedded into C/C++ applications.
To evaluate an expression, we simply call
This gives us back a
PyObject, which we can examine and show to the user.
PyRun_String takes in a pair of dictionaries (named
These dictionaries are used to look up variables when evaluating an
expression. We can pass in a custom dictionary that maps datum names to their
values, making lookups work:
When we evaluate
x + 3, Python looks for
x in the
which returns its value of 2.
This system should track dependencies: if
y looks up
x, then changes
x should cause
y to re-evaluate.
Since Python 2.4, the
locals argument doesn't strictly need to be a
dictionary; it just needs to be an object that implements the
mapping protocol. According
to the release notes,
"there's all sorts of new and shiny evil possible thanks to this little
One such evil possibility is recording when lookups happen. We pass a
object as the
locals argument, which stores both the name-to-value
association and a lookup-to-datum table:
locals for the value of
x, that lookup is stored; the
Proxy knows that
y cares about the value of
x changes, it informs the
Proxy that anything which has looked
x should re-evaluate itself. Since the
Proxy knows that
x, it triggers a re-evaluation of
A cool emergent benefit is correct behavior in response to changing (and
appearing) names. If
y was created first, the
Proxy still records that
it attempted a lookup on
x (even though the lookup failed). Then, when
x is created, the
Proxy knows to re-evaluate
y; the lookup will then
As Antimony doesn't have a constraint solver, certain types of recursive connections create undefined situations:
These kind of loops can be prevented by maintaining a set of upstream sources which contains any datum that is used in evaluating an expression.
Then, when a lookup is attempted, we check to see if the looked-up datum contains the downstream datum in its list of sources. If so, execution halts with an error flagging a recursive lookup.
We also want to make persistent graphical connections in the UI. These connections, unlike the lookup-by-name described above, should persist even if datum names change.
We'd like for connections to use the same lookup mechanism (and reap the benefits of dependency tracking). Making them persist through name changes requires adding a unique identifier (UID) to each Datum. Then, we mark connections with a special character that enables lookups by UID (instead of just by name).
Consider the following structure:
x changes, re-evaluation should be sequenced in the order
z needs to be evaluated twice (i.e. the sequence
Instead of evaluating children immediately, we'll schedule them in a queue. The next object to evaluate is the one without any sources in the queue (other than itself).
The example above proceeds as follows:
xis modified, scheduling
zin the queue
- We check to see whether objects have sources in the queue
yhas no sources in the queue
yas a source, which is in the queue
yis evaluated, adding
zto the queue (ignored since
zis already there)
zhas no sources in the queue other than itself so it's evaluated
- The queue is empty; evaluation is complete
The engine in Antimony goes beyond these idea in a few notable ways.
Datums are clustered into nodes to add a level of hierarchy. Keeping up the theme of "Python scripts everywhere!", nodes are defined by scripts:
You'll notice a set of magic hooks that actually change the node's Datums; designing this feature is left as an exercise for the reader (or you can cheat by looking at how I did it).
Nodes can also define UI hooks, shown in demos on the project page. These UI hooks aren't built-into the graph engine; instead, they're injected by the parent application.
Finally, the actual graph engine includes a callback system to notify the UI of changes to nodes and datums.