上記のスライドによると、LuaTeXのLuaの機能 (io.popen
) を使って数式処理システムMaximaを呼び出す時に、数式ごとにMaximaを起動していると遅いそうです。
毎度Maximaを起動するのが遅いのであれば、文書処理の間はMaximaを立ち上げっぱなしにして、何らかのプロセス間通信でMaximaとやりとりする、という方法が考えられます。
Maximaはプロセス間通信の方法として、標準入出力のほかにソケットに対応しています。また、LuaTeXにはLuaSocketという、Luaでソケットを扱えるライブラリーが付属しています。
ということで、LuaSocketでMaximaと通信してみます。
ncで実験してみる
いきなりLuaSocketを使う前に、 nc
コマンドを使ってMaximaのソケット通信機能を試してみます。
サーバーはMaximaを使いたい側が用意します。Maximaの起動時にポート番号を教えると、Maximaがクライアントとしてそこに接続する、という形になります。
実験では、12345番ポートを使ってみます。
$ nc -l 12345
nc
を起動したのとは別のターミナルでMaximaを立ち上げます。その際、-s
オプションでポート番号を指定します。
$ maxima --very-quiet -s 12345
Connecting Maxima to server on port 12345
Maximaがソケットに接続すると、ソケット経由で pid=<プロセスID>
という文字列が送られてきます。
あとは普通にやりとりします。
[Maximaからの出力]pid=51844
[Maximaへの入力 ]tex1(expand((1+x)^5));
[Maximaからの出力] x^5+5\,x^4+10\,x^3+10\,x^2+5\,x+1
[Maximaへの入力 ]tex1(diff(sin(x)*cos(x^2), x));
[Maximaからの出力] \cos x\,\cos x^2-2\,x\,\sin x\,\sin x^2
LuaSocketでやってみる
あとは、同様のやりとりをLuaSocketで実装します。LuaSocketの説明はマニュアルを参照してください。
Lua標準で外部コマンドを起動する方法は os.execute
と io.popen
がありますが、前者は実行するコマンドが完了するまでLua側に制御が帰ってきません。それでは困るので、Maximaコマンドの起動には io.popen
を使うことにします。
local socket = require "socket"
local server = assert(socket.bind("*", 0))
local ip, port = server:getsockname()
print("IP:", ip)
print("Port:", port)
local maxima = assert(io.popen(string.format("maxima --very-quiet -s %d", port)))
local client = assert(server:accept())
print("Connection accepted")
print("Maxima stdout:", maxima:read("*l")) -- "Connecting Maxima to server on port XXXXX"
client:settimeout(10)
assert(client:receive("*l")) -- "pid=*****"
client:send("tex1(expand((x+3)^2));\n")
local result = assert(client:receive("*l"))
print("Received", result)
client:send("quit();\n")
print("Closing socket", client:close())
print("Closing pipe", maxima:close())
assert(client:receive("*l"))
の行までが準備(Maximaの起動と通信の確立)で、 client:send
と client:receive
の部分が個々の数式に関するMaximaとのやりとりです。
最後に quit();
を送ってMaximaを自発的に終了させていますが、環境によってはこれがなくても動きます。Linuxでは quit();
なしで切断しようとするとMaximaがセグフォを起こしました。
LuaScoket等のエラーチェックは、雑に assert
で済ませています。
このコードを標準のLuaインタープリターで試すにはLuaSocketが必要です。TeX環境が手元のPCに整っているという方なら、LuaTeXのLuaインタープリターである texlua
を使うと別途LuaSocketを用意する必要がないので楽です。
Lua(La)TeX文書に埋め込んでみる
あとはLuaTeX文書にこのコードを埋め込めばOKです。
ただし、TeXファイル中にLuaコードをがっつり書くのはだるいので、LuaコードはTeXとは別のファイルに書きます。
local socket = require "socket"
local meta = {}
meta.__index = meta
-- 新しくMaximaセッションを起動する
local function new()
local server = assert(socket.bind("*", 0))
local ip, port = server:getsockname()
print(ip, port)
local maxima = assert(io.popen(string.format("maxima --very-quiet -s %d", port)))
local client = assert(server:accept())
print("Connection accepted")
-- maxima:read("*l") -- "Connecting Maxima to server on port XXXXX"
client:settimeout(10)
assert(client:receive("*l")) -- "pid=*****"
return setmetatable({_server = server, _client = client, _process = maxima}, meta)
end
-- Maximaセッションを使ってコマンドを実行する
function meta:run(command)
command = command:gsub("\n*$", "")
self._client:send(command..";\n")
local result = assert(self._client:receive("*l"))
print("Received", result)
return (result:gsub("%s", " "))
end
-- Maximaセッションを閉じる
function meta:close()
self._client:send("quit();\n")
self._client:close()
self._process:close()
self._server:close()
end
return {
new = new,
}
LuaLaTeX文書からは dofile
で上述Luaコードを実行し、Maximaセッションを起動します。
\documentclass{article}
\directlua{
maxima = dofile("luatex-maxima.lua")
session = maxima:new()
}
\newcommand\maxima[1]{\directlua{tex.print(session:run([[tex1(#1)]]))}}
\begin{document}
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{diff((x+3)^2,x)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{expand((x+3)^2)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x^3+3)^50,x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\[\maxima{diff((x+3)^(50),x)}\]
\directlua{session:close()}
\end{document}
あとは、 lualatex --shell-escape luatex-maxima-test.tex
という感じで処理します。
処理時間の比較
比較用に、毎回Maximaを立ち上げる版の luatex-maxima-nosocket.lua
を以下の内容で用意しておきます。
local meta = {}
meta.__index = meta
local function new()
return setmetatable({}, meta)
end
function meta:run(command)
command = command:gsub("\n*$", "")
local maxima = assert(io.popen(string.format("echo '%s;' | maxima --very-quiet", command)))
local result = maxima:read("*a")
maxima:close()
result = (result:gsub("%s", " "))
print("Received", result)
return result
end
function meta:close()
end
return {
new = new,
}
こちらを使う場合は、 luatex-maxima-test.tex
の dofile("luatex-maxima.lua")
を dofile("luatex-maxima-nosocket.lua")
に書き換えます。
私のMac環境(High Sierra / TeX Live 2018 / Maxima 5.41.0)で
$ time lualatex --shell-escape luatex-maxima-test.tex
を実行したところ、毎回Maximaを起動する版の処理時間は11.4秒、ソケットで通信する版は3.8秒でした。Maximaの起動回数を減らすことの効果はやはり大きいようです。数式の量が多ければ差はもっと開くかもしれません。
おまけ:popenによる双方向通信
popen関数は通常、「プロセスの標準出力を読み取る」または「プロセスの標準入力に書き込む」のどちらかの動作しかできません。
これに対し、FreeBSDのlibcではpopenの第二引数に "r+"
を指定することで双方向パイプを開けるようです。これを使うと、ソケットを使わなくても次のようにMaximaとやりとりできます:
local maxima = assert(io.popen("maxima --very-quiet", "r+"))
maxima:setvbuf("line")
maxima:write("tex1((1+x)^2);\n")
print("Maxima stdout:", maxima:read("*l"))
maxima:write("tex1(expand((x+3)^2));\n")
print("Maxima stdout:", maxima:read("*l"))
maxima:close()
しかし、popenで双方向通信できるのはFreeBSD系(FreeBSDとMac)だけ(Linux, Windowsでは対応していない)のようなので、文書がMac専用でいいというのでもない限り、Maximaとのやりとりにはソケットを使うのが良いでしょう。