-
Notifications
You must be signed in to change notification settings - Fork 57
Description
- 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 aValueError
.
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