How to Create a Steel Base Plate Design Web App Using Python and Viktor

cover
6 May 2024

Introduction

Steel base plates are placed under steel columns to safely transfer the design forces to the foundations.

When steel columns bear heavy loads with small cross-sections, directly applying the loads to the foundation could lead to punching failure. Thus, it’s crucial to use a base plate beneath the column to distribute the load over a larger area.

This article provides a guide for creating a baseplate design application using Python, leveraging steel geometrical properties stored in CSV format.

The application will assist engineers in easily determining the required base plate area, thickness, concrete bearing strength, and additional width.

Furthermore, by utilizing the Viktor SDK, we can seamlessly transform our Python implementation into a web application.

This SDK offers the capability to visualize our structure in a 3D view. Let’s dive right in and take action!

Basic Principle for the Design of Baseplate

Base plate design uses the equivalent T-stub method for axial forces, where an ‘Effective area’ is calculated for T-stubs in compression, and base plate bending is assessed for T-stubs in tension. Horizontal major and/or minor shear is allowed, but no moments, i.e., pinned base design.

The first thing we calculated is the bearing strength of the supporting concrete, fjd.

The design bearing strength, fjd, between the underside of the base plate and the bedding material in the grout space is given by:

fjd = βj * α * fcd

where

βj = foundation joint material coefficient = (2/3)

α = a coefficient that accounts for the diffusion of the concentrated force within the foundation

fcd = design value concrete compressive strength = αcc * fck / ɣc

αcc = coefficient for long-term effects

fck = concrete characteristic cylinder strength

ɣc = partial safety factor for concrete

The second step is to compute the area of baseplate required which is given by the formula:

Areq = Ned / fjd

Where Ned is the ultimate load from the steel column.

The third step is to compute c which is the cantilever outstand of the effective area:

C is calculated using the formula below:

Aeff = 4 *( c**2) + Pcol * c + Acol

Hence, you Solve for c.

And finally, we compute the thickness of the baseplate, tp:

tp = c * (3 * fjd* Ym0 / _page_fy)**0.50

Python Implementation of the Baseplate Design Workflow

# Partial factor of resistance of cross-sections whatever the class is as per EN 1993-1-1.
Ym0 = 1.0

# Compute foundation bearing strength which is typically concrete

#βj is the foundation joint material coefficient, typically taken as 0.67 as per clause 6.2.5(7) in EN 1993-1-8.
beta_j=0.67

#α is a coefficient of diffusion of the vertical load being applied to the foundation. Conservatively this can be taken as 1.5
alpha= 1.5

# αcc is the coefficient that allows for long-term effects on the compressive strength of concrete vs applied actions. Taken as 0.85 in the U.K National Annex -> on page
# Define alpha_cc 
# Define gamma_c
# define fck
# γc is the partial factor of safety of concrete. Taken as 1.5 in the U.K National Annex -> on page
fjd = beta_j*alpha* (alpha_cc*fck )/gamma_c

# Compute the area of the baseplate required
Areq = (Ned *1000)/ fjd  #Ned is the Ultimate load

def calculate_c(Pcol, Acol, Areq):
#   """
#   This function calculates the value of c for the given equation:
#   Areq = 4 * c^2 + P_col * c + A_col
#   Args:
#       Perimeter_of_section: Perimeter of the column section (mm)
#       Area_of_section: Area of the column section (mm²)
#       Areq: Required area of the baseplate (mm²)
#   Returns:
#       The value of c (mm)
#   """
  a = 4
  b = Pcol
  c = Acol-Areq  # Assuming Areq is already calculated
  discriminant = b**2 - 4 * a * c
  c1 = (-b + (discriminant)**0.5) / (2 * a)
  c2 = (-b - (discriminant)**0.5) / (2 * a)
  return max(c1, c2)

c = calculate_c(Pcol,Acol,Areq)

# Compute the thickness of the baseplate (tp)
tp = c * (3 * fjd* Ym0 / _page_fy)**0.50

Finally, Turning the Python Code Into a Web Application for Baseplate Design

Input

