Matlabからpythonモジュールを呼び出す


<!-- 1 Intro -------------------------------------------->

# Introduction

## Motivation

1. Matlab + Psychtoolbox[ref. [^Ref_psych]]で心理実験を構築する
1. 実験パラダイムの中で、pythonによる処理を呼び出したい
    e.g.,　音声認識（Google cloud platform Speech-to_Text API [ref.[^Ref_spchText]])

## Background

- Matlabから外部言語処理を呼び出す方法は色々とある[ref. [^Ref_callExtLang]]。
    - C/C++ (or Fortran)で記述される処理をMatlab Executable (MEX) 関数[ref. [^Ref_MEX]]としてビルドすることでmatlabとシームレスに統合することが可能となる
        - 最新のC/C++の機能やオブジェクト指向の設計の組み込み
        - 高速化
        - スレッドセーフなマルチスレッド処理
        - アウトプロセスでC++アプリケーションの実行
    - Pythonで記述されたアプリケーションも呼び出すことが可能である[ref. [^Ref_pyCall]]。
        - Pythonでは、豊富なtoolboxから機械学習や音声認識などのアプリケーションが作成しやすい
        - __計算などの高速化を図りたい場合は、Cythonを使ってC/C++に変換してからMEX関数化可能？（現在検討中）__
- 筆者が実装しようとしている音声認識は、Pythonで実装した
    - 心理実験のタイムラインの都合からストリーミング音声のリアルタイム文字起こしが必要だった
    - C/C++では、音声ファイルのストリーミング文字起こしはできても、ストリーミング音声入力に対応させられなかった（力不足）
        - Sample codes： googleapis/python-speech - GitHub [ref. [^Ref_spchTextSample]]

## Goal

**Matlabからpythonで作成したアプリケーションを呼び出す**

## 環境

- Machines
    - macOS Monterey (apple silicon)
    - Windows10
- Python
    - python3.9.13
        - Mac: Python for x86_64 installed via rosetta
- Editor for Python
    - VScode
- Matlab
    - 2021b


<!-- 2 compatibility ---------------------------------------------------->

# Pythonの互換性

## Matlab
MatlabでPythonを使用するためのシステム構成は公式リファレンスを参照のこと[ref. [^Ref_systemCons]]。以下に一部抜粋して記す。
>MATLAB® で Python® モジュールを呼び出すには、サポートされているバージョンの参照実装 (CPython) がシステムにインストールされていなければなりません。https://www.python.org/download などにある配布版をインストールしてください。MATLAB では、Microsoft® ストアからインストールした CPython はサポートしていません。MATLAB は Version 2.7、3.7、3.8、および 3.9 をサポートしています。詳細については、Versions of Python Supported by MATLAB Products by Release を参照してください。Linux® または Mac プラットフォームを使用している場合は、既に Python がインストールされています。Windows® を使用している場合は、Python がインストールされていなければ、配布版をインストールする必要があります。詳細は、サポートされている Python 実装のインストールを参照してください。

Python2はpython2.7のみ、python3はpython3.7以降をサポートしていることに注意されたし。各自の環境によっては、`venv`による仮想環境を用意して環境分けしておいた方がよい。例えば、音声処理のために`PyAudio`ライブラリを使いたい場合、Python3.7以降が正式サポートされていないため、1)非公式ながら公開してくれているパッケージをインストール（win環境のみ）、2)python2.7環境を作る、3)Python3.7以降でなんとかPyAudioをインストールする、といった方法を検討する必要がある。


## Mac

Apple siliconプロセッサ搭載のMacでは、MATLABはRosetta2環境で実行される（R2020b Update3およびそれ以降のリリース）[ref. [^Ref_supportApple]]。Matlab2022aではnative platformのベータ版が公開されているっぽいが、詳細は不明。MatlabがRosetta2環境で実行されるということは、**Pythonもintel cpu用のものを用意する必要がある**。Arm版とIntel版でpythonを切り替える方法については、別途referenceを参照のこと[ref. [^brew_x86_64] [^python_x86_64]]。


<!-- 3 Matlab setting ---------------------------------------------------->

# Matlabの設定

