.. title:: Pythonで心理実験 - 例題27-2 例題27-2:Codeコンポーネントを使った実験をPavloviaに移植してみる ====================================================================================== .. note:: ここで取り上げた実験は以下のURLに置いてあります。動作するように修正した後の状態です。 - ``_ **A:** さて、だいたいこんなJavaScriptコードが出力されるのかを確認したところで、実際に作ってみるか。ゼロから作るのは面倒くさいので、「PsychoPy Builderで作る心理学実験」の第7章、Müller-Lyer錯視の実験をベースにしよう。ただし、第8章練習問題2のマウスで操作する改造を施した方をベースにする。 **B:** 今回はいきなり本題ですね。ずっと休んでいたくせにこのペースで更新とはいったいどういう風の吹き回しでしょうか。 **A:** それを話し出すとまたアレな方向になるので。最近このパターンの導入ばかりだからそういのはやめにしよう。 **B:** ははあ。ではこないだお土産にもらったうなぎパイの話でもします? **A:** なぜにうなぎパイ。ちょっと気になるけどアホな話をしている時間もないのでさっさと始めるぞ。まずは実験の概要。 .. figure:: img/27-2-01.png **B:** 微妙に変わっていますね。刺激の夾角が6種類から5種類に、繰り返し回数は5回に。そしてキーボードではなくマウスで反応。なんで変更したんですか? **A:** うむ。単にこのバージョンのpsyexpファイルが手元にあったからだ。 **B:** なんだ、がっくり。 **A:** あと、データファイルでプローブの位置が-200, 200ではわかりにくいので、left, rightという文字列で指定するようにしている。Codeコンポーネント内でleftなら-200、rightなら200に変換する。 **B:** まぁその方がわかりやすいですね。200って値が入っていた方が後で座標がすぐわかるというメリットがあるような気もしますが。 **A:** うむ。んでもってその時に何を思ったのか第7章では「ターゲットの位置」を条件ファイルで指定するようにしていたのに、なぜか「プローブの位置」を指定するように書いてしまった。なので、条件ファイルには以下の3列が入力されていることになる。 - probePos: プローブの位置を表す。left, rightの2種類。 - probeLen: プローブの初期長を表す。100, 300の2種類。 - angle: 夾角を表す。30, 60, 90, 120, 150の5種類。 **B:** ふむふむ。 **A:** んで、以下のようにtrialルーチンに刺激を描画するためのPolygonコンポーネントと現在の試行を表示するためのTextコンポーネント、そして反応計測に使うMouseコンポーネントを置いている。そして、Codeコンポーネントもこのtrialルーチンに置いてある。 .. figure:: img/27-2-02.png **B:** Codeコンポーネントのプロパティダイアログが表示されていますね。 **A:** Codeコンポーネントには以下のコードが入力してある。まず「Routine開始時」に入力してあるコード。 .. code-block:: Python if probePos == 'left': stimPos = 200 else: stimPos = -200 mouse.setPos((0,-300)) **B:** さっき言っていたleftなら200、rightなら-200ってやつですね…って、あれ? leftなのに200が代入されている。 **A:** stimPosはターゲット刺激の座標を表すので、プローブ刺激がleftならターゲット刺激は右側、つまりX座標は正でなければならない。 **B:** ややこしいなぁ。 **A:** 7章と合わせたらよかったんだがこれを書くための資料をそろえた時点で気づいたんでもうこのままいくことにした。行き当たりばったりが本コーナーのモットーだからな。 **B:** やれやれ。 **A:** あとは各試行の開始時にマウスカーソルが画面の中央下にくるようにsetPos()を使用している。これは特に問題ないだろう。続いて「フレーム毎」のコード。 .. code-block:: Python x,y = mouse.getRel() probeLen = min(400, max(50, probeLen+x)) **B:** あっさりしてますね。第7章では結構ごちゃごちゃ書いてあったような。 **A:** Mouseコンポーネントのおかげでルーチンの終了判定コードを書く必要がないからね。まず1行目はgetRel()でマウスカーソル移動量のX成分、Y成分を受け取っている。そして2行目。これはわかるな? **B:** 5章の練習問題ですね。max(50, probeLen+x)でprobeLenが50未満になるのを防ぐ。そしてさらにmin()をかぶせることで、400より大きくなるのを防ぐ。つまりprobeLenが50以上400以下になるように制限している。 **A:** すばらしい。満点。 **B:** ふふふ。もっと褒めていいんですよ。 **A:** んで最後に「Routine終了時」のコード。変更後のprobeLenを保存している。これが分からない人は「PsychoPy Builderで作る心理学実験」第7章の解説をよく読んでほしい。これでこの実験で使用しているコードはおしまい。 .. code-block:: Python trials.addData('response', probeLen) **B:** 案外少なかったですね。 **A:** だろ? このくらいシンプルなところから始めればすぐに完成するかな…と思ったら **それが甘かったわけだ** 。 **B:** な、なんか声に凄みがありますね。 **A:** うむ。久々にPsychoPy Builderを初めて学び出した頃の **悪夢** を味わったよ…。エラーメッセージがなぜ出ているのかわからなくて、何もかもが手探りだったあの日々が… ぐふっ **B:** AさんAさん、気を確かに。 **A:** はぁはぁ。いや、Builderをこれから学ぶ人は、最初はまさにこういう状態なのだろうな…と反省したよ。げふげふ。 **B:** とりあえずお茶でも飲んで。 **A:** うむ…… ふはぁ、すまんな。先へ進まなければ。この実験では **PsychoJSに未対応のコンポーネントが使用されていない** ので、CodeコンポーネントにJavaScriptのコードを追加すれば動くはずである。というわけで、Codeコンポーネントのプロパティダイアログを開いて、「コード」という項目をBothにする。すると以下のようにコード入力用のボックスが左右に分割されて、左側にPythonのコード、右側にJavaScriptのコードを入力できるようになる。そのままではコードが見にくいのでダイアログの横幅を広げるとよいだろう。 .. figure:: img/27-2-03.png **B:** 「コード」には他にPyとJSという項目がありますね。最初はPyでPythonのコードだけが表示されていたから、JSにするとJavaScriptのコードだけが表示される? **A:** その通り。入力ボックスが狭くて作業しにくいときはPy、JSに切りかえるといいだろう。両方のコードを見比べたり、片方を参考にしながらもう片方を入力する場合はBothが楽だ。今回はPythonのコードを参考にしながら作業するのでBothがいい。 **B:** ふむ。なるほど。 **A:** ではいくぞ。まず「Routine開始時」タブ。JavaScriptの文法についてはここでは解説しないので各自でなんとかしてほしい。 **B:** そんな無責任な。 .. code-block:: JavaScript if (probePos == 'left') stimPos = 200; else stimPos = -200; // mouse.setPos((0,-300)); **A:** if文の条件式が括弧で囲まれたことと、ifやelseの後ろにコロンがないこと、末尾にセミコロンがつくことだけしか違いがない。mouse.setPos()は **まだ実装されていない** ので泣く泣くコメントアウトした。 **B:** // がコメント行ですか? Pythonの # みたいなもんですかね。 **A:** その通り。続いて「フレーム毎」。getRel()は幸い実装済みなのでそのままいける。 .. code-block:: JavaScript xy = mouse.getRel(); probeLen = Math.min(400, Math.max(50, probeLen+xy[0])); **B:** 今回はいろいろ違いますね。ええと、どれからいこうかな。 **A:** 1行目から順番に指摘したまえ。 **B:** なら1行目。mouse.getRel()の戻り値をx, yじゃなくてxyで受けてますね。これは? **A:** Pythonにおいて戻り値がtupleの場合に、戻り値の要素数と同じ個数の変数をカンマ区切りで並べて受け取ることができる。元のコードはこのテクニックを使っていたのだが、JavaScriptでは普通にひとつの変数で受け取っている。これは例題27-1で読んだコードがそうなっているのでそれに従っている。 **B:** なるほど。2行目ですが…まずxy[0]ですね。これはPythonと同じようにxyの最初の要素と考えればいいですか? **A:** Yes. **B:** あ、そうなんだ。意外と書き換えは楽そうだなぁ。あとminとmaxの前にMath.とついていますね? **A:** これはminやmaxがJavaScriptではそのまま使えなくてMathという組み込みオブジェクトのメソッドとして利用する。 **B:** それでMath.min()と書く、と。ちょっと面倒くさそうな要素が出てきた。Math.って付けなかったらどうなるんですか? **A:** 実行時にmin()やmax()なんて関数は知らんと怒られる。 **B:** なるほど。 **A:** 最後に「Routine終了時」タブ。ここはセミコロンを足すだけでいける。 .. code-block:: JavaScript trials.addData('response', probeLen); **B:** おおお、素晴らしい。 **A:** では実行してみようか。Pavloviaにプロジェクトを作成してローカルPCに同期し、編集したpsyexpファイルと条件ファイルをぶち込む。そして同期すれば自動的にJavaScriptファイルが出力されてPavloviaにアップロードされる。 **B:** ちょ、それで読者に通じると思っとるんですか! **A:** ここの操作の解説はまたいずれ…。ほんと、解説しなきゃならんことが多すぎるのよ。 **B:** ブラウザでこのアドレスにアクセスするんですね? んじゃFireFoxで開いてみます。…おお、実験情報ダイアログっぽいのが出てきた! .. figure:: img/27-2-04.png **A:** participantに適当な名前を入力してOKして。 **B:** おおお!胸が高鳴るぜ! …って、あれ? エラーになりましたが。 .. figure:: img/27-2-05.png **A:** あちゃー、deg2rad()がないか。そりゃそうか。RefenceError: deg2rad is not defined. 文字通りだね。 **B:** えっと、deg2rad()なんてCodeコンポーネントにありませんでしたが…って、あぁ! Polygonコンポーネント! **A:** そう。矢羽の座標を計算するところでdeg2rad()を使っていたね。 .. figure:: img/27-2-06.png **B:** えー、Codeコンポーネントだけ考えりゃいいんじゃないんですか?! **A:** そう、ここが見通しが甘かった点のひとつめ。 **Pythonの式はCodeコンポーネント以外にもあちこちに入力されている可能性があるんだよな。ここにJavaScriptと互換性がない式が入力されていたら動作しない** 。これは実験によってはかなり修正が大変。 **B:** むむむ…。そういえば、cosも出てきてますけどcosはnot definedじゃないんですか? **A:** 出力されているJavaScriptのコードを確認すればわかるが、cosやsinはBuilderが自動的にMath.cos、Math.sinに書き換えてくれている。 **Builderが変換方法を知っている式については自動的に変換してくれるが、そうでない式はそのままになるみたいだね** 。まあ合理的な動作ではある。 **B:** どうするんですか? いきなり頓挫ですか? **A:** どうするかな…。ひとつはPolygonコンポーネントのプロパティに入力されたそれぞれの式をdeg2rad()を使わないように書き直すこと。これは **超面倒くさい** のでやりたくない。 **B:** Aさんらしいですな。 **A:** もうひとつはdeg2rad()を定義してしまうこと。deg2rad()は度をラジアンに変換するだけだから、単に180をかけてpiで割るだけでいい。javaScriptで書くとこうなる。 .. code-block:: JavaScript function deg2rad(deg) { return deg * (Math.PI/180); } **B:** おお、意外と簡単。 **A:** 問題はこれをどこに書くかだが…。BuilderでPythonのコードを追加する時の発想で行くと、実験の最初で定義しておくのが確実だ。だからCodeコンポーネントの「実験開始時」に追加したくなる。だが、それではうまくいかない。 **B:** 試したんですか? **A:** すでに試してある。やはりnot definedだと言われる。 **B:** Oh... **A:** なぜそうなるかというと、出力されたJavaScriptを見ればわかる。 **「実験開始時」にJavaScriptのコードを追加すると、experimentInit()関数の内部にそのコードが追加される** 。ということは、「実験開始時」にdeg2rad()の定義を書くと、experimentInit()関数内で有効となる。これに対して、実際にdeg2rad()が必要となるのは各試行の開始時だ。 **実際にdeg2rad()を使う場所から参照できる位置で定義しなければならない** 。 **B:** 一気に難易度が上昇したな…。どこで定義すればいいんです? **A:** 結論から言うと、trialRoutineBegin()である。trialルーチンの開始時にangleやprobePosの値に従ってPolygonコンポーネントの位置を決定するからだ。ということは、Codeコンポーネントのどこにコードを挿入すればいい? **B:** trialRoutineBegin()という名前からして、「Routine開始時」ですか? **A:** その通り。したがって「Routine開始時」は以下のようになる。 .. code-block:: JavaScript :emphasize-lines: 1-3 function deg2rad(deg) { return deg * (Math.PI/180); } if (probePos == 'left') stimPos = 200; else stimPos = -200; // mouse.setPos((0,-300)); **B:** 先頭に追加していますね。先頭じゃないといけないんですか? **A:** 今回についてはべつにif-else文の後でも問題ない。それよりも、Codeコンポーネント自体がPolygonコンポーネントより先に実行されることが重要である。そうすればPolygonコンポーネントの位置を決定する時にはdeg2rad()の定義が終わっている。 **B:** なるほど…これは難しいな。 **A:** 今まで通りPythonで実験する時にも同様の問題はあった。難しく見えるだけで何も変わらない。 **B:** だから **今までも難しかった** んですよ!! まったく、これだから出来る人は。Builder初心者のみなさんはきっとぼくに同意してくれるはず。 **A:** まあこれでdeg2rad()の問題は解決。修正したら同期して実行してみよう。 **B:** ええと、同期して、ブラウザで実験を開き直して…。 またエラーですが。 .. figure:: img/27-2-07.png **A:** 出た…。こいつだよ。こいつのせいで私は飯を食い損ねたのだ。貴重な昼飯の時間が…! **B:** Aさんの時間が食われたというわけですね。 **A:** (無視して) unable to convert: NaN to a number.っていうんだから、NaNを数値に変換できないっていうことだろ? 何かを数値に変換するところなんかあったっけ? とコードを眺めてみてもそれっぽいところがない。じっくりコードを眺めているうちに2つ気づいた点がある。 **B:** 2つ? **A:** うむ。まずTextコンポーネント。現在何試行目かを画面上部に表示するためにこういう式を入力していたのだが、%演算子による埋め込みはJavaScriptにはない文法だからこれは実行できない。で、試しにJavaScriptでこれを評価させてみるとNaNになるんだよな、これが。 .. code-block:: Python 'Trial %d' % (trials.thisN+1) **B:** おお、NaNが出てきましたね。 **A:** それで「こいつが怪しいのでは?」と勘ぐってこの式をあれこれいじってみたのだが、どうにもエラーが解消されない。こいつに引っかかってしまったのが敗因のひとつだ。 **B:** ふむふむ。 **A:** ちなみにこの式は別の厄介な問題をもたらす。まあそれは後で触れるとして、埒が明かないので出力されたJavaScriptをじっくり読んでみたのだな。そうしたらなんとexperimentInit()でPolygonコンポーネントを初期化するときにunitsがpixじゃなきゃいけないのにheightになっているではないか! .. code-block:: Python :emphasize-lines: 3 probe = new visual.ShapeStim ({ win: psychoJS.window, name: 'probe', units: 'height', vertices: [[-[1.0, 1.0][0]/2.0, 0], [+[1.0, 1.0][0]/2.0, 0]], ori: 0, pos: [0, 0], lineWidth: 1, lineColor: new util.Color([1, 1, 1]), fillColor: new util.Color([1, 1, 1]), opacity: 1, depth: -2, interpolate: true, }); **B:** あら、本当だ。 **A:** んで慌ててPsychoPyのログを見たら以下のようなwarningが。なんと、現時点では'from exp settings'は使えなくて勝手にheightに変換されると! .. code-block:: none 13624.9536 WARNING 'from exp settings' units for your 'probe' shape is not currently supported for PsychoJS: switching units to 'height'. **B:** from exp settings? **A:** ああ、日本語環境のBuilderでは「実験の設定に従う」と表示される。単位の設定。 **B:** なるほど。 **A:** heightに変換されるということは、今のままだとNaNの問題が解決してもまともに表示されないってことだ。だからこの問題は解決しないといけない。まあいくつか方法があるが、考えてみれば **オンライン実験だと参加者がどんな解像度のディスプレイで実験するかわからないので、ここはheightに統一するのがよい** のだろうと考えた。そうしたら条件ファイルやCodeコンポーネントに200とか300とか入力してある値を全部変更しないといけない。結局、全ての値を1000で割ることにした。200pixは0.2、50pixは0.05という具合に。 **B:** それはどういう基準で決めたんですか? **A:** 暗算しやすいだろ。それだけ。もともとデモのような実験だから、それほど刺激の大きさにこだわりはない。CodeコンポーネントだけでなくPolygonコンポーネントの位置や大きさ、Textコンポーネントのフォントの高さなども修正しないといけないので忘れないように。 **B:** 結構修正箇所多いですよね。お疲れ様です。 **A:** ん。そしてまたNaNの問題に戻るわけだが…。 **B:** 結局どうなったんですか? **A:** trailルーチンから少しずつコンポーネントを無効にして行って、どこまで無効にすれば動作するかを調べた。するとprobeが有効になっているとダメで、それ以外のコンポーネントは問題がないことがわかった。 **B:** ほうほう。それで。 **A:** あ、ちなみにコンポーネントを無効にするというのは、PsychoPy 3.0だか3.1だったかで有効になった「テスト」という機能を利用している。コンポーネントのプロパティダイアログを表示すると「テスト」というタブが表示されるようになっていて、ここにある「コンポーネントの無効化」という項目にチェックしておくと、コンポーネントは削除されないが実行時にはまったく存在しないかのように動作する。今回のようなデバッグの時に、一時的にコンポーネントを働かないようにしたい場合に役立つ。今までならコンポーネントを一時的に削除しないといけなくて、削除しちゃうと復活させるときにまたパラメータを入力しないといけないからな。 .. figure:: img/27-2-08.png **B:** へえ。これは便利。 **A:** さて、probeが怪しいということであれこれ調べてみたんだが、どうもtrialRoutineBegin()に入った時点でprobeLenがundefinedになっているようなんだ。値がundefinedなので、プローブの長さを設定しようとした時にNaNとなり、そのNaNを数値として扱おうとしたのでunable to convert: NaN to a numberとなる。 **B:** ??? **A:** probeLenの値は条件ファイルからTrialHandlerを通じて設定されるはずで、現にangleやprobePosは設定されている。ではprobeLenはなにがいけないかというと、**Codeコンポーネント内で値が変更される変数として使用されている** 。ちょっとまだ私も完全に理解できていない部分があるのだが、 **そのような変数は条件ファイルの列名(パラメータ名)として使えない** のだ。 **B:** えーと、全然わかっていないのですが、要するにprobeLenは条件ファイルの列名として使えないということ? **A:** そう。だからprobeLenの値はCodeコンポーネント内でいじらないようにして、別の名前の変数を使って現在のプローブ長を保持することにすれば問題は解決する。あるいは、probeLenで現在のプローブ長を保持するのはそのままにしておいて、条件ファイルの列名を変更する。 **B:** どっちが楽そうかというと、条件ファイルの列名を変更する方が楽そうですね… **A:** その通り。というわけで、条件ファイルを修正して probeLen をinitialProbeLen とすることにしよう。そして「Routine開始時」にinitialProbeLenの値をprobeLenに代入する。そうすれば以後のコードは修正せずに使えるはずだ。結果として「Routine開始時」のJavaScriptコードはこうなる。 .. code-block:: JavaScript :emphasize-lines: 10 function deg2rad(deg) { return deg * (Math.PI/180); } if (probePos == 'left') stimPos = 0.2; else stimPos = -0.2; probeLen = initialProbeLen // mouse.setPos(0,0.3); **B:** この1行を追加するだけでいいんですね? **A:** 条件ファイルの修正を忘れずにな。さらに、この実験をPythonでも実行するのなら、Python側のコードも変更しておく必要がある。 .. code-block:: JavaScript :emphasize-lines: 6 if probePos == 'left': stimPos = 0.2 else: stimPos = -0.2 probeLen = initialProbeLen mouse.setPos((0,-0.3)) **B:** あ、そうか。面倒くさい…。 **A:** 最初から注意しておけばなんということはない。まあ考えてみれば各試行の条件として渡された値をじゃんじゃん変更してしまっていたのだから、 **元のコードの行儀が悪かった** ということもできる。元コードを書いたSには反省してもらわんといかんな。 **B:** なんと、「Sさんが悪い」オチ。 **A:** さて、これでNaNの問題は解消された。さて、実行してみたまえ! **B:** …おおおぉ …ぉおお? なんだかヘンな刺激が表示されましたよ? .. figure:: img/27-2-09.png **A:** そーだよ、そーなんだよ。これで解決!と思って実行したらコレなんだもの。笑うしかないわな、は、ははは。 **B:** だいぶ燃え尽きかけていますな…。 **A:** これはもうシンプルな理由で、**PythonとJavaScriptで刺激の回転方向が逆** なのだ。それだけ。「Pythonで心理実験」の第7章で矢羽の座標を表す式を導くところに書いてあったように、これは本来Pythonで実行したときの動作の方がおかしい。上と右が正の座標系を使っているのだから、回転は実験参加者から見て反時計回りが正の回転方向であるべきなのだが、従来からPsychoPyでは時計回りが正となっていた。なのに、なぜか **実験をJavaScriptに出力すると、正の回転が反時計回りになっとるやないか! ふがー! なめとんか!** **B:** Aさん落ち着いて。 **A:** これは将来的に従来の回転方向に統一されると思う、ていうか統一されてほしいんだけど、とりあえず今のところは現時点の仕様に合わせて書くしかない。まあ回転方向が逆なんやからJavaScriptの時はangleに-1をかけたらええやろ、と思ってたらそれではうまくいかない。 **B:** へ? なんで? **A:** 簡単な計算なんだが、図がないとわかりにくな…こんなもんでどうだ。 .. figure:: img/27-2-10.png **B:** ええと?? …あ、そうか。矢羽の中心座標も回転角からsin()を使って計算しているんだから、符号が反転して上側の羽が下側になってしまうんですね。結局描かれる図形は全然変わらない。 **A:** そう。計算で座標を求めているのが裏目に出ているという…。 **B:** あらら。 **A:** もともと従来のPsychoPy(Python)では回転方向がおかしいからY座標の符号を反転させていたので、JavaScriptでは正しい符号に戻してやらないといけない。でも、式はPolygonコンポーネントに入力してしまっているので、Codeコンポーネントからはどうしようもない。というわけで、なんとかCodeコンポーネントだけの修正で済ませようとしてきたんだけど、ここで白旗。Polygonコンポーネントを修正せざるを得ない。 **B:** ちーん なんまいだなんまいだー **A:** で、どうせ手を入れるならということでごっそりやる。そもそもPolygonコンポーネントの[位置 [x,y] $]にごちゃごちゃ式が書いてあるとあとで読み難くて仕方がないので、がばっと変数にしてしまう。 arrowTRのX座標はtr_x、Y座標はtr_yといった具合だ。そして、Codeコンポーネントの「Routine開始前」のPythonの方に以下のように式を追加する。 .. code-block:: Python :emphasize-lines: 8-15 if probePos == 'left': stimPos = 0.2 else: stimPos = -0.2 probeLen = initialProbeLen tr_x = stimPos+0.1-0.025*cos(deg2rad(angle)) tr_y = 0.025*sin(deg2rad(angle)) br_x = stimPos+0.1-0.025*cos(deg2rad(-angle)) br_y = 0.025*sin(deg2rad(-angle)) tl_x = stimPos-0.1+0.025*cos(deg2rad(-angle)) tl_y = -0.025*sin(deg2rad(-angle)) bl_x = stimPos-0.1+0.025*cos(deg2rad(angle)) bl_y = -0.025*sin(deg2rad(angle)) mouse.setPos((0,-0.3)) **B:** おおう。一気に長くなりましたね。 **A:** 各Polygonコンポーネントに入力済みの式をコピペして編集すれば間違えにくいだろう。そしてこの式をJavaScript側にコピーして、Y座標の符号を変えてやる。あとsin()やcos()にMath.をつけるのは自動的にやってくれなくなるので手作業で追加しなければいけない。もちろんこれはX座標、Y座標の両方しなければいけない。 .. code-block:: JavaScript :emphasize-lines: 13,15,17,19 function deg2rad(deg) { return deg * (Math.PI/180); } if (probePos == 'left') stimPos = 0.2; else stimPos = -0.2; probeLen = initialProbeLen tr_x = stimPos+0.1-0.025*Math.cos(deg2rad(angle)) tr_y = -0.025*Math.sin(deg2rad(angle)) br_x = stimPos+0.1-0.025*Math.cos(deg2rad(-angle)) br_y = -0.025*Math.sin(deg2rad(-angle)) tl_x = stimPos-0.1+0.025*Math.cos(deg2rad(-angle)) tl_y = 0.025*Math.sin(deg2rad(-angle)) bl_x = stimPos-0.1+0.025*Math.cos(deg2rad(angle)) bl_y = 0.025*Math.sin(deg2rad(angle)) // mouse.setPos(0,0.3); **B:** むはー、面倒くさい! **A:** これでようやく完成、通常の錯視図形が表示されるはずである。 **B:** すごい!ちゃんと表示された! **A:** まったく、やれやれだぜといったところだが、まだ解決していない問題がある。B君、覚えているか? **B:** へっ? なんでしたっけ? **A:** 現在何試行目かを表示する話だよ。$'Trial %d' % (trials.thisN+1)がJavaScriptの式ではないって話。 **B:** ああ、とっくに忘れていました。 **A:** 実は先ほどの錯視図形がおかしくなってしまっているスクリーンショット、21という数値が中央やや上に表示されているだろう? これはとりあえずTextコンポーネントで表示する文字列を $trials.thisN+1 というだけの式にした時の結果なのだが、最初から最後まで21のままなのだ。 **B:** えっ、それはどういう。21って何の数字だろう? **A:** 実はこのスクリーンショットを撮ったときには動作テストのために繰り返し回数を変更していて、20回が総試行数だ。つまりPythonで実行するとそのループにおける現在の繰り返し回数を保持しているthisNが総繰り返し回数になってしまっているのだ。だからthisNを前提としたコードはきっと正常に動作しない。 **B:** えー。なんですかそれ。そんなのありですか。 **A:** まぁあり得ないわな。あっさりthisNが存在しなくてエラーになる方がありがたいくらいだ。probeLenの問題はまあこちらのコードが行儀悪かったかなと納得できる話だけど、回転方向の不一致やthisNの問題などはちょっと許せないと思う。今後修正されるのか、もうJavaScript版はこの仕様でいくのかで対策が変わってくるので、早く方針を明らかにしてほしい。 **B:** …。 **A:** まあ、文句もいろいろあるが、これからの発展を見守っていきたいすばらしい機能だと思う。PythonとJavaScriptの両方で動かそうとか思わなければ手間はかなり減る。あとはあまり各コンポーネントのプロパティに複雑な式を書かず、Codeコンポーネントで計算を行って結果を変数に代入し、その値をプロパティで使用するようにすればいいと思う。あと、条件ファイルで定義した変数に対してCodeコンポーネントで代入しないこと。最初から注意していれば大した問題ではない。 **B:** Aさんは今後この機能を使いそうですか。 **A:** さあ、どうかなあ。授業とかでは便利だろうな。そうそう、もうひとつ追加しておくと、現時点(3.1.2)でJavaScriptへの出力に対応しているコンポーネントは基本的なものに限られている。Image, Polygon, Movie, Sound, Mouse, Keyboard, Slider, TextそしてCode。こんなもんかな。 **B:** えっ、じゃあかなり限られていますね? **A:** まあとはいってもパラレルポートとかキーボックスとかはオンライン実験で使いようがないしな。RatingScaleはSliderで充分代わりになるだろう。GratingやNoiseが使えないのはこの手の刺激を使う実験をしている人には厳しいな。でも、画像ファイルを刺激として提示してキーボードやマウスで反応をとるような、オーソドックスな実験なら大丈夫だろう。 **B:** うーん、そうなのかな、そうなのか。 **A:** ま、今日のところはこんなもんか。とりあえずやりたいことはやったので、後は気が向いたらPavloviaへの同期の手順とか、そういうのをするかもしれない。まあその辺はSが「PsychoPy Builderで作る心理学実験」に書いちゃったらこっちではやらないだろう。 **B:** あー。今度こそまた出番は数年後かなあ。 **A:** 先のことはわからんよ。まあとにかく、みなさんごきげんよう。 **B:** さよーならー