My final application will need input features where the user can specify the following parameters:

This is achieved using the parametrization class from the Viktor SDK.

class Parametrization(ViktorParametrization):
input = Tab("Input")
input.profile_type = OptionField(
"Profile type",
options=["IPE", "HEA", "HEB"],
default="IPE",
variant="radio-inline",
flex=80,
)
input.nl1 = LineBreak()
input.profile = AutocompleteField(
"Profile",
options=get_profile_types,
default="IPE240",
description="The source of profile properties can be found [here](https://eurocodeapplied.com/design/en1993/ipe-hea-heb-hem-design-properties)",
)
# input.steel_class = OptionField(
# "Steel class", options=["S235", "S275", "S355"], default="S235"
# )
input.fck = NumberField('Fck', default=25, suffix="MPa")
input.Design_load = NumberField('Design_load', default=1000, suffix="KN")
input.acc = NumberField('Concrete coeff(acc)', default=0.85)
input.yc = NumberField('Partial factor of safety for concrete', default=1.5)
input.steel_class = NumberField('steel_class', default=255, suffix="MPa")

Output

After computation, the data view method from the Viktor SDK was used to display the result of the calculation to the user.

@DataView("Output", duration_guess=1)
def Compute_output(self, params: Munch, **kwargs):
        Ym0, beta_j, alpha, fjd, Areq, c, tp = self.calculate_plate_geom(params)

        data = DataGroup(
            DataItem("ultimate load ", params.input.Design_load, suffix="KN"),
            DataItem("beta_j", 0.67),
            DataItem("alpha", 1.5),
            DataItem("Bearing capacity of concrete support(fjd) ", round(fjd), suffix="MPa"),
            DataItem("Areq ", round(Areq), suffix="mm2"),
            DataItem(" c", round(c), suffix="mm"),
            DataItem("Thickness of plate", round(tp), suffix="mm"),
        )

        return DataResult(data)

where:

Bearing capacity of concrete(Fjd)

Area of steel required(Areq)

Additional projection of the baseplate(c)

Thickness of baseplate

Finally, a 3D model of the steel column, the baseplate, and the supporting concrete foundation is created.

A get_3dview method was defined inside the controller class which is decorated with @GeometryView.

The get_3dview method is what defines the logic for creating the 3D model and finally, returns the GeometryResult object containing the created steel column, base plate, and concrete support.

The logic is contained in the code below:

@GeometryView("3D baseplate View", duration_guess=1)
    def get_3dView(self, params: Munch, **kwargs):
        """Create geometry for column, base-plate and add a concrete slab underneath"""
        Ym0, beta_j, alpha, fjd, Areq, c, tp = self.calculate_plate_geom(params)
        concrete_thickness = 15 * tp
        steel = Material(color=Color(95, 158, 240), metalness=1)
        concrete = Material(metalness=0, roughness=1, opacity=0.6)

        h = self.get_profile_property(params.input.profile_type, params.input.profile, "Depth")
        b = self.get_profile_property(params.input.profile_type, params.input.profile, "Width")
        tw = self.get_profile_property(params.input.profile_type, params.input.profile, "Web thickness")
        tf = self.get_profile_property(params.input.profile_type, params.input.profile, "Flange thickness")
        r = self.get_profile_property(params.input.profile_type, params.input.profile, "Root radius")

        beam_profile = self.get_beam_profile(h, b, tw, tf, r)
        beam = Extrusion(beam_profile, Line(Point(0, 0, tp), Point(0, 0, 3 * h)), material=steel)

        base_plate = SquareBeam(sqrt(Areq), sqrt(Areq), tp, material=steel)  # TODO: This area doesn't seem sufficient for large column sizes
        base_plate.translate((0, 0, tp / 2))

        concrete_plate = SquareBeam(6 * h, 6 * h, concrete_thickness, material=concrete)
        concrete_plate.translate((0, 0, -concrete_thickness / 2))

        return GeometryResult([beam, base_plate, concrete_plate])

The complete code can be found below:

from math import sqrt
# import plotly.express as px
from pathlib import Path
from typing import List