Python environmentの公式ドキュメントを確認しておくと良い[ref. [^Ref_pythonEnv]]。

## 処理系の指定

Pythonインタープリタの規定の実行モードを`pyenv`関数によって変更することができる。この変更は複数のmatlabセッション間で維持される。

```Matlab: Matlab command window
pe = pyenv('Version', 'python処理系（実行可能ファイル）への絶対path')
```

仮想環境`/hoge/piyo/.venv/pyIntel3x/bin/python`の処理系を`pyenv`に引き渡して指定した場合、PythonEnvironment のプロパティは以下のように表示される。
```
          Version: "3.9"
       Executable: "/hoge/piyo/.venv/pyIntel3x/bin/python"
          Library: "************************************************"
             Home: "************************************************"
           Status: NotLoaded
    ExecutionMode: InProcess
```

Executableが適切に読み込めれば、LibraryやHomeは自動的に読み取って設定してくれるはずである。Python処理系が適切に設定されていれば、基本的には仮想環境下のライブラリを見に行ってくれるので問題なくpythonプログラムを実行できるはずである。他に認証キーのpath設定が必要だったりする場合は、terminalでpath設定や仮想環境のactivateしてからmatlabをterminalで呼び出すとより安心である。上手くいかない場合は、terminal上で当該pythonプログラムが実行できるのを確認した上で、そのterminalから`matlab`コマンドでMatlaを立ち上げてテストすると良い。

### 各環境のpath

.venvディレクトリ下に仮想環境を構築した場合、matlabに引き渡すpython処理系のpathは以下になる。

| OS | path 　　　　　　|
|:--|:--|
| mac |  \hoge\.venv\環境名\bin\python |
| windows10 | \hoge\.venv\環境名\Scripts\python.exe　|

### はまった点

Mac環境で、arm版とintel版の環境を作っていたので、pathはどれを指定すれば良いのか混乱してしまっていた。arm版pythonをデフォにしていたので、`python`または`python3`で実行できる一方、intel版を実行したい場合はコマンドを`python3x`に設定していた。しかしながら仮想環境をactivateした場合、その環境での処理は`python`コマンドで行うことになる。それを失念しており、matlabの環境設定時に引き渡すpathを、`which python3x`の出力結果（インストール先）にしてしまっていた。正くは、**仮想環境をactivateしている状況下での`which python`コマンドの出力結果**を引き渡す必要がある。なお、windows環境ならば`which`コマンドの代わりに、powershellならば`where.exe`、cmdならば`where`を使えばよい。

## ExecutionMode

Python スクリプトを MATLAB と同じプロセスで実行するか（InProcess）、または別プロセスとして実行するか（OutOfProcess）のモード選択が可能である。一般的には、`OutOfProcess`であればライブラリの競合なども生じないため、安定して動作する。パフォーマンスを重要視する場合には`InProcess`が使われる一方で、安全に実行するならば`OutOfProcess`を使うという住み分けで良い。Matlabでも必要なサードパーティライブラリの異なるバージョンを必要とするpythonライブラリの使用、ワークフローのデバッグなどの場合には`OutOgProcess`を選択する。アウトプロセスだと呼び出しにオーバーヘッドが伴い、これによりパフォーマンスに影響を及ぼす場合がある。このことから、pythonの処理内容を踏まえて検討すべき点である。


モードの切り替え、例えば`OutOfProcess`への切り替えは以下のコマンドで実行できる。
```Matlab:matlab command window
pyenv('ExecutionMode','OutOfProcess')
```

### Pythonのコンパイル

Pythonを実行するまでの時間を少しでも早くできると嬉しい。Python単体ならば`compileall`[ref. [^Ref_compile]]によってライブラリのバイトコンパイルができるとのこと。~~試してみる価値はあるが、まだ検証はしていない~~。Matlab側が勝手にコンパイルしてくれているようで、`hoge.py`をコンパイルした`./__pycache__/hoge.pyc`が自動的にできていますね。

## Versionの切り替え
versionを変更する際には、PythonがInProcessで読み込まれている場合は、MATLABを再起動し、新しいバージョン情報を指定してpyenvを実行する。一方で、OutOfProcessモードで読み込まれている場合は、`terminate(pyenv)`コマンドによってpythonインタープリターに関連づけられているプロセスを修了してから、新しいバージョン情報を指定してpyenvを実行すれば良い。

