Python in TouchDesigner | Extensions | TouchDesigner

Rounding out some of our work here with Python is to look extensions. If you’ve been following along with other posts you’ve probably already looked over some extensions in this post. If you’re brand new to this idea, check out that example first.

Rather than re-inventing the wheel and setting up a completely new example, let’s instead look at our previous example of making a logger and see how that would be different with extensions as compared to a module on demand.

A warning for those following along at home, we’re now knee deep in Python territory, so what’ we’ll find here is less specific to TouchDesigner and more of a look at using Classes in Python.

In our previous example looking at modules we wrote several functions that we left in a text DAT. We used the mod class in TouchDesigner to treat this text DAT as a python module. That’s a neat trick, and for some applications and situations this might be the right approach to take for a given problem. If we’re working on a stand alone component that we want to use and re-use with a minimal amount of additional effort, then extensions might be a great solution for us to consider.

Wait, what are extensions?! Extensions are way that we can extend a custom component that we make in TouchDesigner. This largely makes several scripting processes much simpler and allows us to treat a component more like an autonomous object rather than a complex set of dependent objects. If you find yourself re-reading that last statement let’s take a moment to see if we can find an analogy to help understand this abstract concept.

When we talked about for loops we used a simple analogy about washing dishes. In that example we didn’t bother to really think deeply about the process of washing dishes – we didn’t think about how much detergent, how much water, water temperature, washing method, et cetera. If we were to approach this problem more programatically we’d probably consider making a whole class to deal with the process of washing dishes:

class Dishes():

    def __init__( self ):
        return

    def Full_cycle( self, list_of_dishes ):
        # what if we want to wash and then dry?

        self.Wash( list_of_dishes, water_temp )
        self.Dry( list_of_dishes )

        return

    def Wash( self, list_of_dishes, water_temp ):
        # what does washing really entail
        # all of that would go here

        number_of_dishes    = len( list_of_dishes )
        detergent_amt       = self.Set_soap( number_of_dishes )
        water_temp          = self.Set_water_temp( water_temp )

        return

    def Dry( self, list_of_dishes ):
        # what does washing really entail
        # all of that would go here

        number_of_dishes     = len( list_of_dishes )
        dry_time             = self.Set_dry( number_of_dishes )
        return

    def Set_soap( self, number_of_dishes ):
        return

    def Set_water_temp( water_temp ):
        if water_temp == 'hot':
            temp            = 98

        elif water_temp == 'medium':
            temp            = 80

        if water_temp == 'low':
            temp            = 60

        return temp

    def Set_dry( self, number of dishes ):
        return

Okay, so the above seems awfully silly… what’s going on here? In our silly example we can see that rather than one long function for Wash() or Dry(), we instead use several smaller helper functions in the process. Why do this? Well, for one thing it lets you debug your code much more easily. It also means that by separating out some of these elements into different processes we can fix a single part of our pipeline without having to do a complete refactor of the entire Wash() or Dry() methods. Why does that matter? Well, what if we decide that there’s a better way to determine how much soap to use. Rather than having to sort through a single long complex method for Wash() we can instead just look at Set_soap(). It makes unit testing easier, and allows us to replace or develop that method outside the context of the larger method. It also means that if we’re collaborating with other programmers we can divide up the process of writing methods.

What’s this self. business? self. allows us to call a method from inside another method in the same class. This is where extensions begin to really shine as opposed to using modules on demand. We an also use things like attributes andinheritance.

Okay, so how can we think about actually using this?

Let’s take a look at what this approach looks like in python:

import datetime

log_file            = op( 'text_log_file' )
log_path            = "example_extensions/log_files/log.txt"

full_text           = '''{now}

Current Year        | {year}
Current Month       | {month}
Current Day         | {day}
Current Hour        | {hour}
Current Minute      | {minute}
Current Second      | {second}
Current Microsecond | {microsecond}
'''

raw_date_time       = "On {month}-{day}-{year} at {hour}:{minute}:{second}"

verbose_log_message = '''============================
VERBOSE MESSAGE

{date_time}
----------------------------
operator            || {operator}
At Network Location || {path}

----------------------------
Logged
{message}
============================
'''

log_message         = '''----------------------------
{now}
----------------------------
{operator}
{path}
{message}
'''

