Sample Beam Calculator

Analysis of cross-sections is a fundamental requirement in structural design. Accurate cross-sectional properties are crucial for determining displacements, natural frequencies, and stresses in elements subjected to complex loading conditions. However, when it comes to calculating warping properties—especially for arbitrary or composite geometries—traditional methods fall short.

sectionproperties is an open-source Python package that changes the game. Unlike proprietary solutions like RSECTION, ShapeDesigner SaaS, or CADRE Profiler, sectionproperties is freely accessible under MIT license and offers a comprehensive API that supports both engineering practice and research. It uses the finite element method to compute geometric and warping properties, visualize stress distributions, and handle any shape or material combination, including those with internal holes. It’s an all-in-one solution providing a pre-processor, analysis engine, and post-processor.

With our GUI scripting interface, you can create a fully functional desktop application by integrating the sectionproperties library directly into your custom Excel calculations. This integration allows you to:

  • Simplify Complex Calculations: Automate the determination of geometric and warping properties for any cross-section within your custom Excel environment.
  • Boost Productivity: Leverage the familiarity of Excel combined with the analytical power of sectionproperties, packaged into a user-friendly desktop app tailored to your specific needs.
  • Secure Your Work: Protect your custom Excel calculations with password protection, ensuring that your formulas and data remain secure from unauthorized modifications.

This approach provides a seamless, customized solution for engineers who require advanced analysis without compromising on security, accessibility, or convenience.

This post is aimed to provide a concrete example of how this can be achieved and how you can transform your engineering analysis with this powerful, integrated solution. By following steps below, you would have a working app, as per screenshots below, which calculates the “reference buckling moment (Mo)” of a PFC section as per AS4100-2020. However, please note that this procedure can be similarly applied to any complex section such as light gauge steel sections.

NOTEThis example is included in the latest version of Y3A (Download).

Our sample app performs the code compliance check using an Excel spreadsheet. This method is particularly useful since most engineers are comfortable with writing Excel calculations, making it easy to distribute the development workload among team members. The Excel file contains two tabs:

  1. Calculation tab is used for calculations logic.
  2. Report tab is PDF friendly outline only including desired information.
Report tab holds a reference to the inputs and outputs of the calculation tab in a report friendly format
Calculation tab contains logic of the excel file. This tab is not included in the PDF exports.

The scripting part of our app contains:

  1. App settings such as app icon and tooltip
  2. Python functions for interfacing sectionproperties library
  3. C# functions for interfacing Excel calculation engine
  4. callbacks and layout Lua functions for defining the gui
NOTEBeing an scripting interface, Y3A processes the scripts in the order shown in the project browser. To ensure that function calls do not fail, ensure that the scripts are compiled prior to accessing their variables/functions.
i.e. place callbacks above the layout in the tree browser as callback functions must be defined prior to assigning them to the gui elements.
TestApp: App settings.
TestApp/pyFunctions: Python functions.
TestApp/csFunctions: C# functions.
TestApp/callbacks: Lua functions which are triggered at user interaction.
TestApp/layout: Lua script. Layout of the app.

First we start with defining the layout of the app. The app contains two tabs; one for defining the section geometry and the other one for setting the Excel file inputs.

NOTERefer to Documentation page for a complete list of available gui elements.
TestApp/layout
-- TestApp Layout

MyApps.TestApp.Initialize() -- call 'Initialize' function saved in the nested MyApps.TestApp table

local window = gui.app(420, 660, 'Beam Calculator')
window:callback(MyApps.TestApp.cb.Close)
MyApps.TestApp.layout.window = window

local t = gui.tabs(0, 0, 420, 620)
MyApps.TestApp.layout.tabs = {}
MyApps.TestApp.layout.widgets = {}

window:resizable(t)

-----------------------------------------------------------

local g = gui.group(0, 30, 420, 590, 'Section')

local w = 50
local d = 30
local x1 = 160
local x2 = x1 + w + x1 - 5
local y1 = 35
local y2 = y1 + d + 5
local y3 = y2 + d + 5
local y4 = y3 + d + 5
local y5 = y4 + d + 5
local y6 = y5 + d + 5

MyApps.TestApp.layout.tabs.section = {}
MyApps.TestApp.layout.tabs.section.group = g
MyApps.TestApp.layout.tabs.section.widgets = {
    d = gui.float_input(x1, y1, w, d, 'Depth (mm)'),
    b = gui.float_input(x2, y1, w, d, 'Width (mm)'),
    t_f = gui.float_input(x1, y2, w, d, 'Flange Thickness (mm)'),
    t_w = gui.float_input(x2, y2, w, d, 'Web Thickness (mm)'),
    r = gui.float_input(x1, y3, w, d, 'Root Radius (mm)'),
    n_r = gui.float_input(x2, y3, w, d, 'Radius points (no.)'),

    plotType = gui.choice(x1, y4, 420-x1-5, d, 'Plot Type:'),
    plot = gui.box(10, y6, 400, 400)
}

g:resizable(MyApps.TestApp.layout.tabs.section.widgets.plot)