<!-- 4 How to call python ---------------------------------------------------->

# pythonの呼び出し

Pythonの関数/モジュール/パッケージを呼び出す方法は３つある[ref. [^Ref_pyCall]]。

1. `py.`接頭辞をモジュールの名前に追加して実行 [ref. [^ Ref_pyCallDirect]]
    - Pythoモジュールに直接アクセス
1. pyrun関数の利用 [ref. [^Ref_pyrun]]
    - Pythonスクリプトの実行
1. pyrunfile関数の利用 [ref. [^Ref_pyrunfile]]
    - Pythonスクリプトファイルの実行

呼び出し方法の詳細については、各自正式リファレンスページに譲る。下記では、自分のための覚書と所感を記す程度に止める。

## py.接頭辞

pythonライブラリにアクセスするために利用される方法である。Pythonで利用されるlist関数などを呼び出したいときには、`py.list({'This','is a','list'})`のようにして実行が可能となる。モジュール内で定義される関数を呼び出す場合は階層的に`py.module.func`のようにすれば良い。例えばtextwrapモジュールのwrap関数を使いたいのであれば、`py.textwrap.wrap('This is a string')`のようにして実行する。これは自作モジュールでも同様で[ref. [^ Ref_orgModule]]、自作`hoge.py`モジュールの中で`def piyo():`のように定義される関数を呼び出すには`py.hoge.piyo`となる。シンプルにpythonのモジュールないし関数にアクセスしたいときに使うため、matlabの構文に強い縛りを受ける形にはなる。

## pyrun

PythonインタープリターのPythonステートメントをMATLABコマンドプロンプトから実行したいときに利用する。例えば、Pythonワークスペースに変数aとbを用意して値の和を表示するには、`pyrun(["a = 3", "b = 4", "print(a + b)"])`とすれば良い。MATLABからデータをコマンドに渡して、結果を MATLABデータとして返すこともできます。例えばmatlabのワークスペースに`d1 = 5`, `d2 = 10`として変数を作成していた場合、`res = pyrun("x = y*z","x",y=d1,z=d2)`のようにmatlab変数を引き渡すことで`res=50`がmatlabのワークスペースに格納される。Python likeなステートメントを組み込んでで開発できます、という感じである。


## pyrunfile

関数pyrunfileを使用してPythonワークスペースに作成された変数は永続的ではないため、再利用ができない。Matlabで関数を呼び出すときの感覚に一番近い利用方法である。WorkSpaceを汚したくなければ基本的に`pyrunfile`で良い。Python的にはモジュール実行という印象である。py.接頭辞をつけてモジュール内のmain関数にアクセスしても同様である。

### 入力引数

Matlabで呼び出すときはバックグラウンド処理させるので余計な描画は省きたいときや、デバッグモードをオプションとして組み込みたいときなど、python関数にコマンドライン引数を渡したい状況が出てくる場合がある。Pythonでは、`argparse`[ref. [^ Ref_argparse]]モジュールをimportすれば実装は簡単にできる。ではMatlabで呼び出すときはどのように入力引数を渡せば良いだろうか？その場合は`pyrunfile`を使うこととなる。




<!-- 6 How to debug ---------------------------------------------------->

# 動作確認

上手くいかない場合は、こちらの正式リファレンスを見て１つずつ確認していくと良い[ref. [^Ref_pyDebug]]。
大体の原因はpath設定が環境に合っていなかったり、モジュール名が競合していたり、タイポだったりするように思える。

DOSコマンド[ref. [^Ref_dos]]でpythonを実行してみるのも良い手ではある。
`dos('python hoge.py')`と`dos('python処理系の絶対path hoge.py')`をmatlab上で実行してみて、問題なく実行されるか確認するとよい。これはOSを呼び出して指定されたコマンドを実行してくれるので、matlabとは独立した環境でpythonモジュールが動作するのかを確認することができる。

シンプルに`pyrunfile('hoge.py --foo bar');`としてあげればよい。この方法ならば、`hoge.py`からの戻り値も取得できる。