class Ext_example():

    def __init__( self ):
        '''The init function.

        We're not doing anything with our init function in this example
        so we'll leave this empty.

        Notes
        ---------------
        ''' 
        return

    def Log_date( self ):
        year                        = datetime.datetime.now().year
        month                       = datetime.datetime.now().month
        day                         = datetime.datetime.now().day
        hour                        = datetime.datetime.now().hour
        minute                      = datetime.datetime.now().minute
        second                      = datetime.datetime.now().second

        updated_log_date            = log_date.format( 
                                                        month       = month,
                                                        day         = day,
                                                        year        = year,
                                                        hours       = hour,
                                                        minutes     = minute,
                                                        seconds     = second
                                                    )

        return updated_log_date

    def Log_date_time( self ):
        '''Create a formatted time stamp

        A look at how we might create a formatted time stamp to use with
        various logging applications.

        Arguments
        --------------- 
        None

        Returns
        ---------------
        formatted_text( str ) - a string time stamp

        Notes
        ---------------
        '''

        now         = datetime.datetime.now()
        year        = datetime.datetime.now().year
        month       = datetime.datetime.now().month
        day         = datetime.datetime.now().day
        hour        = datetime.datetime.now().hour
        minute      = datetime.datetime.now().minute
        second      = datetime.datetime.now().second
        microsecond = datetime.datetime.now().microsecond

        date_time   = raw_date_time.format( 
                                        month   = month,
                                        day     = day,
                                        year    = year,
                                        hour    = hour,
                                        minute  = minute,
                                        second  = second
                                        )

        return date_time

    def Log_message( self, operator, message, verbose=False, text_port_print=True, append_log=True ):
        '''Logging Method.

        A simple look at how you might start to think about building a logger for a TouchDesigner
        application. A logger is a great way to build out files with time stamped events. The
        more complex a project becomes, the more important it can become to have some means
        of logging the operations of your program. Here's a simple look at what that might
        look like.

        Arguments
        --------------- 
        operator( touch object ) - the touch object whose path you'd like incldued in the log message
        message( str ) - a message to include in the log
        verbose( bool ) - a toggle for verbose or compact messages
        text_port_print( bool ) - a toggle to print to the text port, or not
        append_log( bool ) - a toggle to append to the log file , or not

        Example
        --------------- 
        target_op       = op( 'constant1' )
        message         = "This operator needs attention"

        parent().Log_message( target_op, message )

        also

        parent().Log_message( target_op, message, verbose = True )

        Returns
        ---------------
        None

        Notes
        ---------------
        You'll notice that some arguments receive default values. This is so you don't have
        to include them in the call. This means that by default the message will be compact, 
        will print to the text port, and will append the log file.
        '''

        path        = operator.path
        op_name     = operator.name

        # logic tests for verbose or compact
        if verbose:
            message = verbose_log_message.format(
                                                    date_time   = self.Log_date_time(),
                                                    operator    = op_name,
                                                    path        = path,
                                                    message     = message
                                                )

        else:
            message = log_message.format(
                                            now                 = self.Log_date_time(),
                                            operator            = op_name,
                                            path                = path,
                                            message             = message
                                        )

        # logic tests for text_port_print
        if text_port_print:
            print( message )

        else:
            pass

        # log tests for appending log
        if append_log:
            log_file.write( '\n' + message )

        else:
            pass

        # save the log file to disk - external from the TouchDesigner project   
        self.Save_log()

        return

    def Save_log( self ):
        '''Saves log to disk.

        This helper function saves the log file to disk.

        Notes
        ---------------
        None
        '''

        op( log_file ).save( log_path )

        return

    def Clear_log( self ):
        '''Clears Log File.

        This helper function clears the text dat used to hold the log file.

        Notes
        ---------------
        None
        '''

        op( log_file ).clear()

        return

So getting started we set up a couple of variables that we were going to reuse several times. Next we declared our class, and then nested our methods inside of that class. You’ll notice that different from methods, we need to include the argument self in our method definitions:

# syntax for just a function
def Clear_log():
    return

# syntax for just a method in a class
def Clear_log( self ):
    return

We can also see how we’ve got several helper functions here to get us up and running – ways to add to the log, save the log to an external file, a way to clear the log. We can imagine in the future we might want to add another method that both clears the log, and saves an empty file, like a reset. Since we’ve already broken those functions into their own methods we could simply add this method like this:

def Reset_log():
    self.Clear_log()
    self.Save_log()
    return

Now it’s time to experiment.

Over the course of this series we’ve looked at lots of fundamental pieces of working with Python in TouchDesigner. Now it’s your turn to start playing and experimenting.

Happy programming!

Python in TouchDesigner | Modules | TouchDesigner

There are a number of ways that we might use modules on demand in TouchDesigner. Before we get too far along, however, we might first ask “what is a module on demand?”

According to the TouchDesigner wiki:

The MOD class provides access to Module On Demand object, 
which allows DATs to be dynamically imported as modules. 
It can be accessed with the mod object, found in the automatically 
imported td module. 

Alternatively, one can use the regular python statement: import. 
Use of the import statement is limited to modules in the search path, 
where as the mod format allows complete statements in one line, 
which is more useful for entering expressions. Also note that DAT modules 
cannot be organized into packages as regular file system based 
python modules can be.

What does this mean? It’s hard to sum up in just a single sentence, but the big thing to take away is that we can essentially use any text DAT to hold whole functions for us that we can then call whenever we want.

Let’s take a closer look at this process. We’ll start with some simple ideas, then work our way up to something a little more complicated.

First we turn things way down, and just think about storing variables. To be clear, we probably wouldn’t use this in a project, but it can be helpful for us when we’re trying to understand what exactly is going on here.

Let’s create a new text DAT and call it “text_variables”, inside let’s put the following text:

width       = 1280
height      = 720

budget      = 'small'

Using the mod class we can access these variables in other operators! To do this we’ll use the following syntax:

mod( 'text_variables' ).width
mod( 'text_variables' ).height
mod( 'text_variables' ).budget

Try adding a constant CHOP, and a text TOP to your network and using the expressions above to retrieve these values.

Next try printing these values:

print( mod( 'text_variables' ).width )
print( mod( 'text_variables' ).height )
print( mod( 'text_variables' ).budget )

So, it looks like we can access the contents of a module as a means of storing variables. That’s hip. Let’s take a moment and circle back to one of the other use cases that we’ve already seen for a module. More than just a single value, we can also put a whole dictionary in a module and then call it on demand. We’ve already done this in some of our previous examples, but we can take a quick look at that process again to make sure we understand.

Let’s create a new text DAT called “text_dictionary_as_module”, inside of this text DAT let’s define the following dictionary:

fruit = {
    "apple" : 10,
    "orange"    : 5,
    "kiwi"  : 16
}

Let’s first print the whole dictionary object:

print( mod( 'text_dictionary_as_module' ).fruit )

Alternatively, we can also access individual keys in the dictionary:

mod( 'text_dictionary_as_module' ).fruit[ 'apple' ]
mod( 'text_dictionary_as_module' ).fruit[ 'orange' ]
mod( 'text_dictionary_as_module' ).fruit[ 'kiwi' ]