import numpy as np
import pandas as pd
from munch import Munch
from viktor import ViktorController, Color
from viktor.geometry import Point, Extrusion, Line, Material, SquareBeam
from viktor.parametrization import (
    ViktorParametrization,
    OptionField,
    Text,
    Tab,
    AutocompleteField,
    LineBreak,
    NumberField
)
# from viktor.external.spreadsheet import SpreadsheetCalculation, SpreadsheetCalculationInput
from viktor.views import DataGroup, DataItem, DataResult, DataView, GeometryView, GeometryResult

def get_profile_types(params: Munch, **kwargs):
    try:
        file_path = (
                Path(__file__).parent
                / "profiles"
                / f"steel-profiles-{params.input.profile_type}.csv"
        )
        df = pd.read_csv(file_path, header=[2], skiprows=[3, 4, 5])
        return df["Profile"].values.tolist()
    except FileNotFoundError:
        return ["IPE80", "IPE100", "HEA100", "HEA120", "HEB100", "HEB120"]

def calculate_c(Pcol, Acol, Areq):
    #   """
    #   This function calculates the value of c for the given equation:
    #   Areq = 4 * c^2 + P_col * c + A_col
    #   Args:
    #       Perimeter_of_section: Perimeter of the column section (mm)
    #       Area_of_section: Area of the column section (mm²)
    #       Areq: Required area of the baseplate (mm²)
    #   Returns:
    #       The value of c (mm)
    #   """
    a = 4
    b = Pcol
    c = Acol - Areq  # Assuming Areq is already calculated
    discriminant = b ** 2 - 4 * a * c
    c1 = (-b + (discriminant) ** 0.5) / (2 * a)
    c2 = (-b - (discriminant) ** 0.5) / (2 * a)
    return max(c1, c2)

class Parametrization(ViktorParametrization):
    info = Tab("Info")
    info.text_01 = Text(
        """## Welcome to baseplate design app!

"""
    )

    input = Tab("Input")

    input.profile_type = OptionField(
        "Profile type",
        options=["IPE", "HEA", "HEB"],
        default="IPE",
        variant="radio-inline",
        flex=80,
    )
    input.nl1 = LineBreak()
    input.profile = AutocompleteField(
        "Profile",
        options=get_profile_types,
        default="IPE240",
        description="The source of profile properties can be found [here](https://eurocodeapplied.com/design/en1993/ipe-hea-heb-hem-design-properties)",
    )
    # input.steel_class = OptionField(
    #  "Steel class", options=["S235", "S275", "S355"], default="S235"
    # )
    input.fck = NumberField('Fck', default=25, suffix="MPa")
    input.Design_load = NumberField('Design_load', default=1000, suffix="KN")
    input.acc = NumberField('Concrete coeff(acc)', default=0.85)

    input.yc = NumberField('Partial factor of safety for concrete', default=1.5)
    input.steel_class = NumberField('steel_class', default=255, suffix="MPa")