<!-- 6 Ref ---------------------------------------------------->
# References

[^Ref_psych]: [Psychtoolbox][Ref_psych]
[^Ref_spchText]: [GCP Speech-to-Text][Ref_spchText]
[^Ref_callExtLang]: [外部言語インターフェース - MathWorks][Ref_callExtLang]
[^Ref_MEX]: [MEXファイル関数][Ref_MEX]
[^Ref_pyCall]: [MATLABからのPythonの呼び出し][Ref_pyCall]
[^Ref_spchTextSample]: [googleapis/python-speech][Ref_spchTextSample]
[^Ref_systemCons]: [Python を使用するためのシステムの構成][Ref_systemCons]
[^Ref_supportApple]: [MacのApple SiliconはMA​TLABでサポートさ​れていますか？ - MathWorks][Ref_supportApple]
[^brew_x86_64]: [Apple Silicon (ARM, M1) のmacOSにaarch64とx86_64のhomebrewを共存させる - qiita][brew_x86_64]
[^python_x86_64]: [M1 MacのHomebrewでIntel版のPythonを手軽に切り替えて使う方法 - qiita][python_x86_64]
[^Ref_pythonEnv]: [PythonEnvironment - MathWorks][Ref_pythonEnv]
[^Ref_compile]: [compileall --- Python ライブラリをバイトコンパイル][Ref_compile]
[^Ref_pyCallDirect]: [MATLAB から Python 機能を直接呼び出す - MathWorks][Ref_pyCallDirect]
[^Ref_pyrun]: [pyrun - MathWorks][Ref_pyrun]
[^Ref_pyrunfile]: [pyrunfile - MathWorks][Ref_pyrunfile]
[^Ref_orgModule]: [ユーザー定義の Python モジュールの呼び出し - MathWorks][Ref_orgModule]
[^Ref_argparse]: [argparse --- コマンドラインオプション、引数、サブコマンドのパーサー][Ref_argparse]
[^Ref_pyDebug]: [名前 py.myfunc を解決できない - MathWorks][Ref_pyDebug]
[^Ref_dos]: [dos - MathWorks][Ref_dos]

[Ref_psych]: http://psychtoolbox.org
[Ref_spchText]: https://cloud.google.com/speech-to-text?hl=ja
[Ref_callExtLang]: https://jp.mathworks.com/help/matlab/external-language-interfaces.html?s_tid=CRUX_lftnav
[Ref_MEX]: https://jp.mathworks.com/help/matlab/call-mex-file-functions.html?searchHighlight=MEX&s_tid=srchtitle_MEX_5
[Ref_pyCall]: https://jp.mathworks.com/help/matlab/call-python-libraries.html
[Ref_spchTextSample]: https://github.com/googleapis/python-speech
[Ref_systemCons]: https://jp.mathworks.com/help/matlab/matlab_external/install-supported-python-implementation.html
[Ref_supportApple]: https://jp.mathworks.com/matlabcentral/answers/669238-mac-apple-silicon-matlab
[brew_x86_64]: https://qiita.com/chibiegg/items/43b7604a589e3760e88e
[python_x86_64]: https://qiita.com/kujirahand/items/bfb63fb485a1e43787e2
[Ref_pythonEnv]: https://jp.mathworks.com/help/matlab/ref/matlab.pyclient.pythonenvironment.html
[Ref_pyCallDirect]: https://jp.mathworks.com/help/matlab/matlab_external/ways-to-call-python-from-matlab.html
[Ref_pyrun]: https://jp.mathworks.com/help/matlab/ref/pyrun.html
[Ref_pyrunfile]: ]https://jp.mathworks.com/help/matlab/ref/pyrunfile.html
[Ref_orgModule]: https://jp.mathworks.com/help/matlab/matlab_external/call-user-defined-custom-module.html
[Ref_pyDebug]: https://jp.mathworks.com/help/matlab/matlab_external/undefined-variable-py-or-function-py-command.html
[Ref_dos]: https://jp.mathworks.com/help/matlab/ref/dos.html
[Ref_compile]: https://docs.python.org/ja/3/library/compileall.html
[Ref_argparse]:https://docs.python.org/ja/3/library/argparse.html