What can you do with this?! Well, you might store your config file in a text DAT that you can call from a module on demand. You might use this to store configuration variables for your UI – colors, fonts, etc. ; you might decide to use this to configure some portion of a network, or to hold on to data that you want to recall later; or really any number of things.

Before we get too excited about storing variables in modules on demand, let’s look at an even more powerful feature that will help us better understand where they really start to shine.

Up next we’re going to look at writing a simple function that we can use as a module on demand. In addition to writing some simple little functions, we’re also going to embrace docstrings – a feature of the python language that makes documenting your work easier. Docstings allow us to leave behind some notes for our future selves, or other programmers. One of the single most powerful changes you can make to how you work is to document your code. It’s a difficult practice to establish, and can be frustrating to maintain – but it is hands down one of the most important changes you can make in how you work.

Alright, I’ll get off my documentation soapbox for now. Let’s write a few methods and see how this works in TouchDesigner.

We can start by creating a new text DAT called “text_simple_reutrn”, inside of this DAT we’ll write out our new functions:

def multi_by_two( value ):
    '''Multiplies input value by 2

    A simple example function to see how we can use modules on demand.
    This module takes a single argument which is multiplied by 2 and
    then returned from the function.

    Arguments
    --------------- 
    value( int / float ) - numeric value to be multiplied by 2

    Returns
    ---------------
    new_val( int / float ) - value * 2

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    new_val             = value * 2

    return new_val

def logic_test( even_or_odd ):
    '''Tests if input value is even or odd

    This is a simple little function to test if an integer is even or odd.

    Arguments
    --------------- 
    even_or_odd( int ) - an integer to be tested as even or odd

    Returns
    ---------------
    test( str ) - string result of the even / odd test

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    if even_or_odd % 2:
        test                = "this value is odd"

    else:
        test                = "this value is even"

    return test


def logic_test_two( value ):
    '''Silly logit test example

    Another simple function, this one to see another example of a 
    logic test working in a module on demand.

    Arguments
    --------------- 
    value( int / float / str / bool ) - a value to be tested

    Returns
    ---------------
    test( str ) - a string indicating the status of the test

    Notes
    ---------------
    These are doc strings - they're a feature of the Python language
    and make documenting your code all easier. This format is based largely
    on Google's Python documentation format - though not exactly. It's 
    generally good practice to document your work, leaving notes both for 
    your future self, as well as for other programmers who might be using
    your code in the future.
    '''
    if value == "TouchDesigner":
        test                = "Nice work"

    else:
        test                = "Try again"

    return test

Great! But what can we do with these? We can start by using some eval DATs or print statements to see what we’ve got. I’m going to use eval DATs. Let’s add several to our network and try out some calls to our new module on demand. First let’s look at the generic syntax:

mod( name_of_text_dat ).name_of_method

In practice that will look like:

mod( 'text_simple_reutrn' ).multi_by_two( 5 )
mod( 'text_simple_reutrn' ).multi_by_two( 2.5524 )
mod( 'text_simple_reutrn' ).logic_test( 5 )
mod( 'text_simple_reutrn' ).logic_test( 6 )
mod( 'text_simple_reutrn' ).logic_test_two( "TouchDesigner" )

Now we can see that we wrote several small functions that we can then call from anywhere, as long as we know the path to the text DAT we’re using as a module on demand! Here’s where we start to really unlock the potential of modules on demand. As we begin to get a better handle on the kind of function we might write / need for a project we can begin to better understand how to take full advantage of this feature in TouchDesiger.

Doc Strings

Since we took the time to write out all of those doc strings, let’s look at how we might be able to print them out! Part of what’s great about doc strings is that there’s a standard way to retrieve them, and therefore to print them. This means that you can quickly get a some information about your function printed right in the text port. Let’s take a closer look by printing out the doc stings for all of our functions:

# first let's clear the text port to make sure we're starting fresh
clear()

