Skip to content

ValueError: Input must have at least three vertices during moment_curvature_analysis with certain moment combinations #153

@PedrooHenriq

Description

@PedrooHenriq
  • concreteproperties version: 0.7.0
  • Python version: 3.12.6
  • OS version and name: Windows 26100.4061

Issue

Of course!
Here is a well-structured text that you can submit as an issue on the concreteproperties GitHub repository — I’ll write it clearly so that the library maintainer can easily understand:


📌 Issue Title (suggestion):

ValueError: Input must have at least three vertices during moment_curvature_analysis with some moment combinations


📌 Issue Description (full text):

Hello,

First of all, thank you for this excellent library — it is very useful and well designed!

I am using concreteproperties for base plate design, running parametric analyses with varying combinations of axial force and biaxial bending moments.

I encountered an error when running moment_curvature_analysis for a specific load combination:

Mx = 25 kN.m
Mz = 5 kN.m
N  = 0

The error I get is:

ValueError: Input must have at least three vertices.
File "triangle/core.pyx", line 235, in triangle.core.triang

This happens inside the AnalysisSection creation during the moment_curvature_analysis:

self.mesh = triangle.triangulate(tri, "p")

Observations:

✅ The same code works well for other moment combinations.
✅ The geometry being analyzed is valid.
✅ The error seems to occur when the neutral axis angle is such that the remaining section after compression has too few points (degenerate polygon).


Possible cause:

  • It seems that meshed_geom sometimes ends up with fewer than 3 vertices when the section is very degraded.
  • The call to triangle.triangulate() does not internally check for this, so it raises a ValueError.

Why this matters:

In automated analyses (batch processing of combinations), having a single load combination stop the whole process is problematic.

A controlled exception or skip would allow users to proceed with the other load cases.


My code that results in error

from sectionproperties.pre.library.primitive_sections import rectangular_section
from sectionproperties.pre.library.concrete_sections import add_bar
from concreteproperties.concrete_section import (
    Concrete,
    ConcreteSection,
)
from concreteproperties.material import SteelBar
from concreteproperties.stress_strain_profile import (
    EurocodeNonLinear,
    SteelProfile,
    EurocodeParabolicUltimate,
)
import numpy as np
import pandas as pd
import math


