LoginSignup
3
3

More than 3 years have passed since last update.

ソケットを使ってLua/LuaTeX文書からMaximaを呼び出す

Posted at

ネタ元:マークシート選択式問題における数式処理の活用

上記のスライドによると、LuaTeXのLuaの機能 (io.popen) を使って数式処理システムMaximaを呼び出す時に、数式ごとにMaximaを起動していると遅いそうです。

毎度Maximaを起動するのが遅いのであれば、文書処理の間はMaximaを立ち上げっぱなしにして、何らかのプロセス間通信でMaximaとやりとりする、という方法が考えられます。

Maximaはプロセス間通信の方法として、標準入出力のほかにソケットに対応しています。また、LuaTeXにはLuaSocketという、Luaでソケットを扱えるライブラリーが付属しています。

ということで、LuaSocketでMaximaと通信してみます。

ncで実験してみる

いきなりLuaSocketを使う前に、 nc コマンドを使ってMaximaのソケット通信機能を試してみます。

サーバーはMaximaを使いたい側が用意します。Maximaの起動時にポート番号を教えると、Maximaがクライアントとしてそこに接続する、という形になります。

実験では、12345番ポートを使ってみます。

ターミナル1
$ nc -l 12345

nc を起動したのとは別のターミナルでMaximaを立ち上げます。その際、-s オプションでポート番号を指定します。

ターミナル2
$ maxima --very-quiet -s 12345
Connecting Maxima to server on port 12345

Maximaがソケットに接続すると、ソケット経由で pid=<プロセスID> という文字列が送られてきます。

あとは普通にやりとりします。

ターミナル1
[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.executeio.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:sendclient: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とは別のファイルに書きます。

luatex-maxima.lua
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セッションを起動します。

luatex-maxima-test.tex
\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 を以下の内容で用意しておきます。

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.texdofile("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とのやりとりにはソケットを使うのが良いでしょう。

3
3
2

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
3
3