MyApps.TestApp.layout.tabs.section.widgets.plotType:add('Geometry')
MyApps.TestApp.layout.tabs.section.widgets.plotType:add('Centroid')
MyApps.TestApp.layout.tabs.section.widgets.plotType:add('Mesh')
MyApps.TestApp.layout.tabs.section.widgets.plotType:tooltip('Select the type to update the plot')
MyApps.TestApp.layout.tabs.section.widgets.plotType:callback(MyApps.TestApp.cb.UpdatePlot)

g:done()

-----------------------------------------------------------

local g = gui.group(0, 30, 420, 590, 'Design')

local function AddInput(x, y, label, cell)
    local i = gui.input(x, y, 50, 30, label)
    i:callback(MyApps.TestApp.cb.NumberInput, {i, cell}) -- Pass multiple data to the callback function in a table
    return i
end

MyApps.TestApp.layout.tabs.design = {}
MyApps.TestApp.layout.tabs.design.group = g
MyApps.TestApp.layout.tabs.design.widgets = {
    E = AddInput(100, 35, 'E (MPa)', 'E'),
    G = AddInput(100, 70, 'G (MPa)', 'G'),
    le = AddInput(100, 105, 'le (mm)', 'le'),
    dummy = gui.box(415, 465, 5, 5)
}

g:resizable(MyApps.TestApp.layout.tabs.design.widgets.dummy)

g:done()

-----------------------------------------------------------

t:resizable(g)
t:done()

local o = gui.box(5, 625, 200, 30)
MyApps.TestApp.layout.widgets.message = o
o:align(ALIGN_INSIDE | ALIGN_LEFT)

local o = gui.button(290, 625, 60, 30, 'Analyse')
MyApps.TestApp.layout.widgets.analyse = o
o:tooltip('Analyse section for geometry and plastic properties')
o:callback(MyApps.TestApp.cb.AnalyseSection)

local o = gui.button(355, 625, 60, 30, 'Export')
MyApps.TestApp.layout.widgets.export = o
o:tooltip('Print report to PDF')
o:callback(MyApps.TestApp.cb.Export)

window:done()
Lua

Next we continue by defining the callback functions which are triggered when user interacts with the app.

TestApp/callbacks
-- TestApp Callbacks

MyApps = {}
-- a nested table to contain all app data used outside current scope. This helps keeping the apps organized.
MyApps.TestApp = {
    cb = {},
    data = {},
    layout = {}
}

-- these variables are only accessed here so not defined inside MyApps.TestApp
local csHelper = nil
local section = nil

MyApps.TestApp.Initialize = function()
    -- Create an instance of 'MyApps.TestApp' defined in helper C# class. Passes the excel file path and password as constructor arguments. 
    csHelper = CS.create('MyApps.TestApp', 'UserData\\Excel\\BeamAnalysis.xlsx', 'MyPassword')
end

Message = function(msg)
    MyApps.TestApp.layout.widgets.message:label(msg)
    gui.check()
end

local function Calculate()
    csHelper:Calculate()
    msg.info('Mo = ' .. csHelper:GetValue('Mo'))
    Message('Calculation successful')
end

MyApps.TestApp.cb.NumberInput = function(udata)
    local widget, cell = table.unpack(udata) -- unpack the callback data
    if widget:value() == '' then
        widget:value(csHelper:GetValue(cell))
    else
        csHelper:SetValue(cell, tonumber(widget:value()))
    end

    Calculate()
end

local function SetSectionProperties()
    csHelper:SetValue('J', tonumber(section:J()))
    csHelper:SetValue('Iw', tonumber(section:Iw()))
    csHelper:SetValue('Iy', tonumber(section:Ic()[2]))
end

MyApps.TestApp.cb.AnalyseSection = function()
    section = PY.create("PFC", 
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.d:value()),
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.b:value()),
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.t_f:value()),
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.t_w:value()),
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.r:value()),
        tonumber(MyApps.TestApp.layout.tabs.section.widgets.n_r:value()))

    section:Analyse()

    SetSectionProperties()

    Message('Successfully analysed section')
end

MyApps.TestApp.cb.UpdatePlot = function()
    local plot = MyApps.TestApp.layout.tabs.section.widgets.plot
    local plotType = MyApps.TestApp.layout.tabs.section.widgets.plotType

    if(not(plotType:changed())) then
        do return end
    end
    local type = plotType:text()

    local img_data = nil
    local w = plot:w()
    local h = plot:h()

    if type == 'Geometry' then
        img_data = section:GeometryPlot(w, h)
    elseif type == 'Centroid' then
        img_data = section:CentroidPlot(w, h)
    else
        img_data = section:MeshPlot(w, h)
    end

    local img = plot:image()
    if (img~=nil) then
        gui.remove(img)
    end
    img = gui.png_image(type, img_data)
    plot:image(img)
    plot:parent():redraw()
    Message('Plot updated')
end