class TensionAnalysis:

    def __init__(
        self,
        identity: str,
        width_cb: float,
        depth_cb: float,
        fck: float,
        fy: float,
        units: str,
        anchors_pos: list,
        load_list: list,
        output_dir: str
    ) -> None:
        # self.identity = identity
        self.output_dir = output_dir
        self.width = width_cb
        self.depth = depth_cb
        self.fck = fck
        self.fy = fy
        self.units = units
        self.anchors_pos = np.asarray(anchors_pos)
        self.load_list = np.asarray(load_list)

        self.define_concrete_model()
        self.define_steel_model()
        self.define_geometry()
        self.bending_conversion()
        self.analysis()
        self.print_results()
        

        
    
    def define_concrete_model(self):
        """Definição de um concreto com módulo de elasticidade e resistência
        à compressão baseados no Fck, mas com resistência nula à tração baseado
        na interação entre chapa de base e peça de concreto"""

        if self.fck <= 50:
            self.elastic_modulus = 5600 * (self.fck) ** (1 / 2)
        else:
            self.elastic_modulus = 21500 * (((self.fck / 10) + 1.25) ** (1 / 3))
            
        self.concrete_model = Concrete(
            name=f"Concreto/Graute {self.fck} MPa",
            density=2.5e-6,
            stress_strain_profile=EurocodeNonLinear(
                elastic_modulus=self.elastic_modulus,
                ultimate_strain=0.0035,
                compressive_strength=self.fck,
                compressive_strain=0.002,
                tensile_strength=1e-3,
                tension_softening_stiffness=1,
            ),
            ultimate_stress_strain_profile=EurocodeParabolicUltimate(
                compressive_strength=self.fck,
                compressive_strain=0.002,
                ultimate_strain=0.0035,
                n=2,
            ),
            flexural_tensile_strength=0,
            colour="Gray",
        )

    def define_steel_model(self):
        """Definição de aço com resistência ao escoamento Fy e módulo de
        elasticidade padrão, mas com resistência nula à compressao baseado
        na interação entre a chapa de base e os chumbadores"""

        self.steel_model = SteelBar(
            name=f"Chumbador Fy={self.fy} MPa",
            density=7.85e-6,
            stress_strain_profile=SteelProfile(
                strains=[-0.05, -0.03, -0.02, -self.fy / 200e3, 0, 100 / 100],
                stresses=[-self.fy, -self.fy, -self.fy, -self.fy, 0, 100],
                yield_strength=self.fy,
                elastic_modulus=200e3,
                fracture_strain=0.05,
            ),
            colour="darkorange",
        )

    def define_geometry(self):
        """Definição da geometria da interface entre chapa de base e peça
        de concreto como uma seção de concreto armado e chumbadores, ambos
        com as propriedades definidas anteriormente"""

        self.plate_geometry = rectangular_section(
            d=self.width, b=self.depth, material=self.concrete_model
        )
        for armadura in self.anchors_pos:
            self.plate_geometry = add_bar(
                geometry=self.plate_geometry,
                area=(math.pi * armadura[0] ** 2 * 0.25),
                material=self.steel_model,
                x=armadura[1],
                y=armadura[2],
                n=6,
            )
        

        file_name = f"{self.output_dir}\Ligação - Geometria"
        self.base_geometry = ConcreteSection(self.plate_geometry)
        self.base_geometry.plot_section(title="Ligação - Geometria", local_filename=file_name)
        
        return self.base_geometry

    def bending_conversion(self):
        """Função que calcula o ângulo da linha neutra com a horizontal
        e o valor combinado de momento no plano da seção. 
        Utiliza padrão de entrada de dados requerido pelo ConcreteSection"""

        aux: int = self.load_list.shape[0]
        self.converted_loads = np.empty((aux, 3))
        inertia_list = self.base_geometry.get_transformed_gross_properties(
        elastic_modulus= self.elastic_modulus
        )

        for x in range(aux): 

            N = self.load_list[x,0]
            Mx = self.load_list[x,1]
            Mz = self.load_list[x,2]
            
            if Mx > -1e-5 and Mx < 1e-5 and Mz > -1e-5 and Mz < 1e-5:
                self.converted_loads[x] = [None, None, None]
            else:
                # Calcula componentes da curvatura
                kappa_x = (Mx * inertia_list.ixx_c) + (Mz * inertia_list.ixy_c)
                kappa_y = (Mz * inertia_list.iyy_c) + (Mx * inertia_list.ixy_c)
     
                # Ângulo da linha neutra com a horizontal
                theta_rad = np.arctan2(kappa_y, kappa_x)
                # Momento combinado (norma do vetor Mx, My)
                m_comb = np.sqrt((Mx) ** 2 + (Mz) ** 2)

                # Armazena resultado
                self.converted_loads[x] = [N, theta_rad, m_comb]
  
        return self.converted_loads
    

    def analysis(self):
        """Função que realiza as análises para determinação das forças nos chumbadores
        e tensões máxima e mínima no concreto retornando com estes valores. Essa conversão
        é necessária devido ao padrão de input para as anáslises do ConcreteSection
        Por enquanto, a função processa apenas 1 trio por vez, mas isso será melhorado
        """
        
        aux_1: int = self.converted_loads.shape[0]
        aux_2: int = self.anchors_pos.shape[0]
        self.anchor_forces = np.empty((aux_1, aux_2))

        for x in range(aux_1):
            # Base = self.load_list[x,0]
            N = self.load_list[x,0]
            Mx = self.load_list[x,1]
            Mz = self.load_list[x,2]
            
            if Mx > -1e5 and Mx < 1e-5 and Mz > -1e5 and Mz < 1e-5:
                if N < 0:
                    for y in range(aux_2):
                        self.anchor_forces[x,y] = N / aux_2
                else:
                    self.anchor_forces[x,y] = 0.0
            else:
            
                self.mk_res = self.base_geometry.moment_curvature_analysis(
                    theta=self.converted_loads[x, 1],
                    n=self.converted_loads[x, 0],
                    progress_bar=True,)

                self.service_stress_res = self.base_geometry.calculate_service_stress(
                    moment_curvature_results=self.mk_res, 
                    m=self.converted_loads[x, 2])

                label = f""" Base -  Combinação {x+1}
                N = {N/1e+3:.02f} kN 
                Mx = {Mx/1e+6:.02f} kN.m
                Mz = {Mz/1e+6:.02f} kN.m""" 

                self.service_stress_res.plot_stress(
                    local_title=label, 
                    local_filename= f"{self.output_dir}\Base  - Combinacao {x+1}")

                aux_3 = np.asarray(self.service_stress_res.lumped_reinforcement_forces)
                    
                for y in range(aux_2):
                    self.anchor_forces[x, y] = aux_3[y, 0]
                # try:
                #     self.mk_res = self.base_geometry.moment_curvature_analysis(
                #         theta=self.converted_loads[x, 1],
                #         n=self.converted_loads[x, 0],
                #         progress_bar=True,)

                #     self.service_stress_res = self.base_geometry.calculate_service_stress(
                #         moment_curvature_results=self.mk_res, 
                #         m=self.converted_loads[x, 2])

                #     label = f""" Base -  Combinação {x+1}
                #     N = {N/1e+3:.02f} kN 
                #     Mx = {Mx/1e+6:.02f} kN.m
                #     Mz = {Mz/1e+6:.02f} kN.m""" 

                #     self.service_stress_res.plot_stress(
                #         local_title=label, 
                #         local_filename= f"{self.output_dir}\Base  - Combinacao {x+1}")

                #     aux_3 = np.asarray(self.service_stress_res.lumped_reinforcement_forces)
                        
                #     for y in range(aux_2):
                #         self.anchor_forces[x, y] = aux_3[y, 0]
                # except Exception as e:
                #     print(f"""# BUG DE GEOMETRIA - ERRO:{e}
                #           AS FORÇAS NOS CHUMBADORES VÃO SER CONSIDERADAS COMO 99.99""")
                #     for y in range(aux_2):
                #         self.anchor_forces[x, y] = 99.99

    def print_results(self):
        """Função que gera um arquivo .txt das forças nos chumbadores
        Observe que os chumbadores serão enumerados conforme a sequência
        de entrada na matriz inicial"""
        aux_1: int = self.converted_loads.shape[0]
        aux_2: int = self.anchors_pos.shape[0]

        titulo_colunas = [f"Chumb.{x+1} [N]" for x in range(aux_2)]
        titulo_linhas = [f"Comb.{y+1}" for y in range(aux_1)]

        nomearquivo = f"{self.output_dir}\CargaChumbadores.txt"
        matriz_forcas_truncadas = np.round(self.anchor_forces, decimals=1)

        with open(nomearquivo, "w", encoding="utf-8") as file:
            # Cabeçalho
            file.write("Dados;" + ";".join(titulo_colunas) + "\n")

            # Linhas com os dados
            for i, linha in enumerate(matriz_forcas_truncadas):
                linha_str = ";".join(map(str, linha))
                file.write(titulo_linhas[i] + ";" + linha_str + "\n")

        print(f"Arquivos de saída foram salvos no diretório")

        
        
dZ_cb = 250
dX_cb = 250
fck = 30
DadosFy_test = 500
unit = ["kN", "kN.m"]
anchor_props = [[22.225,35,35],
                      [22.225,215,215],
                      [22.225,35,215],
                      [22.225,215,35]]


load_list = [(0, -25e6, 1e6)]

anchor_analysis = TensionAnalysis(identity = "TESTE",
    width_cb = dZ_cb,
    depth_cb = dX_cb,
    fck = fck,
    fy = DadosFy_test,
    units=unit,
    anchors_pos = anchor_props,
    load_list = load_list,#loads_input.iloc[:,loads_input.columns.str.startswith(('B','N', 'M'))],
    output_dir = r".\teste",
    )

Thank you again for your work on this library — I hope this report is useful!

Best regards,
Pedro

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions