Generalidades
Los estudios paramétricos son un medio útil y eficaz para ajustar las simulaciones. Los objetivos pueden ser de distinta naturaleza: encontrar una estructura óptima con un peso mínimo para garantizar al mismo tiempo la seguridad y ahorrar recursos, adaptar la finura de la malla para obtener resultados de simulación fiables o variar las propiedades del material para comprobar la sensibilidad de la simulación y poder hacer afirmaciones fiables sobre la seguridad estructural. Para estos casos y muchos otros, es adecuado utilizar un modelado parametrizado. Para no tener que calcular todas las variantes individualmente, se puede usar la API de Dlubal. De este modo, el cálculo del estudio paramétrico puede realizarse sin necesidad de supervisión.
Modelado parametrizado
Muchas funciones en RFEM 6, RSTAB 9 y RSECTION ya se pueden parametrizar. Encontrará más información en los manuales correspondientes:
- Manuales en línea RFEM 6 | Característica del producto | Entrada paramétrica
- Manuales en línea RSTAB 9 | Característica del producto | Entrada paramétrica
- Online-Handbücher RSECTION 1 | Característica del producto | Entrada paramétrica
Acceso al modelo mediante API
Para poder acceder al modelo mediante API, debe estar instalado el paquete correspondiente. Como alternativa a un editor externo, también puede utilizar directamente la consola o el Administrador de scripts en el programa principal. Encontrará más información en los siguientes enlaces.
Ejemplo 1: Optimización de espesores de pared
Este modelo consiste en una caja simple con un cilindro cónico superpuesto. Ambas partes están hechas de chapa de acero con diferentes espesores de pared. Estos se parametrizaron para realizar un estudio paramétrico. El objetivo es encontrar la configuración cuya tensión equivalente de von Mises con un peso mínimo no supere el límite elástico. Como carga actúan el peso propio y, en un caso de carga de uso, una carga puntual por componentes sobre una superficie rígida en la parte superior del cono. El apoyo se realiza de forma articulada en dos líneas en la parte inferior de la caja. El modelo y una vista de la interfaz de usuario con la parametrización se pueden ver a continuación.
El siguiente script está dividido en secciones. En la primera sección se importan las bibliotecas utilizadas, seguido de la definición de las variables. Aquí se especifican el límite elástico con fines de visualización y el rango de variación de los parámetros, que están vinculados al espesor de la chapa. A continuación, se definen las funciones que se utilizarán posteriormente para construir la matriz de variantes y modificar los parámetros globales en RFEM 6. La parte principal del programa comienza con la construcción de la matriz de variantes de los parámetros globales. Para ello, se generan todas las posibles mutaciones a partir de las variaciones de parámetros definidas mediante el producto cartesiano. Posteriormente, se limitan de nuevo según la condición de que el espesor de pared en la zona inferior debe ser mayor o igual que el de la estructura superior. A continuación, se establece la conexión con RFEM 6 a través de la API de Dlubal. En este caso, al modelo activo con la clave API almacenada en el archivo de configuración. Ambos ajustes también se pueden especificar en la llamada a la función. Ahora se ejecuta un bucle en el que, sucesivamente, se ajustan los parámetros globales en RFEM 6 según la mutación actual, se realiza el cálculo y se leen los resultados. Para forzar un ajuste del modelo y la eliminación de los resultados, se borra la malla antes del cálculo y se regenera de nuevo. El máximo de la tensión equivalente de von Mises de todas las superficies se determina a partir de los resultados de la primera situación de cálculo. El peso total de la estructura se determina a partir de la fuerza resultante en la dirección Z en el caso de carga de peso propio. A partir de ésta, se calcula el peso dividiendo por la aceleración de la gravedad. En las dos últimas secciones, los resultados se guardan como una tabla de Excel y se representan en un diagrama de tensión frente al peso de la estructura.
"""ParaS-VMStress_Thick-min.py
Modelo RFEM 6 relacionado: "Para_Plate_Joint.rf6"
Modelo: https://www.dlubal.com/en/downloads-and-information/examples-and-tutorials/models-to-download/006159
Ejecutar un estudio paramétrico a través de la API de Dlubal RFEM, recopilar métricas de resultados, exportar y trazar resultados.
y gráfico.
Secciones:
- Importaciones
- Configuración
- Funciones
- Ejecución principal
- Gráfico
"""
# --------------------- Importaciones ---------------------
import itertools
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from dlubal.api import rfem
# --------------------- Configuración ---------------------
parameter_ranges = {
't_1': {'min': 8, 'max': 12, 'step': 2},
't_2': {'min': 4, 'max': 8, 'step': 2},
} # espesores en mm
f_y = 235 # límite elástico en MPa
# --------------------- Funciones ---------------------
def build_parameter_grid(param_ranges):
"""Construye un conjunto de datos de mutación completo a partir de valores min/max/step en mm."""
columns = []
value_lists = []
for name, bounds in param_ranges.items():
min_val = bounds['min']
max_val = bounds['max']
step = bounds['step']
if step <= 0:
raise ValueError(f"El paso para '{name}' debe ser positivo.")
if max_val < min_val:
raise ValueError(f"El valor máximo para '{name}' debe ser >= valor mínimo.")
values = np.arange(min_val, max_val + step * 0.5, step)
columns.append(name)
value_lists.append(values)
combinations = list(itertools.product(*value_lists))
return pd.DataFrame(combinations, columns=columns)
def set_glpa(dl_app, p_name, p_value,):
"""Establece el parámetro global p_name al valor especificado p_value"""
params = dl_app.get_object_list([rfem.global_parameters.GlobalParameter()])
for p in params:
if p.name == p_name:
p.value = p_value
dl_app.update_object(p)
check=True
return True
if not check:
raise ValueError(f" Error: ¡Parámetro '{p_name}' no encontrado!")
# --------------------- Ejecución principal ---------------------
# Construir conjunto de parámetros
para_dfo = build_parameter_grid(parameter_ranges)
para_dfo = para_dfo[para_dfo['t_1'] >= para_dfo['t_2']]
para_df = para_dfo / 1000 # mm -> m
print(f"Conjunto de parámetros:\n {para_dfo}")
# Conectar a RFEM con el modelo actual y ejecutar estudio paramétrico
with rfem.Application() as rf_app:
# Verificar conexión e imprimir información del modelo
app_info = rf_app.get_application_info()
print("Información de la aplicación:", app_info)
res_paras=pd.DataFrame(columns=['sigvm','sfz'])
for i in para_df.index:
# establecer parámetros globales
print(f"Establecer mutación {i} con parámetros {[x for x in para_df.loc[i]]}")
for p in para_df.columns:
# print(f"Establecer mutación {i} con parámetro {p} a {para_df.loc[i, p]}")
if not set_glpa(rf_app, p, para_df.loc[i, p]):
break
# Remallar
rf_app.delete_mesh()
rf_app.generate_mesh(skip_warnings=True)
# Ejecutar cálculo
calculation = rf_app.calculate_all(skip_warnings=True)
# Resultados
if calculation.succeeded:
# Tensiones de von Mises
res_sigvm = rf_app.get_results(
results_type=rfem.results.STATIC_ANALYSIS_SURFACES_EQUIVALENT_STRESSES_MISES_MESH_NODES,
filters=[
rfem.results.ResultsFilter(column_id='loading', filter_expression='DS1'),
]
).data
sigvm = res_sigvm['sigma_eqv_mises'].max()/10**6 # N/m^2 -> MPa
# peso propio
sfz = rf_app.get_result_table(
table=rfem.results.ResultTable.STATIC_ANALYSIS_SUMMARY_TABLE,
loading=rfem.ObjectId(no=1,object_type=rfem.OBJECT_TYPE_LOAD_CASE)
).data
sfz = float(sfz.loc[6].value) / 10 # N -> kg (g=10 m/s^2 para la aceleración de la gravedad)
res_paras.loc[i] = pd.Series({'sigvm':sigvm,'sfz':sfz})
else:
print(f"Cálculo fallido para la mutación {i} con parámetros {[x for x in para_df.loc[i]]}")
res_paras.loc[i] = pd.Series({'sigvm':np.nan,'sfz':np.nan})
# Fusionar parámetros y resultados para exportar y graficar
para_out = pd.concat([para_dfo,res_paras], axis=1)
# ---------------------- Exportar resultados ---------------------
para_out.to_excel('./ParaS-VMStress_Thick-min_results.xlsx')
# --------------------- Gráfico ---------------------
fig, ax = plt.subplots(figsize=(12, 9))
ax.scatter(para_out['sfz'], para_out['sigvm'], color='tab:blue', s=50, label='Mutaciones')
y_offset = (para_out['sigvm'].max() - para_out['sigvm'].min()) * 0.03
for x, y, mut, t1, t2 in zip(para_out['sfz'], para_out['sigvm'], para_out.index, para_out['t_1'], para_out['t_2']):
ax.text(x , y+y_offset, f"M{int(mut)}, {t1:.0f}, {t2:.0f} mm", fontsize=8, va='top', ha='center')
ax.axhline(y=f_y, color='red', linestyle='--', linewidth=1, label=f'Límite elástico ({f_y} MPa)')
ax.set_title('Tensión vs. Masa para las mutaciones de parámetros')
ax.set_xlabel('Masa [kg]')
ax.set_ylabel('Tensión de von Mises [MPa]')
ax.legend()
fig.tight_layout()
fig.savefig('./ParaS-VMStress_Thick-min_results.png', dpi=200)
plt.show()
Como se muestra en este sencillo estudio paramétrico, en comparación con la configuración inicial con espesores de pared de 12 y 8 mm, con espesores de 8 mm cada uno se puede ahorrar aproximadamente un 30% de material sin superar el límite elástico admisible. Los resultados se pueden ver en la siguiente imagen.
Ejemplo 2: Estudio de convergencia de malla
Este ejemplo ya se utilizó para el artículo técnico sobre estudios de convergencia de malla. Se trata de una lámina cilíndrica para la cual se realiza un análisis de pandeo lineal. El enlace al artículo técnico, que contiene una descripción más detallada, y el modelo correspondiente se pueden ver a continuación:
El objetivo de un estudio de convergencia de malla es adaptar la finura de la malla para que no se produzca ningún cambio relevante en los resultados, sin alcanzar un número de elementos demasiado alto, para permitir un trabajo económico. Para ello, se introdujo un parámetro global (lfe) en el presente modelo. Éste se transfirió luego al refinamiento de la malla de superficies como tamaño de malla.
En el siguiente script, el estudio de convergencia de malla se realiza mediante la modificación gradual del tamaño de malla EF deseado. Su estructura básica corresponde a la del primer ejemplo, por lo que aquí sólo se abordarán las diferencias. Para analizar la influencia de la finura de la malla en el tiempo de cálculo, éste se determina a partir del tiempo de proceso de la llamada de cálculo. Los factores de carga críticos se determinan a partir del análisis de estabilidad del primer caso de carga. A continuación, se determina el cambio del factor de carga con respecto al paso anterior para poder forzar una interrupción en caso de una variación mayor. Antes de la salida de resultados, se realiza un análisis de convergencia que evalúa el cambio relativo de los resultados.
"""ParaS-LBA_MeshConvergence-min.py
Modelo RFEM 6 relacionado: "MeshSensitivity-LBA_AlC.rf6"
Modelo: https://www.dlubal.com/en/downloads-and-information/examples-and-tutorials/models-to-download/006080
Ejecutar un estudio de convergencia de malla en RFEM barriendo el parámetro global
`l_fe`, recopilar el factor de carga crítico, comparar variantes, guardar y graficar resultados.
Secciones:
- Importaciones
- Configuración
- Ejecución principal
- Gráfico
"""
# --------------------- Importaciones ---------------------
# Librería estándar
import time
# Terceros
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from dlubal.api import rfem
# ------------------- Configuración -------------------
FE_SIZES_MM = [15, 12, 10, 9, 8, 7, 6, 5, 4, 3] # Lista de tamaños de elementos finitos a barrer en milímetros
PARAM_NAME = "l_fe" # Nombre del parámetro global en el modelo
CONVERGENCE_THRESHOLD_PCT = 1.0 # Umbral de convergencia en porcentaje
FCR_ANA = 1065 # Factor de carga crítico analítico (para referencia en el gráfico)
# ------------------- Funciones -------------------
def set_glpa(dl_app, p_name, p_value,):
"""Establece el parámetro global p_name al valor especificado p_value"""
params = dl_app.get_object_list([rfem.global_parameters.GlobalParameter()])
for p in params:
if p.name == p_name:
p.value = p_value
dl_app.update_object(p)
check=True
return True
if not check:
raise ValueError(f" Error: ¡Parámetro '{p_name}' no encontrado!")
# ------------------- Ejecución principal -------------------
with rfem.Application() as rf_app:
# Verificar conexión e imprimir información del modelo
app_info = rf_app.get_application_info()
print("Información de la aplicación:", app_info)
results = pd.DataFrame(columns=['l_fe_mm', 'n_elements', 'c_time_s', 'f_cr', 'delta_pct'])
f_prev = None
i = 0
for l_fe in FE_SIZES_MM:
i += 1
l_fe_m = l_fe / 1000.0 # Convertir mm a m para usar en RFEM
if not set_glpa(dl_app=rf_app, p_name=PARAM_NAME, p_value=l_fe_m):
break
# Remallar
rf_app.delete_mesh()
rf_app.generate_mesh(skip_warnings=True)
n_elem = rf_app.get_mesh_statistics().surface_2D_finite_elements
# Ejecutar cálculo
t0 = time.perf_counter()
calculation = rf_app.calculate_all(skip_warnings=True)
t1 = time.perf_counter()
c_time = t1 - t0
# Resultados
if calculation.succeeded:
f_cr = rf_app.get_results(
results_type=rfem.results.STABILITY_ANALYSIS_CRITICAL_LOAD_FACTORS,
filters=[
rfem.results.ResultsFilter(column_id='loading', filter_expression='LC1'),
]
).data.loc[0].f
if f_prev is not None and f_prev != 0 and f_cr is not None:
delta_pct = abs(f_cr - f_prev) / abs(f_prev) * 100.0
delta_str = f"{delta_pct:>4.2f}"
else:
delta_pct = None
delta_str = f"{'---':>4}"
if f_cr is not None:
print(f"Mutación {i}: l_fe = {l_fe:.1f} mm | n_elements = {n_elem} | f_cr = {f_cr:.2f} | delta = {delta_str} %")
results.loc[i] = pd.Series({
'l_fe_mm': l_fe, 'n_elements': n_elem, 'c_time_s': c_time,
'f_cr': f_cr, 'delta_pct': delta_pct
})
f_prev = f_cr
else:
print(f"Cálculo fallido para la mutación {i} con tamaño de malla {l_fe:.1f} mm")
results.loc[i] = pd.Series({
'l_fe_mm': l_fe, 'n_elements': n_elem, 'c_time_s': np.nan,
'f_cr': np.nan, 'delta_pct': np.nan
})
# Calcular 1 / n_elements para el segundo gráfico
results['inv_n_elements'] = 1.0 / results['n_elements']
# Calcular la desviación relativa del factor de carga crítico mínimo
results['rel_dev_f_cr_min'] = (results['f_cr'] - results['f_cr'].min()) / results['f_cr'].min() * 100
# Análisis de convergencia
print(f"\n Análisis de convergencia (umbral: < {CONVERGENCE_THRESHOLD_PCT} % cambio):")
convergence_point = None
for (l_mm, n, a, d) in results.loc[:, ['l_fe_mm', 'n_elements', 'f_cr', 'delta_pct']].itertuples(index=False):
if d is not None and d < CONVERGENCE_THRESHOLD_PCT:
convergence_point = (l_mm, n, a)
break
if convergence_point:
print(f" Convergencia alcanzada en l_fe = {convergence_point[0]:.1f} mm")
print(f" f_cr = {convergence_point[2]:.2f} | n_elements = {convergence_point[1]}")
else:
print(" Convergencia no alcanzada aún – se requiere una malla más fina.")
# ---------------------- Exportar resultados ---------------------
results.to_excel('./ParaS-LBA_MeshConvergence-min_results.xlsx')
# ------------------- Gráfico -------------------
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Gráfico 1: f_cr vs. n_elements
ax1 = axes[0, 0]
ax1.plot(results['n_elements'], results['f_cr'], 'o-', color='tab:blue', markersize=6)
ax1.axhline(y=FCR_ANA, color='red', linestyle='--', linewidth=1, label=f'$F_{{cr,ana}}$ = {FCR_ANA} kN')
ax1.set_xlabel('Número de elementos')
ax1.set_ylabel('Factor de carga crítico [kN]')
ax1.set_title('Factor de carga crítico vs. Número de elementos')
ax1.grid(True, alpha=0.3)
ax1.legend()
# Gráfico 2: f_cr vs. 1/n_elements
ax2 = axes[0, 1]
ax2.plot(results['inv_n_elements'], results['f_cr'], 'o-', color='tab:blue', markersize=6)
ax2.axhline(y=FCR_ANA, color='red', linestyle='--', linewidth=1, label=f'$F_{{cr,ana}}$ = {FCR_ANA} kN')
ax2.set_xlabel('1 / Número de elementos')
ax2.set_ylabel('Factor de carga crítico [kN]')
ax2.set_title('Factor de carga crítico vs. 1 / Número de elementos')
ax2.grid(True, alpha=0.3)
ax2.legend()
# Gráfico 3: Desviación relativa vs. Tiempo de cálculo
ax3 = axes[1, 0]
ax3.plot(results['c_time_s'], results['rel_dev_f_cr_min'], 'o-', color='tab:blue', markersize=6)
ax3.set_xlabel('Tiempo de cálculo [s]')
ax3.set_ylabel('Desviación relativa [%]')
ax3.set_title('Desviación relativa vs. Tiempo de cálculo')
ax3.grid(True, alpha=0.3)
# Gráfico 4: Desviación relativa vs. l_fe_mm
ax4 = axes[1, 1]
ax4.plot(results['l_fe_mm'], results['rel_dev_f_cr_min'], 'o-', color='tab:blue', markersize=6)
ax4.set_xlabel('Tamaño de malla EF [mm]')
ax4.set_ylabel('Desviación relativa [%]')
ax4.set_title('Desviación relativa vs. Tamaño de malla EF')
ax4.grid(True, alpha=0.3)
fig.suptitle('Análisis de convergencia de malla', fontsize=14, fontweight='bold')
fig.tight_layout()
fig.savefig('./ParaS-LBA_MeshConvergence-min_results.png', dpi=200)
plt.show()
La siguiente imagen muestra los diagramas generados por el script del programa, tal como se presentan y evalúan con más detalle en el artículo técnico mencionado al principio.