# Here we're printing out the doc strings for multi_by_two
print( "The Doc Strings for multi_by_two are:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).multi_by_two.__doc__ )

# Here we're printing out the doc strings for lotic_test
print( "The Doc Strings for logic_test:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).logic_test.__doc__ )

# Here we're printing out the doc strings for logic_test_two
print( "The Doc Strings for logic_test_two:" )
print( '\n' ) 
print( mod( 'text_simple_reutrn' ).logic_test_two.__doc__ )

That worked pretty well! But looking back at this it seems like we repeated a lot of work. We just learned about for loops, so let’s look at how we could do the same thing with a loop instead:

# first let's clear the text port to make sure we're starting fresh
clear()

# rather than wasting our time writing all the code in the other example, 
# instead let's write a for loop to automate that process.
# We'll start by first making a list of all of the methods we want to print 
# doc strings for
methods                         = [
                                "multi_by_two",
                                "logic_test",
                                "logic_test_two"
                                    ]

# next we'll make a smiple placeholder expression that we can 
# pass each method into so we can print it out easily
doc_string_temp                 = "mod( 'text_simple_reutrn' ).{target_function}.__doc__"

# finally we write a little for loop to go through all items in our list
# and pretty print their doc strings to the text port
for method in methods:
    print( "The Doc Strings for {} are:".format( method ) )
    temp_doc                    = doc_string_temp.format( target_function = method )
    print( eval( temp_doc ) )
    print( "= "  * 10 )

A Practical Example

This is all fun and games, but what can we do with this? There are any number of functions you might write for a project, but part of what’s exciting here is the ability to write something re-usable in Python. What might that look like? Well, let’s look at an example of a logger. There are a number of events we might want to log in TouchDesigner when we have a complex project.

In our case we’ll write out a method that allows a verbose or compact message, a way to print it to the text port or not, and a way to append a file or not. Alright, here goes:

import datetime

log_file            = op( 'text_log' )

full_text           = '''{now}

Current Year        | {year}
Current Month       | {month}
Current Day         | {day}
Current Hour        | {hour}
Current Minute      | {minute}
Current Second      | {second}
Current Microsecond | {microsecond}
'''

verbose_log_message = '''============================
VERBOSE MESSAGE

On {month}-{day}-{year} at {hour}:{minute}:{second}
----------------------------
operator            || {operator}
At Network Location || {path}

----------------------------
Logged
{message}
============================
'''

log_message         = '''----------------------------
{now}
----------------------------
{operator}
{path}
{message}
'''

def Full_date():
    '''Create a formatted time stamp

    A look at how we might create a formatted time stamp to use with
    various logging applications.

    Arguments
    --------------- 
    None

    Returns
    ---------------
    formatted_text( str ) - a string time stamp

    Notes
    ---------------
    '''

    now         = datetime.datetime.now()
    year        = datetime.datetime.now().year
    month       = datetime.datetime.now().month
    day         = datetime.datetime.now().day
    hour        = datetime.datetime.now().hour
    minute      = datetime.datetime.now().minute
    second      = datetime.datetime.now().second
    microsecond = datetime.datetime.now().microsecond

    formatted_text = full_text.format(
                                        now         = now,
                                        year        = year,
                                        month       = month,
                                        day         = day,
                                        hour        = hour,
                                        minute      = minute,
                                        second      = second,
                                        microsecond = microsecond
                                        )
    return formatted_text

def Log_message( operator, message, verbose=False, text_port_print=True, append_log=True ):
    '''Logging Method.

    A simple look at how you might start to think about building a logger for a TouchDesigner application. A logger is a great way to build out files with time stamped events. The more complex a project becomes, the more important it can become to have some means of logging the operations of your program. Here's a simple look at what that might look like.

    Arguments
    --------------- 
    operator( touch object ) - the touch object whose path you'd like included in the log message
    message( str ) - a message to include in the log
    verbose( bool ) - a toggle for verbose or compact messages
    text_port_print( bool ) - a toggle to print to the text port, or not
    append_log( bool ) - a toggle to append to the log file , or not

    Returns
    ---------------
    None

    Notes
    ---------------
    You'll notice that some arguments receive default values. This is so you don't have
    to include them in the call. This means that by default the message will be compact, 
    will print to the text port, and will append the log file.
    '''

    now         = datetime.datetime.now()
    year        = datetime.datetime.now().year
    month       = datetime.datetime.now().month
    day         = datetime.datetime.now().day
    hour        = datetime.datetime.now().hour
    minute      = datetime.datetime.now().minute
    second      = datetime.datetime.now().second
    microsecond = datetime.datetime.now().microsecond

    path        = op( operator ).path
    op_name     = op( operator ).name

    if verbose:
        message = verbose_log_message.format(
                                                month       = month,
                                                day         = day,
                                                year        = year,
                                                hour        = hour,
                                                minute      = minute,
                                                second      = second,
                                                operator    = op_name,
                                                path        = path,
                                                message     = message
                                            )
    else:
        message = log_message.format(
                                        now                 = now,
                                        operator            = op_name,
                                        path                = path,
                                        message             = message
                                    )

    if text_port_print:
        print( message )

    else:
        pass

    if append_log:
        log_file.write( '\n' + message )

    else:
        pass
    return

So now that we’ve written out the method, what would call for this look like?

operator = me

message ='''
Just a friendly message from your TouchDesigner Network.
Anything could go here, an error message an init message.

You dream it up, and it'll print
'''

# print and append log file with a verbose log message
mod( 'text_module1' ).Log_message( operator, message, verbose = True )

# print and append log file with a compact log message
# mod( 'text_module1' ).Log_message( operator, message )


# append log file with verbose log message
# mod( 'text_module1' ).Log_message( operator, message, verbose = True, text_port_print = False )


# print a compact log message
# mod( 'text_module1' ).Log_message( operator, message, append_log = False )

Take a moment to look at the example network and then un-comment a line at a time in the text DAT with the script above. Take note of how things are printed in the text port, or how they’re appended to a file. This is our first generalized function that has some far reaching implications for our work in touch. Here we’ve started with a simple way to log system events, both to a file and to the text port. This is also a very re-usable piece of code. There’s nothing here that’s highly specific to this project, and with a little more thought we could turn this into a module that could be dropped into any project.

Local Modules

We’ve learned a lot so far about modules on demand, but the one glaring shortcoming here is that we need the path to the text DAT in question. That might not be so bad in some cases, but in complex networks writing a relative path might be complicated, and using an absolute path might be limiting. What can we do to solve this problem. We’re in luck, as there’s one feature of modules we haven’t looked at just yet. We can simplify the calling / locating of modules with a little extra organization.

First we need to add a base and rename it to “local”, inside of this base add another base and rename it to “modules”. Perfect. I’m going to reuse one of our existing code examples so we can see a small change in syntax here. I’ve also changed the name of the text DAT inside of local>modules to “simple_return”.

mod.simple_return.multi_by_two( 5 )
mod.simple_return.multi_by_two( 2.5524 )
mod.simple_return.logic_test( 5 )
mod.simple_return.logic_test( 6 )
mod.simple_return.logic_test_two( "TouchDesigner" )
mod.simple_return.logic_test_two( 10 )

Looking at the above, we can see that we were able to remove the parentheses after “mod”. But what else changed? Why is this any better? The benefit to placing this set of functions in local>modules is that as long as you’re inside of this component, you no longer need to use a path to locate the module you’re looking for.

Alright, now it’s time for you to take these ideas out for a test drive and see what you can learn.

Python in TouchDesigner | Dictionary Loops | TouchDesigner

We’ve looked at dictionaries as data structures already, and have gotten a peak into how powerful then can be for storing and manipulating data. That’s all well and good, but wouldn’t it be lovely if we could loop through the contents of our dictionaries the same way we can loop through list items?

In fact, we can.

There are lots of ways to get there, so we’ll take a look at a variety of approaches to help us make this happen.

In this set of examples we’re going to use modules on demand as a fast way to get to our dictionaries that are stored in text DATs. We’ll take a closer look at how modules work later on, so for now let’s look at how we’re going to use them in this set of examples.

Here we can use the syntax:

mod( 'our_target_text_dat_here' ).our_target_variable

What does that mean exactly? This means we can put a python variable in a text DAT, and retrieve it with a mod call. For example, let’s say we have the following in a text DAT called “simple_mod_example”:

my_int = 5
my_float = 0.5
my_stirng = 'Hello there'

We can access these variables from any other operation with: mod( ‘simple_mod_example’ ).my_int mod( ‘simple_mod_example’ ).my_float mod( ‘simple_mod_example’ ).my_string

A little later one we’ll use some actual JSON that’s not in a module on demand as well – so with any luck by the end of this we’ll have a sense of how dictionaries work through and through. It’s also worth pointing out that python storage can also take advantage of dictionaries for fast variable access. So, once you have a handle on how this data structure works you’ll have a whole new world of options available to you.

Let’s first start with a a simple dictionary:

inventory = {
    "fruit" : {
        "apples" : 22 ,
        "kiwis" : 28 ,
        "grapes" : 11 ,
        "oranges" : 65 ,
        "grapefruit" : 75
    } ,
    "vegetables" : {
        "carrots" : 10 ,
        "potatoes" : 8 , 
        "onions" : 50 ,
        "kale" : 10
    } , 
    "beer" : {
        "ipa" : 32 ,
        "stout" : 46 ,
        "ale" : 56 ,
    }
}

Dictionaries are made of key and value pairs – the key being a string, and it’s pair being any data type of your choice. You start the construction of a dictionary with curly braces. Key value pairs are separated by a colon, and new entires are separated by commas. You’ll notice in the example above that we actually have a three dictionaries inside of a dictionary.

Let’s start by looking at just one:

fruit = {
    "apples" : 22 ,
    "kiwis" : 28 ,
    "grapes" : 11 ,
    "oranges" : 65 ,
    "grapefruit" : 75
}

This would be a fine way to create a single dictionary called fruit, with five entires, each with a quantity associated with them.

If we keep this in mind, looking back at our first example we can see that we have a single dictionary called “inventory” which contains a dictionary for fruit, vegetables, and beer. This nesting of key value pairs is a part of what makes these data structure so powerful. Where lists rely on an index based system to find an entry, dictionaries rely on keys. The major difference we can think about is that lists are ordered, and dictionaries are orderless. At first glance it might seem like an orderless data structure might not be useful, but the more we use them the more we’ll see how powerful they are.

Let’s look at our first code example, using modules means that we can use our dot notation to access an object inside of our text DAT.

inventory = mod( 'text_test_dictionary' ).inventory

# list of dictionary keys and their position
dictionary_keys = list( enumerate( inventory ) )

# let's get a feeling for how this works before we
# write a sentence
for item in dictionary_keys:
    print( item[ 0 ] , item[ 1 ] )

# loop through list and print the key and its
# list position
sentence = "The key {key} is in the {position} position of the list"

for item in dictionary_keys:
    print( sentence.format( key = item[ 1 ] , position = item[ 0 ] ) )

Okay, so what did we just do exactly? Well, we first made a list out of our dictionary keys, then we enumerated that list so we had an ordered construction with both the dictionary keys and a position, and then we printed all of that out. Why? We started by looking at how we might do this if we were working with a list. Isn’t that exactly what we’re trying to avoid? Yes, but since we already know how lists work this is a good place to get started so we can begin to better understand how these two things are different.

Okay, now that we’ve seen how this might work with an enumerated list, let’s look at key value loop. We’re going to use .items() to get the items out of our dictionary. We’ll first just look at how to get to the keys in our dictionary.

inventory = mod( 'text_test_dictionary' ).inventory

for dictionary_key , dictionary_value in inventory.items():
    print( "this dictionary key is: " , dictionary_key )

That’s a pretty good start, and makes it easy to see how keys work. But this is a different kind of loop here, so what else can we do? Let’s look at printing both our key and value this time around.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory

for dictionary_key , dictionary_value in inventory.items():
    print( "this dictionary key is: " , dictionary_key )
    print( "the dictionary value for this key is: " , dictionary_value )

Alright, that’s pretty slick but our formatting still leaves something to be desired. Let’s add a few more lines and see if we can print out something a little nicer.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory

# loop through the dictionary and print out contents
for dictionary_key , dictionary_value in inventory.items():
    # print out the first key depth in the dictionary   
    print( "- " * 10 )
    print( "this dictionary key is: " , dictionary_key )
    print( "in this dictionary we have:" )

    # loop through second dictionary layer
    for inventory_key , inventory_quantity in dictionary_value.items():
        # print out the key and value pairs from the second level
        print( '\t' , inventory_key , inventory_quantity )

Alright, not bad. Not bad at all. Here we can see that we ended up with a loop nested inside of our first loop. We started by printing out keys, then we run another loop to print out items and their quantities.

Recalling Presets

Okay, this is all well and good but what can we do with our new found tricks? Let’s start by doing something seemingly simple – filling out a table with the contents of our dictionary. Why put things into a table?! At some point you’ll certainly want to fill up a table with the contents of a dictionary… so we might as well look at how to do this early on.

# define our variables
# using modules means that we can use our dot notation to
# access an object inside of our text DAT
inventory = mod( 'text_test_dictionary' ).inventory
table = op( 'table_inventory' )
table_length = []
place_holder = 1

# first things first, let's clear out the contents of the table
table.clear()

# loop through the dictionary and add headers
for dictionary_key , dictionary_value in inventory.items():
    table.appendCol( [ dictionary_key ] )
    # add a blank column for next set of data points
    table.appendCol()

    # add to the list so we can determine the max length of the table
    table_length.append( len( dictionary_value ) )

# set the table length based on the maximum number of dictionary entries
table.setSize( max( table_length ) + 1 , table.numCols )

# loop through dictionary again to fill in table
for dictionary_key , dictionary_value in inventory.items():

# generate an enumerated list to loop through
    value_list = list( enumerate( dictionary_value ) )

# use enumerated list to fill in table 
    for item in value_list:
        table[ item[ 0 ] + 1 , dictionary_key  ] = item[ 1 ]
        table[ item[ 0 ] + 1 , place_holder ] = inventory[ dictionary_key ][ item[ 1 ] ]

# increment placeholder to ensure that our data goes in the right place
    place_holder += 2

Okay. Great. But how can we use this to actually do some interesting work?! Well, let’s begin by looking at how we might use these as presets. To get started let’s build out a simple little example. First we’ll add a movie file in TOP and call it ‘moviefilein_a’, connect this to a mono TOP called ‘mono_a’, and finally to a level TOP called ‘level_a’. We’re going to build out two sets of modules on demand for this example. First up is a set of default parameters for our TOPs. This is going to make sure that we can come back to a default position in our presents where nothing is on / nothing has changed in our pipeline. The idea here is that we can figure out the values to make sure that our chain of TOPs is not having any affect on our input texture.

level_defaults = {
    "invert" : 0,
    "blacklevel" : 0,
    "brightness1" : 1,
    "gamma1" : 1,
    "contrast" : 1,
    "gamma2" : 1,
    "opacity" : 1,
    "brightness2" : 1
}

mono_defaults = {
    "mono" : 0,
    "rgb" : 0,
    "alpha" : 4
}

Next let’s build out a set of cues that we can use to configure our little chain of TOPs.

cue_list = {
    "cue1" : {
        "file" : '/Map/Banana.tif',
        "mono" : 0 ,
        "invert" : 1,
        "brightness" : 0.71,
        "blacklevel" : 0.51,
        "contrast" : 1.93 
    } ,
    "cue2" : {
        "file" : '/Map/Smiley.tif',
        "mono" : 1 ,
        "invert" : 0,
        "brightness" : 1,
        "blacklevel" : 0.51,
        "contrast" : 1.5 
    } ,
    "cue3" : {
        "file" : '/Map/OilDrums.jpg',
        "mono" : 0,
        "invert" : 1,
        "brightness" : 0.71,
        "blacklevel" : 0.51,
        "contrast" : 1.93 
    }
}

You’ll notice that we’ve used two different approaches to name space here – that’s intentional. In our defaults we can see that we’ve made sure our key names match exactly to the parameter names on our TOPs. In our cues we can see that we’ve deviated from that a little. Why? Well, this will let us look at two different ways that we can target parameters when we’re using loops.

If we look back to our experiments with .pars() in looking at for loops, you might remember that we were able to us .pars() to quickly fill in a constant CHOP by ensuring that the contents of our table matched in terms of parameter names. This time around we can do the same thing by using our dictionary as our data structure instead of a table. We can then also quickly loop through the contents of our dictionary and assign values based on key names.

Let’s look briefly at how that might work.

First let’s look at how we might build a string to execute:

mono_defaults = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults = mod( op( 'text_defaults' ) ).level_defaults

for keys, values in mono_defaults.items():

    #reset mono defaults for A 
    exec( "op( 'mono_a' ).par." + keys + " = " + str( values ) )

    # debut print so you can see what this is actually doing
    # print( "op( 'mono_a.par' )." + keys + " = " + str( values ) )

for keys, values in level_defaults.items():

    #reset mono defaults for A 
    exec( "op( 'level_a' ).par." + keys + " = " + str( values ) )

That works well, but as someone pointed out on the facebook help group, using exec() can cause a set of problems. It’s useful to know that we can build an expression as a string, and then execute that command – all dynamically, but we can also use pars():

mono_defaults       = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults      = mod( op( 'text_defaults' ) ).level_defaults

for keys, values in mono_defaults.items():

    #reset mono defaults for A 
    op( 'mono_a' ).pars( keys )[ 0 ].val = values   

    # debut print so you can see what this is actually doing
    # print( "op( 'mono_a.par' )." + keys + " = " + str( values ) )

for keys, values in level_defaults.items():

#   #reset level defaults for A 
    op( 'level_a' ).pars( keys )[ 0 ].val = values

So far those seem like the same thing?! In this case, they’re pretty close. Because our key name matches our parameter name things are pretty straightforward here. Where it gets complicated is when our key names and parameter names don’t match.

Let’s use our cue_list dictionary to see how it might be different. Let’s imagine that we want to use cue2 as a preset. Now, because our key names and our parameter names don’t match, we have to write something like this:

movie_a                     = op( 'moviefilein_a' )
mono_a                      = op( 'mono_a' )
level_a                     = op( 'level_a' )
cue_list                    = mod( op( 'text_cue_list' ) ).cue_list

cue                         = 'cue2'

movie_a.par.file            = app.samplesFolder + cue_list[ cue ][ 'file' ]
mono_a.par.mono             = cue_list[ cue ][ 'mono' ]
level_a.par.invert          = cue_list[ cue ][ 'invert' ]
level_a.par.brightness1     = cue_list[ cue ][ 'brightness' ]
level_a.par.blacklevel      = cue_list[ cue ][ 'blacklevel' ]
level_a.par.contrast        = cue_list[ cue ][ 'contrast' ]

You’ll notice in the example above, we have to specify how each key and parameter relate to one another. In some cases this might be desirable. How?! You might ask. Well, lets’ imagine you have an AB system. In addition to our current set of operators, we’d also have movie_b, mono_b, and level_b. In this case it might be a little more tricky to know when to assign which parameter to which TOP. We might also end up in a situation where there are multiple level tops, so knowing when to target level1 vs. level2 might get a little more complicated. There are lots of ways you might solve these problems cleverly while still using pars(). It’s also worth pointing out that sometimes you want to be explicit in your code (probably more often than not), so you know exactly what’s happening. The important thing to remember here is that there’s a balance to find. You might not find it the first time you write your function or for loop, and it’s always okay to refactor your code to make it better.

Alright, let’s put some of these skills to use in an interesting way! We’ve used dictionaries as data structures, and we just learned how to loop through dictionaries. Let’s build out a set of buttons to recall our presets. I’m not going to go through how to make our buttons in detail – here are the cliff notes. Create a new container, add a button0 and give it the name “defaults”, next setup a replicator network to make three presets. Let’s make sure that our operator prefix is set to “preset” for our operator names when they get replicated. Finally, let’s make sure our buttons are set to radio. If all of this sounds like greek to you, make sure you go back and look at how replicators work first, and then look inside the example panel to get a better sense of how I set things up.

Okay! Now we’ll use a panel execute DAT (we just learned about panel executes) to recall our presets. We’ll use a logic test to run different loops depending on which button is pressed. In our case, Preset 1 – 3 will fill in our chain of operators with the presets from our cue_list dictionary. Clicking in the defaults button will return our chain of operators back to its default state. We can use the scripts we already wrote, and use the panel idea of the radio button selected to make this work. Let’s look at what that looks like:

# me - this DAT
# panelValue - the PanelValue object that changed
# 
# Make sure the corresponding toggle is enabled in the Panel Execute DAT.

mono_defaults       = mod( op( 'text_defaults' ) ).mono_defaults
level_defaults      = mod( op( 'text_defaults' ) ).level_defaults

movie_a             = op( 'moviefilein_a' )
mono_a              = op( 'mono_a' )
level_a             = op( 'level_a' )
cue_list            = mod( op( 'text_cue_list' ) ).cue_list

def offToOn(panelValue):
    return

def whileOn(panelValue):
    return

def onToOff(panelValue):
    return

def whileOff(panelValue):
    return

def valueChange(panelValue):
    # default radio button
    if panelValue == 0:
        for keys, values in mono_defaults.items():
            #reset mono defaults for A 
            op( 'mono_a' ).pars( keys )[ 0 ].val = values   

        for keys, values in level_defaults.items():                 
        #   #reset level defaults for A 
            op( 'level_a' ).pars( keys )[ 0 ].val = values  

    # for any of our preset buttons
    else:
        cue = 'cue' + str( panelValue )

        movie_a.par.file            = app.samplesFolder + cue_list[ cue ][ 'file' ]
        mono_a.par.mono             = cue_list[ cue ][ 'mono' ]
        level_a.par.invert          = cue_list[ cue ][ 'invert' ]
        level_a.par.brightness1     = cue_list[ cue ][ 'brightness' ]
        level_a.par.blacklevel      = cue_list[ cue ][ 'blacklevel' ]
        level_a.par.contrast        = cue_list[ cue ][ 'contrast' ]

    return

I know it’s seems like it’s been a long journey, but we’re now starting to see the real power of python in TouchDesigner.

Auto Configuration

Let’s quickly remember that with the Op Class we saw how we could create and destroy nodes. If we combine that with what we’ve just learned about using data structures and loops, we can begin to look at how we can automate the organization of a network.

Let’s imagine that we want to create a one toe file, and then depending on the IP address of a given machine we want TouchDesigner to set up different configurations. On one machine we might have two projectors, another might have three, another still might be our control machine. How can we make this work? Well, in this case we can store all of that information in a data structure like a python dictionary (you might also use JSON, or XML, or any data structure that you like). Next we’ll create a table based on our IP address, next we’ll loop through that table and then create our operators. Once you get a feeling for how this works with a table, you’ll quickly see how you could skip that step and just do this from the loop itself.

At the heart of this idea is the fact that you can store settings in your data structure, then build out your network based on that information. Alright, let’s dig in and get started.

A few things to keep in mind. Your IP address is going to matter. Remember that as you follow long here you’ll need to use your own IP address. You’ll notice that I’ve put a “default” address into a text DAT… this is so we can practice, but if you’re going to use this idea on a project you’ll need to familiarize yourself with the IP addresses, and have some control over how your machines are addressed.

Okay, to get started lets look at the configuration data we’ve set up:

uri = {
    "10.0.0.2" : {
        "name" : "mission_control" ,
        "role" : "controller" ,
        "displays" : [
            "control01" 
        ]
    },
    "10.0.0.3" : {
        "name" : "eyes" ,
        "role" : "node" ,
        "displays" : [
            "projector01" , 
            "projector02" 
        ]
    }
}

displays = {
    "control01" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 0
    },
    "projector01" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 0
    },
    "projector02" : {
        "width" : 1920 ,
        "height" : 1080 ,
        "orientation" : 1
    }
}