class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization

    @DataView("profile geometrical Properties", duration_guess=1)
    def display_geometrical_properties(self, params: Munch, **kwargs):
        """Initiates the process of rendering an image of the bending moments of the structure,
        as well as a view of a few key values related to the bending moments."""

        # results = self.calculate_allowable_bending_moment(
        #     params.input.profile_type, params.input.profile
        # )
        results = self.get_geometrical_properties(
            params.input.profile_type, params.input.profile
        )

        data = DataGroup(

            DataItem("Depth", results["Depth"], suffix="mm"),
            DataItem("Width", results["Width"], suffix="mm"),
            DataItem("Thickness_of_web", results["Thickeness_of_web"], suffix="mm"),
            DataItem("Thickness_of_flange", results["Thickeness_of_flange"], suffix="mm"),
            DataItem("Area_col", results["Area_col"], suffix="mm2"),
            DataItem("Perimeter_col", results["Perimeter_col"], suffix="mm"),
        )

        return DataResult(data)

    def calculate_plate_geom(self, params, **kwargs):
        results = self.get_geometrical_properties(
        params.input.profile_type, params.input.profile
        )

        # Partial factor of resistance of cross-sections whatever the class is as per EN 1993-1-1.
        Ym0 = 1.0

        # Compute ultimate load (Ned) -> on page

        # Compute foundation bearing strength which is typically concrete

        # βj is the foundation joint material coefficient, typically taken as 0.67 as per clause 6.2.5(7) in EN 1993-1-8.
        beta_j = 0.67
        # α is a coefficient of diffusion of the vertical load being applied to the foundation. Conservatively this can be taken as 1.5
        alpha = 1.5

        # αcc is the coefficient that allows for long term effects on the compressive strength of concrete vs applied actions. Taken as 0.85 in the U.K National Annex -> on page

        # γc is the partial factor of safety of concrete. Taken as 1.5 in the U.K National Annex -> on page
        fjd = beta_j * alpha * (params.input.acc * params.input.fck) / params.input.yc
        # Compute area of baseplate required
        Areq = (params.input.Design_load * 1000) / fjd
        c = calculate_c(results["Perimeter_col"], results["Area_col"], Areq)
        # Compute the thickness of baseplate (tp)
        tp = c * (3 * fjd * Ym0 / params.input.steel_class) ** 0.50

        return Ym0, beta_j, alpha, fjd, Areq, c, tp

    @DataView("Output", duration_guess=1)
    def Compute_output(self, params: Munch, **kwargs):
        Ym0, beta_j, alpha, fjd, Areq, c, tp = self.calculate_plate_geom(params)

        data = DataGroup(
            DataItem("ultimate load ", params.input.Design_load, suffix="KN"),
            DataItem("beta_j", 0.67),
            DataItem("alpha", 1.5),
            DataItem("Bearing capacity of concrete support(fjd) ", round(fjd), suffix="MPa"),
            DataItem("Areq ", round(Areq), suffix="mm2"),
            DataItem(" c", round(c), suffix="mm"),
            DataItem("Thickness of plate", round(tp), suffix="mm"),
        )

        return DataResult(data)

    @staticmethod
    def get_beam_profile(h, b, tw, tf, r) -> List[Point]:
        """Generates the points which make up the chosen profile for the column cross-section"""
        # Get points for top flange
        points = [
            Point(-b / 2, (h / 2) - tf),
            Point(-b / 2, h / 2),
            Point(b / 2, h / 2),
            Point(b / 2, (h / 2) - tf),
        ]
        # Get curve for top right
        angles = np.linspace(np.pi / 2, np.pi, 10)
        x = r * np.cos(angles) + tw / 2 + r
        y = r * np.sin(angles) + h / 2 - tf - r
        for _x, _y in zip(x, y):
            points.append(Point(_x, _y))
        # Get curve for bottom right
        angles = np.linspace(-np.pi, -np.pi / 2, 10)
        x = r * np.cos(angles) + tw / 2 + r
        y = r * np.sin(angles) - h / 2 + tf + r
        for _x, _y in zip(x, y):
            points.append(Point(_x, _y))
        # Get points for bottom flange
        points.extend([
            Point(b / 2, - (h / 2) + tf),
            Point(b / 2, -h / 2),
            Point(-b / 2, -h / 2),
            Point(-b / 2, -(h / 2) + tf),
        ])
        # Get curve for bottom left
        angles = np.linspace(1.5 * np.pi, 2 * np.pi, 10)
        x = r * np.cos(angles) - tw / 2 - r
        y = r * np.sin(angles) - h / 2 + tf + r
        for _x, _y in zip(x, y):
            points.append(Point(_x, _y))
        # Get curve for top left
        angles = np.linspace(0, np.pi/2, 10)
        x = r * np.cos(angles) - tw / 2 - r
        y = r * np.sin(angles) + h / 2 - tf - r
        for _x, _y in zip(x, y):
            points.append(Point(_x, _y))
        # Repeat the first point to close the profile
        points.append(Point(-b / 2, (h / 2) - tf))

        return points

    @GeometryView("3D baseplate View", duration_guess=1)
    def get_3dView(self, params: Munch, **kwargs):
        """Create geometry for column, base-plate and add a concrete slab underneath"""
        Ym0, beta_j, alpha, fjd, Areq, c, tp = self.calculate_plate_geom(params)
        concrete_thickness = 15 * tp
        steel = Material(color=Color(95, 158, 240), metalness=1)
        concrete = Material(metalness=0, roughness=1, opacity=0.6)

        h = self.get_profile_property(params.input.profile_type, params.input.profile, "Depth")
        b = self.get_profile_property(params.input.profile_type, params.input.profile, "Width")
        tw = self.get_profile_property(params.input.profile_type, params.input.profile, "Web thickness")
        tf = self.get_profile_property(params.input.profile_type, params.input.profile, "Flange thickness")
        r = self.get_profile_property(params.input.profile_type, params.input.profile, "Root radius")

        beam_profile = self.get_beam_profile(h, b, tw, tf, r)
        beam = Extrusion(beam_profile, Line(Point(0, 0, tp), Point(0, 0, 3 * h)), material=steel)

        base_plate = SquareBeam(sqrt(Areq), sqrt(Areq), tp, material=steel)  # TODO: This area doesn't seem sufficient for large column sizes
        base_plate.translate((0, 0, tp / 2))

        concrete_plate = SquareBeam(6 * h, 6 * h, concrete_thickness, material=concrete)
        concrete_plate.translate((0, 0, -concrete_thickness / 2))

        return GeometryResult([beam, base_plate, concrete_plate])

    @staticmethod
    def get_profile_property(
            profile_type: str, profile: str, property_name: str
    ) -> float:
        """Retrieve the profile properties based on the profile type, profile and property

        :param profile_type: One of the following profile types: HEA, HEB or IPE.
        :param profile: Profile name, e.g. IPE80 (IPE was given as profile_type)
        :param property_name: The name of the property, e.g. Weight
        """
        file_path = (
                Path(__file__).parent / "profiles" / f"steel-profiles-{profile_type}.csv"
        )
        df = pd.read_csv(file_path, header=[2], skiprows=[3, 4, 5])
        return df.loc[df["Profile"] == profile, property_name].item()

    @staticmethod
    def get_geometrical_properties(
            profile_type: str, profile: str
    ):
        """Calculates the allowable bending moment based on the given parameters.

        :param profile_type: One of the following profile types: HEA, HEB or IPE.
        :param profile: Profile name, e.g. IPE80 (IPE was given as profile_type)
        :param steel_class: The steel class, e.g. S235
        :return: A dict with the moment of inertia, profile height, yield strength and allowable bending moment.
        """
        file_path = (
                Path(__file__).parent / "profiles" / f"steel-profiles-{profile_type}.csv"
        )
        df = pd.read_csv(file_path, header=[2], skiprows=[3, 4, 5])

        Depth = df.loc[df["Profile"] == profile, "Depth"].item()

        Width = df.loc[df["Profile"] == profile, "Width"].item()
        Thickeness_of_web = df.loc[df["Profile"] == profile, "Web thickness"].item()
        Thickeness_of_flange = df.loc[df["Profile"] == profile, "Flange thickness"].item()
        Area_col = df.loc[df["Profile"] == profile, "Area"].item()
        Perimeter_col = df.loc[df["Profile"] == profile, "Perimeter"].item()
        Perimeter_col = Perimeter_col * 1000

        return {

            "Depth": Depth,
            "Width": Width,
            "Thickeness_of_web": Thickeness_of_web,
            "Thickeness_of_flange": Thickeness_of_flange,
            "Area_col": Area_col,
            "Perimeter_col": Perimeter_col

        }

Conclusion

I always come across the problem of checking the baseplate thickness and the required effective area that should be provided so that the column loads can be effectively transmitted to the foundation.

Utilizing a Python script that can help me with such checks and converting the script into a shareable and easy-to-access web application using the Viktor SDK was easy and brought more efficiency into the workflow of baseplate design.

Building this type of engineering application can make you confident at your construction job site with only your mobile phone and be able to check the design accuracy of structural members like baseplates before they are fabricated and placed.