Sympy の 関数,sympify の使い方と 有効数字の判定など

24 8月

e-Learning の教材の回答を取り扱う話です。回答は数値を想定していて,その有効数字を検証したいと思っています。数式処理ソフト等に回答を渡すと,自動的に処理が入ってしまうので,回答の有効数字が分からなくなります。回答を文字列のまま処理をしていくことになるかと思います。まず最初は回答の取り込みです。

sympify は文字列(中身は数式)を Sympy に式として取り込む関数です。

from sympy import *
expr = sympify('x +2*x +1')
print(expr)
>>> 3*x + 1

見ての通り、処理されてしまいます。止める方法は下記。

from sympy import *
expr = sympify('x +2*x +1', evaluate=False)
print(expr)
>>> x + 2*x + 1

これで評価が止まります。項の並べ替えはなされるようです。ただしこれは ubuntu22 での話。ubuntu20 では evaluate=False を書き加えるとエラーとなりました。Python の環境は,OSインストールしたままの状態です。それに synaptic で sympy を入れています。ubnutu22 で環境構築をすることになりそうです。以下はエラーキャッチの方法です。数式に相当する文字列かどうか判断することが目標なので,エラーへの対処が必要です。

from sympy import *
try:
    expr = sympify('1 + 1', evaluate=False)    
except Exception as ex:
    expr = ex  
print(expr)
print(type(expr))
>>> 1 + 1
>>> <class 'sympy.core.add.Add'>

下付きの文字などの扱いが気になります。アンダーラインを用いて定義は可能なようです。maxima の表記とは異なります。jupyter-notebook では init_printing() によって,TeX 的な表示が可能になります。その場合表示には print ではなくて,display を使います。

from sympy import *
init_printing()

x= symbols("x_{0:3}")

print(x)

display(x)

display(x[0])

出力は下記です。

sympify で数式を取り込む場合を考えます。アンダーラインは上手く取り込めなくて、x0 というような表記だとこんな感じになります。違いがなさそうです。

atoms(Symbol) で、定義されている変数を調べています。x0 と x1 が見つかっていますが、その結果は set オブジェクト(要素間の重複を無くした集合)で、これをリストに変更しています。リストでないとインデックスが利用できません(set には順番がない)。変数への代入を subs でやっています。代入もできているので、確かに変数となっているようです。

回答の有効数字判定ですが,今思いつく判定処理の順番は

  1. sympy が解釈できる数式としての表現になっているかどうか
  2. 文字が入っていないことの確認。入っていなければ数値のみの回答と認定する
  3. 数値ひとつかどうかの判断。1.23 + 2.341.234 * 2.345 他にはsin(3.1415) などを排除する
  4. 有効数字の判定

こんなところです。現在の e-Learning 教材は maxima の形式で回答してくるので、それを sympy 用に編集する必要もありますが,数値だけと判断した後なら必要ないのかもしれません。

下記のようなコードを書いてみました。色々なパターンの数値の有効数字を判定するものです。関数 getSignificantFigures が有効数字を求める所です。引数 tmpStr には,整数や 1.234e-5 等の形式の数式が記述された文字列が入ります。またコードには 1.234*10^3 や 1.234*10**3 のような形式への対応も書いています。

%reset -f

import sys
from sympy import *


def getSignificantFigures(tmpStr):

    def mainProcess(tmpStr):
        
        workingString = tmpStr
        
        index_e = tmpStr.find('e')
        index_E = tmpStr.find('E')

        if index_e != -1:
            workingString = tmpStr[:index_e]

        if index_E != -1:
            workingString = tmpStr[:index_E]

        workingString = workingString.replace('+',"")
        workingString = workingString.replace('-',"")
        workingString = workingString.replace('.',"")

        while workingString[0] == '0':
            workingString = workingString[1:]

        #print('last string "' + workingString + '"')

        significantFigures = len(workingString)    
    
        return significantFigures
    
    
    message = "none"
    significantFigures = -1
    
    typeArray = ["<class 'sympy.core.numbers.Integer'>", "<class 'sympy.core.numbers.Float'>"]

    tmpStr = tmpStr.replace(' ',"")
    #print('"' + tmpStr + '"')

    try:
        expr = sympify(tmpStr, evaluate=False)
    except Exception as ex:
        expr = ex

    #print(expr)   
    exprClass = str(type(expr))
    
    if exprClass == "<class 'sympy.core.sympify.SympifyError'>":    
        return significantFigures, "is not expression"

    # 以下、とりあえず数式にはなっている
    
    # 指数部の表記が異なる場合への対応
    
    tmpStr2 = tmpStr.replace('*10^',"e")
    tmpStr2 = tmpStr2.replace('*10**',"e")    

    try:
        expr2 = sympify(tmpStr2, evaluate=False)
    except Exception as ex:
        expr2 = ex

    #print(expr2)
    expr2Class = str(type(expr2))    
    

    if exprClass in typeArray:        
        significantFigures = mainProcess(tmpStr)        
        return significantFigures, "float or integer"
        
    elif expr2Class in typeArray:
        significantFigures = mainProcess(tmpStr2)        
        return significantFigures, "you use 10^ or 10**."

    else:
        return significantFigures, "is not integer or float"
    


testArray = []

testArray.append('-0.12e-3')
testArray.append('-0.12E-3')
testArray.append('-0.0120e-3')
testArray.append(' + 0.01200e-3')
testArray.append('-2.14*10^3')
testArray.append('-2.14*10**3')
testArray.append('-0.123')
testArray.append('-123')
testArray.append('- 123') # 符号との間に空白
testArray.append('-123 ') # 右側に空白
testArray.append(' - 123  ') # 多数の空白
testArray.append(' -0123  ') 
testArray.append('123*10^2*3*10^2') 




for i in range(len(testArray)):

    tmpStr = testArray[i]
    significantFigures = -1
    
    print('\nNo. ' + str(i))
    print('"' + tmpStr + '"')
    
    significantFigures, message = getSignificantFigures(tmpStr)
    
    print(message)
    print('significantFigures : ' + str(significantFigures))

出力は下記のような感じです。

No. 0
"-0.12e-3"
float or integer
significantFigures : 2

No. 1
"-0.12E-3"
float or integer
significantFigures : 2

No. 2
"-0.0120e-3"
float or integer
significantFigures : 3

No. 3
" + 0.01200e-3"
float or integer
significantFigures : 4

No. 4
"-2.14*10^3"
you use 10^ or 10**.
significantFigures : 3

No. 5
"-2.14*10**3"
you use 10^ or 10**.
significantFigures : 3

No. 6
"-0.123"
float or integer
significantFigures : 3

No. 7
"-123"
float or integer
significantFigures : 3

No. 8
"- 123"
float or integer
significantFigures : 3

No. 9
"-123 "
float or integer
significantFigures : 3

No. 10
" - 123  "
float or integer
significantFigures : 3

No. 11
" -0123  "
is not expression
significantFigures : -1

No. 12
"123*10^2*3*10^2"
is not integer or float
significantFigures : -1