At a glance we can see that we’re working with two dictionaries – one called uri and the other called displays. These dictionaries have their own dictionaries nested inside, and have lists embedded inside as well. What’s going on here?! Our URI (uniform resource identifier) is a string key that corresponds to an IP address on a given machine. This key’s value pair is a dictionary instead of a single value. The contents of this dictionary are a name, a role, and a list of displays. For more complex configurations you might a much larger number of keys, but for now this should help us see what we’re up to. We also have a dictionary of displays. These contain keys related to resolution and orientation.

Alright, let’s put this information to good use. Before we get started we’re going to first add two more operators to our network. A table DAT called table_replicator_info and a text DAT called text_default_ip. We will fill in our table with the contents from our for loop process. For now we will use our default IP to see how this works.

Okay! Now let’s write a script to build out a network of ops:

# import any necessary modules

import socket

# define some variables

pos_x               = 3300
pos_y               = 0
replicator_table    = op( 'table_replicator_info' )
system_config       = mod( 'text_data' ).uri
displays            = mod( 'text_data' ).displays
local_ip            = socket.gethostbyname( socket.gethostname() )
default_ip          = op( 'text_default_ip' ).text
local_config        = {}
container_COMPs     = parent().findChildren( type = COMP , depth = 1 )

# clear contents of replicator_table
replicator_table.clear()