MyApps.TestApp.cb.Export = function()
    local chooser = gui.native_file_chooser()
    chooser:filter(ext)
    chooser:show()
    path = chooser:filenames(1)
    if path == nil then do return end end
    csHelper:Export(path);
end

MyApps.TestApp.cb.Close = function()
    csHelper:Close()
    section:Close()
    Message('')
    MyApps.TestApp.layout.window:hide()
end
Lua

Then we create a Python class which provides a clean interface for calling sectionproperties functions inside Lua callbacks.

TestApp/pyFunctions
import io
import base64
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.pyplot import figure
from sectionproperties.pre.library import channel_section
from sectionproperties.analysis import Section

def Plot2PNG():
    buffer = io.BytesIO() # A memory storage to save the figure data
    plt.savefig(buffer, format='png')
    plt.close()
    png = buffer.getvalue() # Write figure data to the buffer
    encoded = base64.b64encode(png).decode('utf-8') # Convert binary data to string so that it can be sent back to Lua 
    return encoded

class PFC:
    def __init__(self, d, b, t_f, t_w, r, n_r): # Refer to sectionproperties documentation for codes in this section
        self.geom = channel_section(d, b, t_f, t_w, r, n_r)

    def Analyse(self): # Refer to sectionproperties documentation for codes in this section
        self.geom.create_mesh(mesh_sizes=7)
        self.sec = Section(geometry=self.geom)
        self.sec.plot_mesh(materials=False)
        self.sec.calculate_geometric_properties()
        self.sec.calculate_warping_properties(solver_type="direct")

    def SetPlotSize(self, w, h):
        png_dpi = 100
        fig, ax = plt.subplots(figsize=(w/png_dpi, h/png_dpi), dpi=png_dpi)
        return ax

    def J(self):
        return self.sec.get_j()

    def Iw(self):
        return self.sec.get_gamma()

    def Ic(self):
        return self.sec.get_ic()

    def GeometryPlot(self, w, h):
        ax = self.SetPlotSize(w, h)
        self.geom.plot_geometry(ax = ax)
        return Plot2PNG()

    def CentroidPlot(self, w, h):
        ax = self.SetPlotSize(w, h)
        self.sec.plot_centroids(ax = ax)
        return Plot2PNG()

    def MeshPlot(self, w, h):
        ax = self.SetPlotSize(w, h)
        self.sec.plot_mesh(ax = ax)
        return Plot2PNG()
        
    def Close(self):
        del self.geom
        del self.sec
Python

Finally we define a C# class to simplify Excel engine API calls from Lua callbacks.

TestApp/csFunctions
using System;
using Aspose.Cells;
using System.Collections.Generic;
using LoggerLib;
#r UserData/Net/Utilities.cs.dll

namespace MyApps
{
    public class TestApp
    {
        private readonly string path;
        private readonly Workbook workbook;
        private readonly Worksheet reportWS;
        private readonly Dictionary<string, Cell> cells;

        public TestApp(string path, string password)
        {
            this.path = path;
            LoadOptions loadOptions = new() { Password = password };
            workbook = new Workbook(path, loadOptions);
            var worksheets = workbook.Worksheets;
            reportWS = worksheets["Report"];

            cells = new Dictionary<string, Cell>
            {
                { "E", worksheets.GetRangeByName("E")[0, 0] },
                { "G", worksheets.GetRangeByName("G")[0, 0] },
                { "Iy", worksheets.GetRangeByName("Iy")[0, 0] },
                { "J", worksheets.GetRangeByName("J")[0, 0] },
                { "Iw", worksheets.GetRangeByName("Iw")[0, 0] },
                { "le", worksheets.GetRangeByName("le")[0, 0] },
                { "Mo", worksheets.GetRangeByName("Mo")[0, 0] }
            };
        }

        public double GetValue(string cellName)
        {
            if (cells.TryGetValue(cellName, out var cell))
                return cell.DoubleValue;
            throw new ArgumentException($"Invalid cell name: {cellName}");
        }

        public void SetValue(string cellName, double value)
        {
            if (cells.TryGetValue(cellName, out var cell))
                cell.PutValue(value);
            else
                throw new ArgumentException($"Invalid cell name: {cellName}");
        }

        public void Calculate()
        {
            workbook.RefreshDynamicArrayFormulas(true);
            workbook.CalculateFormula();
        }

        public void Export(string path)
        {
            PdfSaveOptions pdfSaveOptions = new()
            {
                CustomPropertiesExport = Aspose.Cells.Rendering.PdfCustomPropertiesExport.Standard
            };

            foreach (Worksheet ws in workbook.Worksheets)
                ws.IsVisible = ws == reportWS;

            workbook.Save(path, pdfSaveOptions);
        }

        public void Close()
        {
            workbook.Dispose();
        }
    }
}
C#

Once all above steps are complete, select all 4 scripts in the tree browser and click on the compile icon on the menu bar to make the app available in current session. If “Publish” option under app settings is ticked, the app would automatically compile at Y3A startup.

After running the app and setting the parameters, clicking on export would open a prompt to set a PDF export location. Refer below for the excel file and sample PDF output.