29
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TouchDesigner Advent Calendar 2019Advent Calendar 2019

Day 11

TouchDesigner pythonスクリプトとCHOP Executeの実行順序

Last updated at Posted at 2019-12-11

はじめに

TouchDesigner Advent Calendar 2019 11日目の記事です。

TouchDesignerで各種システムを実装する場合、「遅延なく1msでも早くこの関数を実行したい」とか「必ずこの順序で機能が実行されることを担保したい」など、関数実行の順序で夜も眠れないことが幾夜もありました。
そこで、今回はCHOPExecuteやpythonスクリプトからのCHOPパラメータ変更、コンポーネント内関数実行の実行順序に性質について今年のうちに確認してみることにしました。

手っ取り早く最後にまとめがあります。
toeはこちら
https://github.com/shks/TDCHOPExecuteOrderTest

実行環境

  • macOS 10.14.2
  • TouchDesigner 2019.17550

検証方法

pythonコード内実行順序で確認

text1DAT
import time
print('python ' ,  1 ,  1000*time.time())
print('python ' ,  2 ,  1000*time.time())
print('python ' ,  3 ,  1000*time.time())
python >>> 
python  1 1576067145201.466
python  2 1576067145201.4822
python  3 1576067145201.489

ここで、text1 DATを選択した状態でcontrol + Rによってpythonスクリプトを実行することができます。今回時間計測はtimeモジュールのtime.time()を用います。windowsとmacで挙動が異なる、time.perf_counter()のほうが好ましい等の情報もありますが、今回は遅延時間計測よりも順序の検証が主な目的なので、time.time()をそのまま使っています。数字はms単位にして表示します。

python実行での出力は、textportにて確認できます。ここで、毎計測ごとにclearをしないとprint関数実行自体の遅延が発生するので、計測毎にclearして観測します。

image.png

CHOP並列接続 CHOP Execute実行

まず、ネットワーク上で「値が**になったら〇〇する」といった場合に用いるCHOP Executeの実行順序を調査します。

下記のようなネットワークで、ボタンを押すことで、null CHOPnull1, null2, null3を参照している、CHOP Executeが実行されます。これらの実行順序はどのように決定しているのか。
null CHOPの生成順序は1,2,3、chopexec1 Datの生成順序も1,2,3となる。ネットワーク接続順番も、1,2,3の順番で生成している。下記の図では、丸で囲まれた数字がtextportで確認した実行順序を示す。

image.png

CHOPExecute
import time
def onOffToOn(channel, sampleIndex, val,  prev):
	print("chop exec - onOffToOn     " , me.digits ,1000 * time.time())
return

結果

python >>> 
chop exec - onOffToOn      3 1576067851541.211
chop exec - onOffToOn      2 1576067851541.3289
chop exec - onOffToOn      1 1576067851541.434

これを見る限り、なにか1, 2, 3の逆順で実行されているようです。

オペレーターの名前で変わるのであろうか?

image.png

null2null99 , chopexec2chopexec99
のように、名前を変更する。

python >>> 
chop exec - onOffToOn      3 1576067912511.506
chop exec - onOffToOn      99 1576067912511.7139
chop exec - onOffToOn      1 1576067912511.945

しかし、実行順序は変わらない。つまり、名前の順序順序ではない。(なお、配置を変えても変わらない)となると、では、

  • CHOPオペレーターの生成順序なのか
  • DATオペレーターの生成順序に依存するのか?
  • もしくはCHOPオペレーター同士を接続するネットの順番か?

並列に接続されたCHOPにそれぞれに、逆順で生成したchopexec DATを参照させる。

image.png

python >>> 
chop exec - onOffToOn      3 1575998873208.3982
chop exec - onOffToOn      2 1575998873208.636
chop exec - onOffToOn      1 1575998873208.7642

null Chopの生成順序は1,2,3、chopexec1 Datの生成順序も3,2,1。よって上記の「DATオブジェクトの生成順序」ではない。

オペレーター同士を接続するネットワークの生成順序か?

上記のネットワークのnull_xnull*の接続を切った後に、下記の順番で再接続。実行順番は変わるか?図中のネットワークに記載した番号はネットワークの生成順序を示す。

image.png

ネットワークの生成順序で実行順序が変わる。例として、下記の場合は

image.png

python >>> 
chop exec - onOffToOn      1 1576068005900.81
chop exec - onOffToOn      3 1576068005900.926
chop exec - onOffToOn      2 1576068005901.0278

