Monday 4 June 2018

VBA - Python - sympy factorizes quadratics

So, moving on from hello world programs, next I want to get Python to factorize quadratic expressions to show off what one can do in Python. Sympy does symbolic manipulation of math expressions. So I want to import from sympy.

Bad Indent reveals error stack and policy.py

So my first attempt, I pass an extra parameter into Greeting because I will want to pass equations. That worked. So now I want to import from sympy.

class QuadEqFactorizer(object):
    _reg_clsid_ = "{CAAA6723-6A89-4BCD-A375-43BF4C06B01B}"
    _reg_progid_= 'SciPyInVBA.QuadEqFactorizer'
    _public_methods_ = ['Greeting']

    def Greeting(self,name):
    import sympy as sp           #accidentally got indentation wrong
        return ("Hello world " + name)

if __name__=='__main__':
    print ("Registering COM server...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(QuadEqFactorizer)

And the VBA looks like this

Sub Test()

    Dim obj As Object
    Set obj = CreateObject("SciPyInVBA.QuadEqfactorizer")
    
    Debug.Print obj.greeting("Dave")

End Sub

And in the VBA on the CreateObject line I get the following error

Unexpected Python Error: Traceback (most recent call last):
  File "C:\Progra~2\Micros~4\Shared\Python~1\lib\site-packages\win32com\server\policy.py", line 136, in CreateInstance
    return retObj._CreateInstance_(clsid, reqIID)
  File "C:\Progra~2\Micros~4\Shared\Python~1\lib\site-packages\win32com\server\policy.py", line 194, in _CreateInstance_
    myob = call_func(classSpec)
  File "C:\Progra~2\Micros~4\Shared\Python~1\lib\site-packages\win32com\server\policy.py", line 728, in call_func
    return resolve_func(spec)(*args)
  File "C:\Progra~2\Micros~4\Shared\Python~1\lib\site-packages\win32com\server\policy.py", line 717, in resolve_func
    module = _import_module(mname)
  File "C:\Progra~2\Micros~4\Shared\Python~1\lib\site-packages\win32com\server\policy.py", line 736, in _import_module
    __import__(mname)
  File "C:\Users\Simon\source\repos\QuadEqFactorizer\QuadEqFactorizer\QuadEqFactorizer.py", line 7
    import sympy as sp
         ^
IndentationError: expected an indented block

So this simple indention error revealed a whole error stack, the source for policy.py looks interesting but I'll leave that for another day.

Indentation matters in Python, it defines code blocks without resorting to curly brackets (like C++,C#,Java,Javascript) or If..End If blocks like VBA. Note the trailing colons on some lines signifying start of a code block.

Installing sympy

Even after fixing indentation I got the error No module named 'sympy'. So I open command window and ran pip install sympy but got access error. Tried again with a new command window with admin rights and it worked.

C:\WINDOWS\system32>pip install sympy
Collecting sympy
  Using cached https://files.pythonhosted.org/packages/91/26/4e477dbd1f9260eb743d9f221af3044648a8fb2fcf3f2f23daee4dc831a4/sympy-1.1.1.tar.gz
Collecting mpmath>=0.19 (from sympy)
  Using cached https://files.pythonhosted.org/packages/7a/05/b3d1472885d8dc0606936ea5da0ccb1b4785682e78ab15e34ada24aea8d5/mpmath-1.0.0.tar.gz
Installing collected packages: mpmath, sympy
  Running setup.py install for mpmath ... done
  Running setup.py install for sympy ... done
Successfully installed mpmath-1.0.0 sympy-1.1.1

And now the code works but still returns Hello World. Let's move on and start calling sympy's factor().

sympy's factor return value cannot be converted to a COM_VARIANT

So we can call sympy's factor function to simplify quadratic equations but its return value is not returnable via COM and it cannot be converted to a COM_VARIANT.

    def Factorize(self,sEquation):
        import sympy as sp           
        return sp.factor (sEquation)

this is the exact error message

Unexpected Python Error: TypeError: Objects of type 'Pow' can not be converted to a COM VARIANT (but obtaining the buffer() of this object could).  

Wrap result in str() got it working

I'm guessing 'Pow' referred to the exponent term in the result and that it is complaining about my attempts to return the object to VBA without converting it to a string. It turns out that all we need is to wrap the answer in str(). [N.B. buffer() was a misleading error.] The final code listings are

class QuadEqFactorizer(object):
    _reg_clsid_ = "{CAAA6723-6A89-4BCD-A375-43BF4C06B01B}"
    _reg_progid_= 'SciPyInVBA.QuadEqFactorizer'
    _public_methods_ = ['Factorize']

    def Factorize(self,sEquation):
        import sympy as sp    
        factorized = sp.factor (sEquation)
        return str(factorized)

if __name__=='__main__':
    print ("Registering COM server...")
    import win32com.server.register
    win32com.server.register.UseCommandLine(QuadEqFactorizer)

And the VBA is thus

Sub Test()

    Dim obj As Object
    Set obj = CreateObject("SciPyInVBA.QuadEqfactorizer")
    
    Debug.Print obj.Factorize("x**2 + 2*x + 1")
    Debug.Print obj.Factorize("x**2 - x - 6")

End Sub

Which when run produces the output

(x + 1)**2
(x - 3)*(x + 2)

Final Thoughts

I do not believe a VBA programmer has any other open source options if they want to symbolically manipulate mathematical expressions. This is marvellous. One can now do one's math homework in VBA (whilst enlisting Python!)

No comments:

Post a Comment