Long integers in Python and Excel and py_Fact

I have added a new py_Fact function to the py_Scipy module, that returns the factorial of an integer as a float if it is up to 15 digits long, or a string for longer numbers.

The revised code, and the example shown below in the py_Special-Dist.xlsb spreadsheet, can be downloaded from:

py_SciPy.zip

@xl_func(category = "Scipy",  help_topic="https://docs.scipy.org/doc/scipy/reference/special.factorial.html!0")
@xl_arg('n', 'numpy_array<int>', ndim=1) 
@xl_arg('exact', 'bool')
@xl_return('numpy_array<var>')
def py_fact(n, exact = False):
    res = scipy.special.factorial(n, exact = exact)
    if exact: 
        for i in range(0, res.shape[0]):
            if res[i] > 1E15: res[i] = str(res[i])
    return res

If the “exact” argument is set to True, or omitted, the Python function returns a long integer of the required length, but if this is returned to Excel as a number it would be converted to a float, so all the results greater than 1E15 are converted to a string so no digits are lost, and the resulting array is returned to Excel as a variant array.

In the example below the last 13 digits of the strings have been compared with the last 13 digits of the values returned as floats. Because the factorial numbers always have trailing zeros the integer and float results are exactly equal up to 22!, after which the difference rapidly starts to increase.

Recently someone told me that they were going to a birthday party of a neighbour who was turning 106! and I can now point out that this is equal to:
114628056373470835453434738414834942870388487424139673389282723476762012382449946252660360871841673476016298287096435143747350528228224302506311680000000000000000000000000.

Posted in Excel, Link to Python, Maths, Newton, NumPy and SciPy, PyXLL, UDFs | Tagged , , , , , | Leave a comment

Python functions for rotation

I have recently needed code to rotate 2D coordinates, either about the origin, or some specified other point, using pyxll to call the code from Excel. My first attempt was a direct translation of some VBA code, that also worked with 3D coordinates, with rotation about any of the 3 axes:

@xl_func
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('Axis', 'int')
def py_Rotate(Rcoords, Rotation, Axis, result = 0):
    return Rotate(Rcoords, Rotation, Axis, result)

def Rotate(Rcoords, Rotation, Axis, result = 0):
#  # Rotate about the origin, Rotation in radians
    Plane = np.zeros(2, dtype = int)
    if Rotation != 0:
        if Axis == 1:
            Plane[0] = 1
            Plane[1] = 2
        elif Axis == 2:
            Plane[0] = 2
            Plane[1] = 0
        elif Axis == 3:
            Plane[0] = 0
            Plane[1] = 1
        else:
            return "Invalid Axis"
        NumRows = Rcoords.shape[0]
         # Rotate
        for i in range(0, NumRows):
            try:
                if (Rcoords[i, Plane[0]] == 0 and Rcoords[i, Plane[1]] == 0) == False:
                    r = (Rcoords[i, Plane[0]]** 2 + Rcoords[i, Plane[1]]** 2)** 0.5
                    Theta = atan2(Rcoords[i, Plane[1]], Rcoords[i, Plane[0]])
                    Theta = Theta + Rotation
                    Rcoords[i, Plane[0]] = r * cos(Theta)
                    Rcoords[i, Plane[1]] = r * sin(Theta)
            except:
                pass
    if result == 0:
        return Rcoords
    else:
        return Rcoords[0, result-1]

This was greatly simplified, and speeded up, by using 2D coordinates for the input, simplifying the rotation formula, and using Numpy procedures to operate on entire columns, rather than looping through row by row:

@xl_func
@xl_arg('Rcoords', 'numpy_array')
def Rotate2(Rcoords, Rotation):
#  2D Rotate about the origin, Rotation in radians
    sinR = sin(Rotation)
    cosR = cos(Rotation)
    Rcoords2 = np.zeros((Rcoords.shape[0],2))
    Rcoords2[:,0] = Rcoords[:,0]*cosR - Rcoords[:,1]*sinR
    Rcoords2[:,1] = Rcoords[:,0]*sinR + Rcoords[:,1]*cosR    
    return Rcoords2