結果、上記3つですべて実行順序が異なり、ネットワーク生成順序の(若い方)新しく生成されたネットワークから先に伝搬し、chopexecDatが実行されることを確認。

では、一つのCHOPに複数のchopexecが参照されていたらどうなるのか。

並列に接続されたCHOPにそれぞれに、複数のchopexec DATを参照させる。

image.png

ここでもおなじく、つなげた順番で若い順で実行される。上記で、chopexec2_created_2,chopexec2_created_4chopexec2_created_5は、この順番でオペレーターを生成したが、
下記の順番にnull2_2への参照(リンク)を行った。

  1. chopexec2_created_2
  2. chopexec2_created_5
  3. chopexec2_created_4

その場合、CHOPに接続されたchopexec DATの実行順序は


python >>> 
chop exec - onOffToOn      7 1576002202557.483
chop exec - onOffToOn      1 1576002202557.6028
chop exec - onOffToOn      4 1576002202557.711
chop exec - onOffToOn      5 1576002202557.908
chop exec - onOffToOn      2 1576002202558.051
chop exec - onOffToOn      6 1576002202558.2139
chop exec - onOffToOn      3 1576002202558.403

ここで、chopexec2_created_4が、chopexec2_created_5よりも早く実行されているが分かる。
よって、chopexec Datへの参照リンクが若い方から実行される。
(なお、生成した後に名前を変えたり、digit(オペレーターの名前を変更しても挙動は変わらない)

CHOP直列接続 chop executeDat実行

image.png

python >>> 
chop exec - onOffToOn      3 1575997858135.001
chop exec - onOffToOn      2 1575997858135.2651
chop exec - onOffToOn      1 1575997858135.4722

このように、後ろから実行されているようだが、これは、接続の終端からだろうか?それとも生成順であろうか。そこで次のネットワークでは、同じだか生成タイミングと接続順序が異なる状況でCHOP Executeを実行させる。

image.png


python >>> 
chop exec - onOffToOn      4 1575998953775.958
chop exec - onOffToOn      6 1575998953776.133
chop exec - onOffToOn      5 1575998953776.284
chop exec - onOffToOn      3 1575998953776.4722
chop exec - onOffToOn      2 1575998953776.708
chop exec - onOffToOn      1 1575998953776.9019

直列に接続されている場合には、終端のオペレーターに参照されているchopexecから実行される。その歳には、オペレーターの生成順序は関係ない。なお、ここでnull4,null5,null6に`接続しているネットワークを後から付け足したので、4,6,5が先に実行されている。

つまり、分岐が存在した場合には、若いネットワークの方を探索し、その終端から順番に実行される。
上記のネットワークを下記のように変えると、*マークのネットワークのほうが新しい(若い)接続になるので、下記のように終端の新しい接続から先に実行される。

image.png

python >>> 
chop exec - onOffToOn      4 1576000433919.5671
chop exec - onOffToOn      3 1576000433920.164
chop exec - onOffToOn      6 1576000433920.728
chop exec - onOffToOn      5 1576000433921.278
chop exec - onOffToOn      2 1576000433921.777
chop exec - onOffToOn      1 1576000433922.294

もう十分ですね。はい。

CHOP Execute直列/並列実行

image.png

このように、chopexec1内でop('constant4').par.value0 = 1, op('constant3').par.value0 = 1 のように、constantCHOP オペレーターの値を直接書き込んでしまう方法もある。この場合の実行順序はpythonスクリプトでのアクセス順序であろうか。

chopexec1
op('constant2').par.value0 = 1
chopexec2
op('constant4').par.value0 = 1
op('constant3').par.value0 = 1

この場合、その場合には、pythonに記載した順序で変更先で伝搬した値に基づいてchopexec3 , chopexec4が実行される。

textport
python >>> 
chop exec 1 1576006026799.0889
chop exec 2 1576006026799.2761
python >>> 
chop exec 4 1576006026799.5068
chop exec 3 1576006026799.634

pythonスクリプト内のchopの読み書き

TouchDesignerではpython上で動作ロジックを構築することも多くありますが、そのpythonからCHOP等の様々なオペレーターの値を取得したり、更新することができます。それらの処理が行われるタイミングに関して調べてみます。

まずは、pythonスクリプト実行途中で、ConstantCHOP等に読み書きを行った場合、”即時”に値が反映されているのであろうか?

image.png

text_read_and_write内のpythonスクリプトで3つのconstantに対して値を書き込んで、かつ値を読んでprint出力します。かつ、その値の差分を計算して、書き込んだ値が入っているかを確認します。これを見る限り、pythonスクリプトでConstantCHOPのパラメータへ値を代入した時点で代入値が保持され、次の行ではすでにその値を取得できることがわかります。

text_read_and_write
import time
import random

def functionExe(index):
    r =random.randint(1,10)
    #write
    t = time.time()
    op('constant' + str(index)).par.value0 = r
    print('writetime ' ,  str(index) ,  (time.time() - t) * 1000 , 'ms',  r )
    
    #read
    t = time.time()
    v = op('constant' + str(index))[0]
    print('readtime  ' ,  str(index) ,  (time.time() - t) * 1000 , 'ms',  v )
    print('write-read' ,  str(index) ,  v - r )

for i in range(1,6):
    functionExe(i)

textport
python >>> 
writetime  1 0.03910064697265625 ms 5
readtime   1 0.032901763916015625 ms 5.0
write-read 1 0.0
writetime  2 0.019073486328125 ms 1
readtime   2 0.015974044799804688 ms 1.0
write-read 2 0.0
writetime  3 0.014066696166992188 ms 10
readtime   3 0.0050067901611328125 ms 10.0
write-read 3 0.0

では、pythonスクリプトから更新したconstantChopから接続されているNullCHOPに入る値はどのようになるのでしょうか?

Pythonスクリプト内からのCHOP読み書きとネットワーク接続の関係

pythonスクリプトから、あるConstantCHOPの値を更新し、その先に接続しているNullChopの値をpythonスクリプト上から取得する。

image.png

すると、CHOPへ書込みの値とCHOPからの読み出しの値が同じ値で、printには0.0と出ていることが確認できます。

textport
write - read 1 0.0
write - read 2 0.0
write - read 3 0.0

結果:

pythonからあるCHOPを更新し、その先に接続しているChopにpythonからアクセスすると、更新処理が伝搬する。(これは、平たく言うと、pythonからあるChopを更新すると、その接続先のChopまで更新処理が伝搬する。厳密には違う)

厳密には上図の「3」が呼ばれたときに「2」のCookが実行され、更新された値を「3」で取得できるという流れであるが、そうであっても実行順序は1, 2, 3のようになる。
詳しくはこちら→https://docs.derivative.ca/Cook

The Order of Cooking

TouchDesigner is a "pull system". A common misconception with cooking in TouchDesigner is that cooking starts upstream and moves downstream.
Almost all operators will only cook when something is interested in their data.

謎:

それでは、このようにpythonスクリプトから、Constant CHOPの値を変更した場合、前述の CHOPExcuteの実行順序はどのようになるのでしょうか?値が変更されているのならば、cook(処理)が走って値が変更された時点で、CHOPExcuteスクリプトが実行されるのでしょうか?

Pythonスクリプト内からのCHOP読み書きとネットワーク途中のCHOPExcuteの関係

先程の終端にあったnullChoponValueChangeで関数を実行するようにします。

chopexec
import time
def onValueChange(channel, sampleIndex, val, prev):
    print("chop exec - read" , me.digits ,1000 * time.time())
    return

そこで上記と同じように、constantCHOPnullCHOPを接続して、
constant1の値書き換え、null1から値読み出し、
constant2の値書き換え、null2から値読み出し、
constant3の値書き換え、null3から値読み出し
を行います。上記の実験から、null1読み出しを行った時点で、constant1-null1の更新部分は実行されているので、そのタイミングで、onValueChange()も実行されているのでは、と期待してしまいそうになりますが。。

image.png

python >>> 
write - read 1 0.0 1576057360065.77
write - read 2 0.0 1576057360065.836
write - read 3 0.0 1576057360065.887
chop exec - read 1 1576057360066.437
chop exec - read 2 1576057360066.59
chop exec - read 3 1576057360066.747

結果は、最初にpythonスクリプト内の処理(nullChop1, nullChop2, nullChop3 からの値読み出しを含む)が完了した後に、chopexec_1,chopexec_2, chopexec_3onValueChange()が順次実行されています。

結果:

pythonスクリプト内の処理が完了した後に、CHOPExecuteが順次実行される。

謎:

では、python内部で1回以上、constantChopの値を書き換えた場合には、どのようになるのでしょうか。onValueChangeは二度実行されるのか。

Pythonスクリプト内からの複数のCHOP読み書きとネットワーク途中のChop excute関係

単純に、上記のスクリプトを2度、textDatのpython実行で繰りした。但し、printで表示される値は、書込み値と、読み込み値を別で表示している。

textDat
import time
import random
###FIRST 
r =random.randint(1,10)
#1
op('constant1').par.value0 = r
v = op('null1')[0]
print(1, 'write', r, 'read', v,1000 * time.time())

r =random.randint(1,10)
#2
op('constant2').par.value0 = r
v = op('null2')[0]
print(2, 'write', r, 'read', v,1000 * time.time())

r =random.randint(1,10)
#3
op('constant3').par.value0 = r
v = op('null3')[0]
print(3, 'write', r, 'read', v,1000 * time.time())
###SECOND
r =random.randint(1,10)
#1
op('constant1').par.value0 = r
v = op('null1')[0]
print(1, 'write', r, 'read', v,1000 * time.time())

r =random.randint(1,10)
#2
op('constant2').par.value0 = r
v = op('null2')[0]
print(2, 'write', r, 'read', v,1000 * time.time())

r =random.randint(1,10)
#3
op('constant3').par.value0 = r
v = op('null3')[0]
print(3, 'write', r, 'read', v,1000 * time.time())

image.png

1 write 8 read 8.0 1576058581321.446 ms
2 write 4 read 4.0 1576058581321.513 ms
3 write 2 read 2.0 1576058581321.5588 ms
1 write 8 read 8.0 1576058581321.5881 ms
2 write 1 read 1.0 1576058581321.626 ms
3 write 10 read 10.0 1576058581321.6628 ms
chop exec - read 1 1576058581322.1929 ms
chop exec - read 2 1576058581322.376 ms
chop exec - read 3 1576058581322.56 ms

pythonスクリプト上では、constantChopの値書込みと、その接続されてるnullChopからの値読み込みは2度行われているが、nullChopを参照している、chopexec1度しか実行されていない

ここで、「値が変化するたびにchopexeconValueChangeを実行する」という前提だけだと、pythonスクリプト上で複数回変化したとしても、CHOPExecute上では1度しか変化していないということになる。

結果:

python上からのネットワーク上のChop値を変更している場合、chopexecが実行されるタイミングはpythonスクリプトの実行が完了した後に1度だけ順次起こる。

Pythonスクリプト内からのchop読み書きとネットワーク接続の関係(破綻編)

上記の例からも、chopexecの実行タイミングは、参照しているCHOPのcookタイミングそのものではないことがわかります。よって、chopexec等を用いて値を更新させている場合、例えば

constant1からnull1に受け取った値で、onChangeValue時に隣のconstant_readの値を更新する。

op('constant_read').par.value0 = val

image.png

この場合には、上記のようにconstant1の値を更新して、その結果としてのconstant_readの値を取得しようとすると、実行順番は下記のように、python実行後に、onChangeValue()が実行されてconstant_readの値が更新されるので、pythonスクリプト内でのwriteとreadの値は一致せず、readでは、”前回実行時の値”が取得されてしまう。

textDat
import time
import random
r = random.randint(1,10)
#write
op('constant' + str(1)).par.value0 = r
#read
v = op('constant_read')[0]
print('write ',  r , 'read', v)
write  9 read 6.0 1576061048494.0022 ms
chop exec 1576061048494.885 ms

Pythonスクリプト内からのchop読み書きとCHOP Referenceでの参照接続

CHOP Referenceでの参照接続はどうなっているのであろう。

image.png
image.png

結論:

ネットワーク接続と同じ。pythonスクリプトからの値取得時にcookされて、値が更新された状態で、constant_readからの値を取得できる。

コンポーネント内の関数実行順序

システムが複雑になって来ると、container COMPbase COMPへcustomize Component機能に使って、関数などをまとめていく事がありますが、COMPの中にまとめた関数実行のタイミングがいまいち不明瞭でしたので、この勢いで実験してみました。

COMPへのカスタムコンポーネント追加の方法については、他のよき記事を参照していただければ幸いです。
https://qiita.com/joe0hara/items/de0f91e8886c116504b3#_reference-00aa98459583b0bcec67

Extension Codeでの実行と、Custom Parameters でのPulseボタンへのpulse()

個人的によくやるのが、

  • コンポーネントのcustomize ComponentでのExtension Codeを追加する
  • Custom ParametersでのPulseボタンを追加して、コンポーネントのパラメータリストから実行できるようにする
    • ParameterのPulseボタンに対して、btnName.pulse()のように、スクリプトからPulseボタンを押す。

の二種類ですが、どうやら後者の挙動が期待どおりにならないことが多かったので、調査しました。

image.png

text1 DATには下記のスクリプトを記載。
op('container1') にまとめている関数、2種類の実行方法のタイミングを調べる。

text1DAT
import time
print('text1 python   excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
print('text1 python   excuted 2 ', time.time() * 1000)

container1内には、下記の2つ、Scriptparexec1がある。Scriptは、ComponentでのExtension Codeを追加して作成。parexec1は、me.parent()を対象にして、onPulse関数で、container1Pulseボタンが押されたときに実行するスクリプトを記述。それぞれ、実行時の時間も出力されるようにしておく。

image.png

Script text DAT内スクリプト

Script
from TDStoreTools import StorageManager
TDF = op.TDModules.mod.TDFunctions
import time
class Script:
    """
    Script description
    """
    def __init__(self, ownerComp):
        # The component to which this extension is attached
        self.ownerComp = ownerComp
    def PromotedFunction(self):
        print('PromotedFunction excuted ', time.time() * 1000)

parexec1内スクリプト

parexec1

import time
def onPulse(par):
    print('container1 onPulse  excuted   ', time.time() * 1000)
    return

結果のtextport

python >>> 
text1 python   excuted 1  1576063357049.925
PromotedFunction excuted  1576063357049.95
text1 python   excuted 2  1576063357050.025
container1 onPulse  excuted    1576063357050.614

これを、上記の実行したtext1DATのスクリプトを見比べると

text1DAT
##(再掲)
import time
print('text1 python   excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
print('text1 python   excuted 2 ', time.time() * 1000)

Extension Codeによって実行した、container内のScriptの関数は、pythonコード関数として期待したタイミングで実行されていますが、par.Execute.pulse()から、parexec1経由で実行したonPulse()関数は、text1DATのスクリプトの完了後に実行されています。

Pythonスクリプトから、pulse()を2回以上実行した場合

ちなみに、ここで、text1DATのスクリプト内で、par.Execute.pulse()を2度実行した場合には、、、
(これまでの流れから、なんとなく予感はしますが。。)

text1DAT
import time
print('text1 python   excuted 1 ', time.time() * 1000)
op('container1').PromotedFunction()
op('container1').par.Execute.pulse()
op('container1').par.Execute.pulse() #2回目
print('text1 python   excuted 2 ', time.time() * 1000)

結果:

python >>> 
text1 python   excuted 1  1576064625857.658
PromotedFunction excuted  1576064625857.682
text1 python   excuted 2  1576064625857.71
container1 onPulse  excuted    1576064625858.234

そうです。parexec1onPulse()関数は1度しか呼ばれていません。(恐ろしや)
Extension Codeによる関数実行と、par.ButtonName.pulse()による関数の実行は順序と性質が全く異なることがわかります。

#まとめ

今回は、日頃気になっていたCHOPExecuteの実行順番、pythonスクリプトとの関係、コンポーネント内の関数実行順序について調べてみました。

CHOP Executeの実行順番

  1. CHOPが並列に接続されている場合、ネットワーク生成順序の(若い方)新しく生成されたネットワーク順に、CHOPExecuteが実行される
  2. CHOPが直列に接続されている場合、ネットワークの終端に近い順にCHOPExecuteが実行される
  3. 同一のCHOPに対する複数のchopexec Datがある場合、参照リンクが若い方からCHOPExecuteが実行される。

PythonスクリプトからのCHOP操作と、CHOP Executeの実行順番

  1. pythonスクリプトから、CHOPの値を取得するときにも、接続しているネットワークの処理(cook)が走る。
  2. よって、pythonスクリプトから、CHOPの値を操作してネットワーク経由で更新された値を即時取得することは可能
  3. 1.2.の性質はCHOP Referenceで接続していても同じ。
  4. しかしCHOPEcuteは、実行しているpythonスクリプトが完了した後に実行されるので、実行順序に注意が必要

コンポーネント内の関数実行順序

  1. Componentのextension codeのpythonスクリプトは、他のpythonから実行した場合、単純にpythonコードとして実行されるので、順序は期待どおり
  2. Custom ParametersでのPulseボタンへのpulse()経由で行うと、実行中のpythonコード完了後に、実行される。かつ、複数回pulse()をよんでいても実行回数は1回になるので、注意が必要。

すこし、スッキリしましたね。これで、良い年を迎える準備ができそうです。

29
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?