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

## はじめに

[TouchDesigner Advent Calendar 2019](https://qiita.com/advent-calendar/2019/touchdesigner) 11日目の記事です。

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

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

実行環境
- macOS 10.14.2
- TouchDesigner  2019.17550

## 検証方法 
### pythonコード内実行順序で確認

![](https://paper-attachments.dropbox.com/s_C59222FC9B71FF49EF0098F0518AC968B0A17B960C9C51AFCDB5816C78D197F7_1576067164965_image.png)


```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/58633d5e-20d0-2213-8dcd-2a81c910e3f5.png)


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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/7d115764-5c45-3531-a1fa-5f0e9d495055.png)


```python: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
```

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/051fbe40-0aa6-ab3e-e5bc-7aa7a3454302.png)

`null2` → `null99` , `chopexec2`→ `chopexec99`
のように、名前を変更する。

```
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/deada18a-19a2-e7bf-8b92-d2e3aa6741d0.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_x` と `null*`の接続を切った後に、下記の順番で再接続。実行順番は変わるか？図中のネットワークに記載した番号はネットワークの生成順序を示す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/709fa509-af6e-a523-533d-502a0b34553d.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/81ffde70-1cf5-c85d-0fa5-873a51323f38.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/bd2b1d07-fcc8-4a8d-5870-c9c5028a802d.png)

ここでもおなじく、**つなげた順番で若い順で実行される**。上記で、`chopexec2_created_2`,`chopexec2_created_4`と`chopexec2_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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/1b4cd628-3f9c-60d8-7827-bfe6a24e460a.png)

```
python >>> 
chop exec - onOffToOn      3 1575997858135.001
chop exec - onOffToOn      2 1575997858135.2651
chop exec - onOffToOn      1 1575997858135.4722
```
このように、後ろから実行されているようだが、これは、接続の終端からだろうか？それとも生成順であろうか。そこで次のネットワークでは、同じだか生成タイミングと接続順序が異なる状況で`CHOP Execute`を実行させる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/1916af23-0eb8-4b0e-3bd8-02067a8e4b35.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/3d7b7320-22a3-dc21-054d-e27ac8ec2ca0.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/38eac344-8135-2152-3d8c-7b848e4bfc85.png)

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

```python:chopexec1
op('constant2').par.value0 = 1
```

```python:chopexec2
op('constant4').par.value0 = 1
op('constant3').par.value0 = 1
```

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

``` python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/c8c22a7f-e4c3-6f88-7f06-91d214ec1a01.png)

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

```python: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)

```


``` python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/b18a3333-2191-c546-3ce7-5ee3d15df743.png)

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

``` python: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の関係
先程の終端にあった`nullChop`に`onValueChange`で関数を実行するようにします。


``` python:chopexec
import time
def onValueChange(channel, sampleIndex, val, prev):
    print("chop exec - read" , me.digits ,1000 * time.time())
    return
```
そこで上記と同じように、`constantCHOP`と`nullCHOP`を接続して、
`constant1`の値書き換え、`null1`から値読み出し、
`constant2`の値書き換え、`null2`から値読み出し、
`constant3`の値書き換え、`null3`から値読み出し
を行います。上記の実験から、`null1`読み出しを行った時点で、`constant1`-`null1`の更新部分は実行されているので、そのタイミングで、`onValueChange()`も実行されているのでは、と期待してしまいそうになりますが。。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/d0fd5706-a86b-f0a1-cf31-f0ab41f714fa.png)

``` textport
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_3`の`onValueChange()`が順次実行されています。

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

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

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

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

``` python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/a603075e-7c71-a70b-50cb-8f11fb10403d.png)


```textport
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`を参照している、`chopexec`は**1度しか実行されていない**。

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

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

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

上記の例からも、`chopexec`の実行タイミングは、参照している`CHOP`のcookタイミングそのものではないことがわかります。よって、`chopexec`等を用いて値を更新させている場合、例えば
> `constant1`から`null1`に受け取った値で、`onChangeValue`時に隣の`constant_read`の値を更新する。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/e36bb812-7010-a9b0-14e0-bbdf9bba5cab.png)


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

``` python: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)
```

```textport
write  9 read 6.0 1576061048494.0022 ms
chop exec 1576061048494.885 ms
```

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/8edaec66-1e99-cea5-8ba0-4bc7843d3a04.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/f45a3f75-d184-9f58-46b3-f954e788997f.png)

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



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

システムが複雑になって来ると、`container COMP`や`base 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/b76def64-eccf-22cf-778c-76987302bf10.png)

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


``` python: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つ、`Script`と`parexec1`がある。`Script`は、ComponentでのExtension Codeを追加して作成。`parexec1`は、`me.parent()`を対象にして、`onPulse`関数で、`container1`の`Pulse`ボタンが押されたときに実行するスクリプトを記述。それぞれ、実行時の時間も出力されるようにしておく。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70810/33c6ee59-e4f4-e05c-6320-e32806da39ea.png)

`Script text DAT`内スクリプト

```python: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`内スクリプト

```python:parexec1

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

結果のtextport

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

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

``` python: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度実行した場合には、、、
(これまでの流れから、なんとなく予感はしますが。。）

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

結果：

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

そうです。`parexec1`の`onPulse()`関数は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回になるので、注意が必要。


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