# delete existing container COMPs
for items in container_COMPs:
    if op( items ).name != 'container_presets':
        op( items ).destroy()
    else:
        pass

# Test for local_ip in system config
in_system_config = local_ip in system_config

# if our IP matches with a URI in the system config, then we
# fill our dictionary with keys based on the system config file
if in_system_config == True:
    local_config[ 'name' ]      = system_config[ local_ip ][ 'name' ]
    local_config[ 'role' ]      = system_config[ local_ip ][ 'role' ]
    local_config[ 'displays' ]  = system_config[ local_ip ][ 'displays' ]
    local_config[ 'uri' ]       = local_ip

# if our IP doesn't match our system config file, then we fill our
# dictionary with keys based on the default IP address, which is 
# entered in the text dat called 'text_default_ip'
else:
    # assign ip address
    local_ip = default_ip
    local_config[ 'name' ]      = system_config[ local_ip ][ 'name' ]
    local_config[ 'role' ]      = system_config[ local_ip ][ 'role' ]
    local_config[ 'displays' ]  = system_config[ local_ip ][ 'displays' ]
    local_config[ 'uri' ]       = local_ip

# fill table with display information
for items in local_config[ 'displays' ]:
    replicator_table.appendRow( [ 
        items , 
        displays[ items ][ 'width' ] , 
        displays[ items ][ 'height' ] , 
        displays[ items ][ 'orientation' ] 
        ] )

