pythonからRisa/Asirを使ってみた記録

唐突にpythonからRisa/Asirを使えるようにしようと思った。pythonの場合、sympyやsageのような選択肢もあるけど、
・sympy:全部pythonで書いてるので圧倒的に遅い。機能も多くはない。BSDライセンスらしい
・sage:Singularとかも使えるようなので、Sageでいい人は、別にいい気もする。機能は多すぎる。GPLらしい
とか。Risa/Asir本体のライセンスは、Fujitsu Research Laboratory Risa/Asir licenseというのが適用されるっぽい。大体、商用利用禁止・改変の有無に関わらず再配布禁止・ソースコードに変更を加えた場合は連絡しろとかいう内容。商用利用したい人(そんな人がいるのかは知らないけど)とかは、別の選択肢を探すしかなさげ


ライブラリインタフェース
http://www.math.kobe-u.ac.jp/OpenXM/Current/doc/asir2000/html-ja/man_284.html
によれば『Asir の提供する機能を他のプログラムから使用する方法として, OpenXM による他に, ライブラリの直接リンクによる方法が可能である. ライブラリは, GC ライブラリである `libasir-gc.a' とともに OpenXM distribution (http://www.math.kobe-u.ac.jp/OpenXM) に含まれる.』とかある。ライブラリとして利用するのは禁止しない方向っぽいので、(OpenXMは無視して)libasir.aから、libasir.soを作って、ctypes経由で呼んでやれば、何とかなりそうな気もする(Python/C APIで繋ぐ方がいいかもしれないけど、面倒なので)。で、こんな感じでlibasir.soを作った

ld -shared -o libasir.so --whole-archive libasir.a  --whole-archive libasir-gc.a -lXaw -lXmu -lXt  -lSM -lICE  -lXext -lX11  -lm

fPIC付けてコンパイルしろと言われたら、何か適当にいじって頑張る。X関連はいらないのだけど、without-xみたいなオプションもないようだし、入れておかないと怒られてしまうので、仕方ない。Windows環境でも、何か頑張ってdll作れば、同じだと思う


それで、なんかlibasir.soができたら

import ctypes

libasir=ctypes.CDLL('libasir.so')
libasir.asir_ox_init(0)

libasir.asir_ox_execute_string('1+1;')
res_size = libasir.asir_ox_peek_cmo_size()
result = ctypes.create_string_buffer(res_size)
libasir.asir_ox_pop_cmo(result,res_size)
print(map(ord,list(result)))

とかやって[20, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]という結果が返ってくるようなら、OK。この結果は、CMO(Common Mathematical Object)というものを、バイナリエンコーディングしたものらしい。あんまり、ちゃんとしたドキュメントとかはないけど、今の場合、最初の4バイト[20,0,0,0]の部分は、このデータが多倍長整数であることを示す。次の[1, 0, 0, 0]は、整数の符号と多倍長整数を表すのに必要なバイト数(DWORDで一単位)を示す。最後の[2,0,0,0]が実データで、結果が2であることを示す。asir_ox_initでエンディアンを指定するらしい。多分、複数のスレッドから使うようなことは想定されてなさげ。これは、差し当たって、どうしようもない気がする


これで、多項式とかリストも表せるのだけど、細かいデコードの仕方は、ソースコードのio/cio.cとか見た方が早い気がする(read_cmoと、その周辺)。一応、CMOの仕様(?)は、OpenXMの一部に含まれているっぽくて、
OX-RFC-100-ja
http://www.math.kobe-u.ac.jp/OpenXM/1.2.3-10/doc/OX-RFC-100-ja/OpenXM/node1.html
あたりを見ればよいのかもしれない


asir_ox_execute_stringは、Risa/Asirのevalみたいなもんで、関数とかも定義できる

import ctypes

libasir=ctypes.CDLL('libasir.so')
libasir.asir_ox_init(0)

s = """def test(X,Y){return X+Y;}"""
libasir.asir_ox_execute_string(s)
libasir.asir_ox_execute_string('test(1,2);')
res_size = libasir.asir_ox_peek_cmo_size()
result = ctypes.create_string_buffer(res_size)
libasir.asir_ox_pop_cmo(result,res_size)
print(map(ord,list(result)))

というわけで、asir_ox_execute_stringを呼び出して、グレブナー基底の計算をやることにする。グレブナー基底の計算は基本的に重いので、パースしたりするオーバーヘッドは、大体問題にならないだろう。といっても、nd_grとかは最初から組み込まれているので、

s = 'nd_gr([s*(y+z),t*(x*y),(1-s-t)*(x+y*z)],[s,t,x,y,z],0,[[0,2],[0,3]]);'
libasir.asir_ox_execute_string(s)

とかでいいらしい(完)。普通のgr関数は、dp_gr_mainとかを呼んでいるっぽいけど、マニュアルによると、nd_grは、一般にdp_gr_mainより高速であると書いてあるので、nd_gr関数を使っておけばいいのだと思う(グレブナー基底を計算する組み込みの関数は、nd_gr_traceやnd_f4とかもある。ありすぎだろ。。)


あと、必要なのは、CMOをデコードする関数。これは自分で作るしかない。まぁ、それで適当に作ってみた感じのものが末尾のコード。CMOデコーダ書く(というか、フォーマットを調べる)のが面倒なので、有理数とかは対応してないけど、とりあえず、入出力が32bit整数係数の多項式なら、正しく扱えるはず。


#ちなみに、多項式は、

>>> x,y=Symbol('x'),Symbol('y')
>>> (x+y)**2
y^2+x^2+2*y*x

とかで、明示的にexpandしなくても勝手に展開する。sympyとか、自分で展開する必要のある数式処理系は多い気がするけど、(1+3)**2って書いたら、勝手に計算して16が返ってくるのだから、相手が多項式であっても、勝手に"計算"(正規化)して結果を返すのが一貫性のある挙動だと思う。まぁ、多項式の場合、何を正規形とするか選択肢が色々あるので、そうなってるのだろうけど


これ書くのに、改めてRisa/Asirのドキュメントとか眺めたけど、組み込みの数学的アルゴリズムは、そんなに多くない。名前が付いているアルゴリズムは、多項式GCD、因数分解グレブナー基底の計算、最小分解体の計算くらい(Risa/Asir上で実装されてるアルゴリズムはもうちょっとあるけど)?それだけグレブナー基底が強力ということでもあり、ボトルネックになる部分は、そんなに多くないということでもあるし、こいつらが遅いと、あんまり価値はないということでもある

# -*- coding:utf-8 -*-
import ctypes
import StringIO
import struct

libasir=ctypes.CDLL('libasir.so')
libasir.asir_ox_init(0)


class Monomial(object):
   def __init__(self):
      self.degs = {}
   def __hash__(self):
      return tuple(self.degs.items()).__hash__()
   def __eq__(self , other):
      if len(self.degs)!=len(self.degs):
         return False
      else:
         for k,v in self.degs.iteritems():
             if other.degs.get(k,0)!=v:
                return False
         return True
   def __mul__(self , other):
      ret = Monomial()
      vars = set(self.degs.keys()) | set(other.degs.keys())
      for var in vars:
         d = self.degs.get(var , 0) + other.degs.get(var , 0)
         if d!=0:
            ret.degs[var] = d
      return ret
   def __str__(self):
      if len(self.degs)==0:
         return str(1)
      else:
         terms = []
         for k,v in self.degs.iteritems():
             if v==1:
                terms.append( str(k) )
             else:
                terms.append( "%s**%d" % (k,v) )
         return "*".join( terms )
   def __repr__(self):
      if len(self.degs)==0:
         return str(1)
      else:
         terms = []
         for k,v in self.degs.iteritems():
             if v==1:
                terms.append( str(k) )
             else:
                terms.append( "%s^%d" % (k,v) )
         return "*".join( terms )


class Polynomial(object):
   def __init__(self):
       self.coeffs = {}
   def __eq__(self , other):
       if not isinstance(other , Polynomial):
           if len(self.coeffs)>1:
                return False
           elif len(self.coeffs)==1:
                k,v = self.coeffs.items()[0]
                return (len(k.degs)==0 and v==other)
           else:
                return (other==0)
       elif len(self.coeffs)!=len(other.coeffs):
           return False
       else:
           for k,v in self.coeffs.iteritems():
               if other.coeffs.get(k , 0)!=v:
                  return False
           return True
   def __ne__(self,other):
       return not(self==other)
   def __add__(self , other):
       if isinstance(other , Polynomial):
          ms = set(self.coeffs.keys()) | set(other.coeffs.keys())
          ret = Polynomial()
          for m in ms:
              c = self.coeffs.get(m , 0) + other.coeffs.get(m , 0)
              if c!=0:ret.coeffs[m] = c
          return ret
       else:
          ret = Polynomial()
          cm = Monomial()
          for m,c in self.coeffs.iteritems():
              ret.coeffs[m] = c
          if other!=0:ret.coeffs[cm] = ret.coeffs.get(cm , 0) + other
          return ret
   def __radd__(self , other):
       return self.__add__(other)
   def __sub__(self , other):
       if isinstance(other , Polynomial):
          ms = set(self.coeffs.keys()) | set(other.coeffs.keys())
          ret = Polynomial()
          for m in ms:
              c = self.coeffs.get(m , 0) - other.coeffs.get(m , 0)
              if c!=0:ret.coeffs[m] = c
          return ret
       else:
          ret = Polynomial()
          cm = Monomial()
          for m,c in self.coeffs.iteritems():
              ret.coeffs[m] = c
          if other!=0:ret.coeffs[cm] = ret.coeffs.get(cm , 0) - other
          return ret
   def __neg__(self):
       ret = Polynomial()
       for k,v in self.coeffs.iteritems():
           ret.coeffs[k] = -v
       return ret
   def __rsub__(self , other):
       tmp = self.__sub__(other)
       return tmp.__neg__()
   def __mul__(self , other):
       if isinstance(other , Polynomial):
          ret = Polynomial()
          for m1,c1 in self.coeffs.iteritems():
             for m2,c2 in other.coeffs.iteritems():
                 m = m1*m2
                 c = c1*c2
                 if c!=0:ret.coeffs[m] = ret.coeffs.get(m , 0) + c
          return ret
       else:
          ret = Polynomial()
          for m,c in self.coeffs.iteritems():
             c0 = c*other
             if c0!=0:ret.coeffs[m] = ret.coeffs.get(m , 0) + c0
          return ret
   def __rmul__(self , other):
       return self.__mul__(other)
   def __pow__(self , n):
       assert(isinstance(n,int))
       assert(n>=0)
       ret = 1
       for _ in xrange(n):
           ret *= self
       return ret
   def __call__(self,**args):   #-- 代入操作
       vars = [str(v) for v in args.keys()]
       ucvars = [("v_%d" % n) for n in xrange(len(vars))]
       terms = []
       for (m,c) in self.coeffs.iteritems():
           if len(m.degs)==0:
              terms.append( str(c) )
           else:
              mterms = []
              for (varname , degree) in m.degs.iteritems():
                  if varname in vars:
                      mterms.append('%s**%d' % (ucvars[vars.index(varname)] , degree))
                  else:
                      mterms.append('Symbol("%s")**%d' % (str(varname) , degree))
              terms.append('%s*%s' % (str(c) , '*'.join(mterms)))
       if len(terms)==0:
           pyexpr = ("lambda %s:%s" % (",".join(ucvars) , "0"))
       else:
           pyexpr = ("lambda %s:%s" % (",".join(ucvars) , "+".join(terms)))
       return apply(eval(pyexpr) , args.values())
   def __str__(self):
       terms = []
       for (m,c) in self.coeffs.iteritems():
           if len(m.degs.keys())==0:
               terms.append( str(c) )
           elif c==1:
               terms.append( str(m) )
           elif c==-1:
               terms.append( "-%s" % str(m) )
           else:
               terms.append( "%s*%s" % (str(c),str(m)) )
       if len(terms)!=0:
          return "+".join(terms)
       else:
          return "0"
   def __repr__(self):
       """
        strとreprの(主な)違いは、べきをpython流に"**"で表すか"^"で表すか
        変なSymbol名を付けなければ、reprの返り値はRisa/Asirで解釈できるはず
        
       """
       terms = []
       for n,(m,c) in enumerate(self.coeffs.iteritems()):
           if n>0 and c>0:
              terms.append('+')
           if len(m.degs.keys())==0:
               terms.append( str(c) )
           elif c==1:
               terms.append( repr(m) )
           elif c==-1:
               terms.append( "-%s" % repr(m) )
           else:
               terms.append( "%s*%s" % (str(c),repr(m)) )
       if len(terms)!=0:
          return "".join(terms)
       else:
          return "0"


class Symbol(Polynomial):
   def __init__(self , s):
      super(Symbol , self).__init__()
      m = Monomial()
      m.degs[s] = 1
      self.coeffs[m] = 1



#ref: read_cmo @ io/cio.c
def readCMO(buf):
   def __main__(ss,vtab=[]):
      tag = struct.unpack('I',ss.read(4))[0]
      if tag==1:   #-- CMO_NULL
         return 0
      elif tag==4:  #-- CMO_STRING
         szStr = struct.unpack('I' , ss.read(4))[0]
         return ss.read(szStr)
      elif tag==17: #-- CMO_LIST
         szList = struct.unpack('I' , ss.read(4))[0]
         return [__main__(ss) for _ in xrange(szList)]
      elif tag==19:  #--CMO_MONONIAL32
         assert False,"FIXME:decode CMO_MONOMIAL32"
      elif tag==20:   #-- CMO_ZZ
         sz = struct.unpack('i',ss.read(4))[0]
         if sz<0:
            sign = -1
            vals = struct.unpack('I'*(-sz) , ss.read(-4*sz))
         elif sz>0:
            sign = +1
            vals = struct.unpack('I'*(sz) , ss.read(4*sz))
         assert(len(vals)==1),"FIXME:decode Multi-byte ineteger"
         return sign*vals[0]
      elif tag==21:   #-- CMO_QQ
         assert False,"FIXME:decode CMO_QQ"
      elif tag==27:   #-- CMO_RECURSIVE_POLYNOMIAL
         vlist = __main__(ss)
         remote_vtab = [Symbol(vname) for vname in vlist]
         return __main__(ss,remote_vtab)
      elif tag==31:   #-- CMO_DISTRIBUTED_POLYNOMIAL
         assert False,"FIXME:decode CMO_DISTRIBUTED_POLYNOMIAL"
      elif tag==33:    #-- CMO_UNIVARIATE_POLYNOMIAL
         n = struct.unpack('I' , ss.read(4))[0]
         idx = struct.unpack('I' , ss.read(4))[0]
         ret = 0
         for _ in xrange(n):
            d = struct.unpack('I' , ss.read(4))[0]
            c = __main__(ss,vtab)
            ret += c*(vtab[idx]**d)
         return ret
      elif tag==60:    #-- CMO_INTERMINATE
         return __main__(ss)
   stream = StringIO.StringIO(buf)
   return __main__(stream)


#-- simple wrapper of Risa/Asir's nd_gr function
def nd_gr(I , Vars , char=0 , ord=0):
   def getvars(p):
      assert(isinstance(p, Polynomial))
      ret = sum([m.degs.keys() for m in p.coeffs.keys()] , [])
      return ret
   assert(isinstance(char , int)),"invalid characteristic"
   # python側で使用可能な不定元名がAsir側で利用可能とは限らないのでrenameする
   all_vars = list(set(map(str,Vars) + sum([getvars(p) for p in I] ,[])))
   ptoa = dict([(str(v) , Symbol('x%d' % n)) for (n,v) in enumerate(all_vars)])
   atop = dict([('x%d' % n , Symbol(v)) for (n,v) in enumerate(all_vars)])
   NI = [apply(p,[],ptoa) for p in I]
   NVars = [apply(p,[],ptoa) for p in Vars]
   asir_expr = 'nd_gr(%s , %s , %d , %s);' % (repr(NI) , repr(NVars) , char ,str(ord))
   libasir.asir_ox_execute_string(asir_expr)
   res_size = libasir.asir_ox_peek_cmo_size()
   result = ctypes.create_string_buffer(res_size)
   libasir.asir_ox_pop_cmo(result,res_size)
   ret = readCMO(result.raw)
   return [apply(p,[],atop) for p in ret]


if __name__=="__main__":
   s,t,x,y,z = map(Symbol , "stxyz")
   rlist = nd_gr([s*(y+z),t*(x*y),(1-s-t)*(x+y*z)],[s,t,x,y,z],ord=[[0,2],[0,3]]);
   #-- Risa/Asirで計算した結果を、そのまま持ってきたもの
   plist = [(-y**2-z*y)*x**2+(-z*y**3-z**2*y**2)*x,
            t*y*x,
            (-y+(t-1)*z)*x+(t-1)*z*y**2+(t-1)*z**2*y,
            (y+(-t+1)*z)*x**2+(z*y**2+z**2*y)*x,
            s*y+s*z,
            (s+t-1)*x+(t-1)*z*y-s*z**2,
            -t*s*z*x,
            (-t*s-t**2+t)*x**2]
   for (p1,p2) in zip(plist,rlist):
       if (p1!=p2):
          print ("invalid result:%s and %s" % (repr(p1),repr(p2)))
          break
   else:
       print("correct result:%s" % repr(rlist))