Performing the rotation by using matrix multiplication gave a further speed improvement:

@xl_func
@xl_arg('Rcoords', 'numpy_array')
def RotateM(Rcoords, Rotation):
#  2D Rotate about the origin, Rotation in radians
    sinR = sin(Rotation)
    cosR = cos(Rotation)
    rotnA = np.array([[cosR, sinR],[-sinR, cosR]])
    Rcoords2 = np.matmul(Rcoords, rotnA)    
    return Rcoords2

To rotate about a point other than the origin the 3 functions above were modified by:

  • Translating the coordinates to move the rotation point to the origin.
  • Rotate the coordinates about the origin.
  • Translate the rotated coordinates back by the same amount.
@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
@xl_arg('Axis', 'int')
def RotateC(Rcoords, Rotation, RotnPt, Axis, result = 0):
 # Rotate about RotnPt, Rotation in radians
    Tol = 0.000000000001
    NumRows = Rcoords.shape[0]
    if abs(Rcoords[0, 0] - Rcoords[NumRows-1, 0]) + abs(Rcoords[0, 1] - Rcoords[NumRows-1, 1]) < Tol: NumRows = NumRows - 1
    NumRCols = Rcoords.shape[1]
    if RotnPt.shape[0] > RotnPt.shape[1]:
        Rotnpt2 = RotnPt
        NumCCols = RotnPt.shape[0]
        RotnPt = np.zeros((1, NumCCols))
        RotnPt[0, :] = Rotnpt2[:, 0]
        NumCCols = RotnPt.shape[1]
    if NumRCols < NumCCols:
        NumCols = NumRCols
    else: 
        NumCols = NumCCols
    OCoords = np.zeros((NumRows, NumRCols))
    for i in range(0, NumRows):
        for j in range(0, NumCols):
            OCoords[i, j] = Rcoords[i, j] - RotnPt[0, j]
        OCoords = Rotate(OCoords, Rotation, Axis)
    for i in range(0, NumRows):
        for j in range(0, NumCols):
            OCoords[i, j] = OCoords[i, j] + RotnPt[0, j]
    OCoords[0:NumRows, NumCols:NumRCols] = Rcoords[0:NumRows, NumCols:NumRCols]
    return OCoords

@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
@xl_arg('Axis', 'int')
def RotateC2(Rcoords, Rotation, RotnPt, Axis, result = 0):
 # Rotate about RotnPt, Rotation in radians; with numpy array calculations
    Tol = 0.000000000001
    NumRows = Rcoords.shape[0]
    if abs(Rcoords[0, 0] - Rcoords[NumRows-1, 0]) + abs(Rcoords[0, 1] - Rcoords[NumRows-1, 1]) < Tol: NumRows = NumRows - 1
    NumRCols = Rcoords.shape[1]    
    if RotnPt.shape[0] > RotnPt.shape[1]:
        Rotnpt2 = RotnPt
        NumCCols = RotnPt.shape[0]
        RotnPt = np.zeros((1, NumCCols))
        RotnPt[0, :] = Rotnpt2[:, 0]
    NumCCols = RotnPt.shape[1]
    if NumRCols < NumCCols:
        NumCols = NumRCols
    else: 
        NumCols = NumCCols
    OCoords = Rcoords - RotnPt
    OCoords = Rotate2(OCoords, Rotation)
    OCoords = OCoords + RotnPt
    OCoords[0:NumRows, NumCols:NumRCols] = Rcoords[0:NumRows, NumCols:NumRCols]
    return OCoords

@xl_func()
@xl_arg('Rcoords', 'numpy_array')
@xl_arg('RotnPt', 'numpy_array')
def RotateCM(Rcoords, Rotation, RotnPt):
 # Rotate about RotnPt, Rotation in radians; with numpy array calculations
    OCoords = Rcoords - RotnPt
    OCoordsr = RotateM(OCoords, Rotation)    
    OCoordsr = OCoordsr + RotnPt
    return OCoordsr