# check and correct height / width based on orientation flag
for row in range( replicator_table.numRows ):
    # create a temporary variable to store the width and height of the display
    width_height = [ replicator_table[ row , 1 ].val , replicator_table[ row , 2 ].val ]

    # test for orientation flag
    # if true, swap height and width
    if replicator_table[ row , 3 ] ==  1:
        replicator_table[ row , 1 ] = width_height[ 1 ]
        replicator_table[ row , 2 ] = width_height[ 0 ]

    # if false, we pass and do nothing
    else:
        pass

# create container COMPs based on our replicator table
for row in range( replicator_table.numRows ):
    # create our new container COMP, give it a name from our replciator table
    new_op = parent().create( containerCOMP , replicator_table[ row , 0 ] )

    # set the x position of the new container
    new_op.nodeX = pos_x

    # set the y position of the new container
    new_op.nodeY = pos_y

    # set the width of the new node based on the replicator table
    new_op.par.w = replicator_table[ row , 1 ]

    # set the height of the new node based on the replicator table
    new_op.par.h = replicator_table[ row , 2 ]

    # turn on the viewer flag
    new_op.viewer = True

    # increment the y position for the next loop
    pos_y -= 200

Why not just use a replicator?! That’s an excellent question. This might work just as well using a replicator – but, if we want to know exactly what order a set of operations happens in, and if we want to control when that operation fires then we need to write out a script to do just what we want. It might well seem like some additional work, but being able to work through the process line by line is worth at least learning even if you don’t end up using it much.