Finally for comparison and to check results some code was adapted from https://gist.github.com/LyleScott/ . Note that this code treats clockwise rotations as positive, whereas the other functions use the standard approach of anti-clockwise rotation being positive.

@xl_func()
@xl_arg('xy', 'numpy_array')
@xl_arg('radians', 'float')
@xl_arg('origin', 'numpy_array', ndim=1)
def rotate_around_point_highperf(xy, radians, origin=(0, 0)):
    """Rotate a point around a given point.
    From: https://gist.github.com/LyleScott/
    I call this the "high performance" version since we're caching some
    values that are needed >1 time. It's less readable than the previous
    function but it's faster.
    """
    x = xy[:,0]
    y = xy[:,1]
    offset_x, offset_y = origin[:]
    adjusted_x = (x - offset_x)
    adjusted_y = (y - offset_y)
    cos_rad = cos(radians)
    sin_rad = sin(radians)
    qx = offset_x + cos_rad * adjusted_x + sin_rad * adjusted_y
    qy = offset_y + -sin_rad * adjusted_x + cos_rad * adjusted_y
    res = np.zeros((qx.shape[0],2))
    res[:,0] = qx
    res[:,1] = qy
    return res

Results and execution times for rotation of a simple shape are shown in the screen-shot below (click on the image for full-size view):

The functions using matrix multiplication are significantly faster, and this improvement is increased with a longer list of coordinates:

Posted in Coordinate Geometry, Excel, Link to Python, Maths, Newton, PyXLL | Tagged , , , , , , | Leave a comment

Evaluate and display definite integrals in Excel with Latex and Matplotlib

The py_Evalu spreadsheet was last presented at Scipy Functions with Excel and pyxll 5 – Evaluate with Units and included the Plot_Math function, which converts a text string to Latex format, then uses Matplotlib to convert this to a graphic image, and optionally evaluates the function. To extend this functionality I have now added the Plot_Quad function to evaluate and display definite integrals.

The updated files are included in the py_SciPy zip file that can be downloaded from:

py_SciPy.zip

Details of the required pyxll package (including download, free trial, and full documentation) can be found at: pyxll

For those installing a new copy of pyxll, a 10% discount on the first year’s fees is available using the coupon code “NEWTONEXCELBACH10”.

Examples of the Plot_Quad function are shown on the Latex sheet.

Required inputs are:

  • The function to be integrated as a text string.
  • The integration limits.
  • Any fixed parameters and their values.

When the function is entered the result of the integration is returned in the function cell, and an image of the definite integral is displayed in Latex format immediately below. The image may be selected and dragged to any desired new location, as shown in the second example above. This example returns the area of a circle quadrant, which in this case is accurate to 15 significant figures.

Help on the function can be displayed by clicking on the “insert function” icon, immediately to the left of the edit line, to open the “function wizard”. This shows brief help on each function argument. Detailed help on the SciPy Quad function (used for the evaluation of the integral) can be displayed by clicking on “Help on this function”, at the bottom left of the function wizard display:

By default, the integration variable is “x”. Any other symbol may be specified with the optional “var” argument. Optional arguments for the quad function may also be specified with the “kwargs” argument (see the Scipy help for details of all options). In the example below the epsrel argument has been entered to allow a greater relative error in the result, with in this case only a small reduction in execution time:

In the next example the function has been used to find the force on a circular concrete section generated by a parabolic-rectangular stress distribution, as defined by the Eurocode 2 concrete design code. In this case two separate functions are required, for the rectangular and parabolic part of the stress distribution:

The resulting total force is compared with the results of the py_Circu concrete design function, showing near exact agreement.

The spreadsheet also has an on-sheet check using Simpson’s method with each block divided into 20 layers, showing good (but less exact) agreement:

Posted in Excel, Link to Python, Maths, Newton, Numerical integration, PyXLL, UDFs | Tagged , , , , , , , , , , , , | Leave a comment

py_RC Elastic 1.04 and fatigue to AS 3600

The py_RC Elastic spreadsheet (last discussed here) has been updated with a new function py_Fatigue3600, providing design for fatigue to the latest AS 3600 concrete structures code. The new spreadsheet and associated Python code can be downloaded from:

py_RC Elastic.zip

The download file includes full open-source Python code. For details of the pyxll package required to link the Python code to Excel see: https://newtonexcelbach.com/python-and-pyxll/

Input for the new py_Fatigue3600 function is as before. Note that the new function is intended for use on bridge structures, and the number of fatigue cycles is calculated to AS 5100.2 requirements, based on the current number of heavy vehicles/lane/day:

The output of both functions is shown below:

The main differences between the calculations to the two codes are:

  • Different variation in the steel fatigue stress limit related to the number of stress cycles. AS 3600 has a higher stress limit for mid-range cycles, but for low cycle numbers (as in this case), or very high cycles, the AS 5100.5 stress limits are higher.
  • For the concrete compressive stress limit AS 5100.5 has a conservative limit, not related to the number of cycles. AS 3600 has a procedure for calculating the number of cycles to failure for any specified compressive stress. This has been adapted in the function to calculate a stress limit for any given number of cycles. In most cases the AS 3600 limit is significantly higher than the AS 5100.5 simplified approach.

A check has been added to the spreadsheet to verify that the concrete stress limit calculated by the AS 3600 function does correspond with the required number of stress cycles:

Posted in Concrete, Excel, Link to Python, Newton, PyXLL, UDFs | Tagged , , , , , , , , , , | 1 Comment

Importing text files with Python and pyxll

VBA code to import text files to Excel was presented at Importing text files with VBA – 2 and following posts. We now look at Python code for a user defined function (UDF) for the same task.

The code requires Python and Numpy, together with pyxll to link the code to Excel. See Python and pyxll for information on the pyxll package, including a discount coupon for new users.

The code uses the Python readlines function to read the file to a list of text lines, then optionally the Numpy fromstring function to extract the numbers from each line.

import os
import numpy as np

@xl_func
@xl_arg('filename', 'str')
@xl_arg('dir', 'str')
@xl_arg('ExtractVals', 'bool')
@xl_return('numpy_array<var>')
def py_ReadText(filename, dir = '', ExtractVals = True ):
    """ 
    Returns the contents of a text file
    :param filename: Text file name.
    :param dir: Full directory path.
    :param ExtractVals: True = extract numerical values from each line and return as a numpy array.
    """ 
    if dir == '':
        dir = os.getcwd() # + r'/'
    filename = os.path.join(dir, filename) 
    try:
        with open(filename, 'r') as file:
            txta = file.readlines()
    except:
        return np.array(['File not found at ' + dir])

    if ExtractVals:
        numlist = []
        nrows = len(txta)
        numlist = []
        maxn = 0
        for row in txta:
            numline = np.fromstring(row, sep = ' ')
            numlist.append(numline)
            n = numline.shape[0]
            if n > maxn: maxn = n
        numa = np.zeros((nrows, maxn))
        for i in range(0, nrows):
            n = numlist[i].shape[0]
            numa[i, 0:n] = numlist[i]        
        return numa
    else:
        return np.array(txta, dtype = str)

Sample output is shown in the screenshot below:

Column B imports the file as text, with Column F importing the same file with the numbers split into separate columns.

The text files to be imported may be in any folder, if the full path is specified as the second function argument. Alternatively, the active folder will be used. For the example shown the text files were in the same folder as the spreadsheet, but to make this the active folder the file had to be saved to the same location after opening, using Save-As-Replace.

Posted in Computing - general, Excel, Link to Python, PyXLL, UDFs | Tagged , , , , , | Leave a comment