JSON

JSON, as a format is very similar to python dictionaries, so similar in fact there’s a very simple way to work with it.

If you’re already working with dictionaries this will feel like an easy transition. There are, however, a few things you need to keep in mind.

We need to make sure that our JSON is correctly formatted. If you compare the in our final example, with our first example you can see that we’ve enclosed our entire dictionary in curly brackets {}. You can also see that we’ve replaced our first variable with a key-value pair.

Let’s take a closer look:

# First we need to import the json module. 

import json

# next we're going to use just the text from our text dat
imported_dict       = op( 'text_simple_json' ).text

# next we'll use json.loads to import that as a python dictionary
dict_from_json      = json.loads( imported_dict )

# first let's just print the dictionary to make sure things worked out
print( "Here is our whole json object" )
print( imported_dict )

print( '_ ' * 10 )

# next we could print just the top tier keys
print( "Here are our top tier keys" )
for item in dict_from_json.keys():
    print( item )

print( '\n' )
print( '_ ' * 10 )
# next let's print the keys in our 'inventory' dictionary
print( "Here are the keys in inventory" )
for item in dict_from_json[ 'inventory' ].keys():
    print( item )

print( '\n' )
print( '_ ' * 10 )
# since we're on a roll, let's first print our keys, and
# then print their values
print( "Here are the keys and values in inventory" )
for key, value in dict_from_json[ 'inventory' ].items():
    print( key, 'contains', value )

print( '\n' )
print( '_ ' * 10 )
# That's still not very pretty, so let's see if we can make 
# something that's a little nicer
print( "Pretty printing our keys and values" )
for key, value in dict_from_json[ 'inventory' ].items():
    print( key, 'contains' )

    for item, quantity in value.items():
        print( quantity, item )

    print( '\n' )

With some easy ways to work with dictionaries under your belt challenge yourself to use these in an interesting way!