26x
002055
2026-06-25

参数研究借助Dlubal应用程序编程接口

这篇技术文章通过两个例子展示了如何通过定义全局参数和Dlubal API来自动化进行参数研究。

概述

参数研究是一种调整模拟的有效且可靠的方法。其目标可能多种多样:找到重量最小、能同时保证安全并节约资源的最佳结构,调整网格密度以获得可靠的模拟结果,或改变材料属性以检验模拟的敏感性,并能够就结构安全性做出可靠的判断。对于这些情况以及许多其他应用,参数化建模非常适用。为了无需手动计算所有变体,可以使用 Dlubal 应用程序编程接口。通过它可以在无人值守的情况下自动完成参数研究的计算。

参数化建模

RFEM 6、RSTAB 9 和 RSECTION 中的许多功能已经可以参数化。更多信息可在相关手册中找到:

通过应用程序编程接口访问模型

为了能够通过应用程序编程接口访问模型,必须安装相关的软件包。您也可以直接在主程序中使用控制台或脚本管理器,而不是外部编辑器。更多信息可通过以下链接获取。

示例 1:壁厚优化

该模型由一个简单的箱体和一个装配的锥形圆柱体组成。两个部分均由钢板制成,但壁厚不同。这些壁厚已被参数化,以进行参数研究。目标是在重量最小的前提下,找到一种配置,使其冯·米塞斯等效应力不超过屈服强度。荷载包括自重以及在一个活荷载工况中,作用于圆锥顶部刚性面上的分量式集中荷载。支撑为位于箱体底部两条线上的铰接支撑。模型和带有参数化的用户界面视图如下所示。

下面的脚本分为几个部分。第一部分导入所使用的库,然后是变量的定义。这里指定了用于可视化目的的屈服强度以及要改变的参数的跨度,这些参数与板厚相关联。接着定义了功能,这些功能将在后续流程中用于构建设计变体矩阵以及更改 RFEM 6 中的全局参数。 主程序部分从构建全局参数的设计变体矩阵开始。为此,通过笛卡尔积从定义的参数变化中生成所有可能的变体。随后,这些变体将根据条件进一步限制,即下部区域的壁厚必须大于或等于上部结构的壁厚。 然后通过 Dlubal 应用程序编程接口建立与 RFEM 6 的连接。在本例中,是使用配置文件中存储的应用程序编程接口密钥连接到活动模型。这两项设置也可以在函数调用中指定。 现在进入一个循环,在其中依次根据当前的变体调整 RFEM 6 中的全局参数,执行计算并读取结果。为了强制调整模型并删除结果,在计算前会删除网格并重新生成。所有面的最大冯·米塞斯等效应力从第一个设计状况的结果中确定。结构的总自重从自重荷载工况中的 Z 方向合力得出。通过除以重力加速度,从该合力计算出重量。 在最后两个部分,结果被保存为 Excel 表格,并绘制成应力与结构重量的关系图。


"""ParaS-VMStress_Thick-min.py

Related RFEM 6 model: "Para_Plate_Joint.rf6"
Model: https://www.dlubal.com/en/downloads-and-information/examples-and-tutorials/models-to-download/006159

Run a parameter study via the Dlubal RFEM API, collect result metrics, export and plot results.
and chart.

Sections:
- Imports
- Configuration
- Functions
- Main execution
- Plotting
"""
# --------------------- Imports ---------------------
import itertools
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from dlubal.api import rfem


# --------------------- Configuration ---------------------
parameter_ranges = {
    't_1': {'min': 8, 'max': 12, 'step': 2},
    't_2': {'min': 4, 'max': 8, 'step': 2},
} # thicknesses in mm
f_y = 235  # yield strength in MPa

# --------------------- Functions ---------------------
def build_parameter_grid(param_ranges):
    """Build a complete mutation dataset from min/max/step values in 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"Step width for '{name}' must be positive.")
        if max_val < min_val:
            raise ValueError(f"Max value for '{name}' must be >= min value.")
        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,):
    """Sets the global parameter p_name to the specified value 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: Parameter '{p_name}' not found!")

# --------------------- Main execution ---------------------
# Build parameter set
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"Parameter set:\n {para_dfo}")

# Connect to RFEM with current model and run parameter study
with rfem.Application() as rf_app:
    # Check connection and print model info
    app_info = rf_app.get_application_info()
    print("Application Info:", app_info)
    res_paras=pd.DataFrame(columns=['sigvm','sfz'])
    for i in para_df.index:
            # set global parameters
            print(f"Set mutation {i} with parameters {[x for x in para_df.loc[i]]}")
            for p in para_df.columns:
                # print(f"Set mutation {i} with parameter {p} to {para_df.loc[i, p]}")
                if not set_glpa(rf_app, p, para_df.loc[i, p]):
                    break
            # Remesh
            rf_app.delete_mesh()
            rf_app.generate_mesh(skip_warnings=True)
            # Run calculation
            calculation = rf_app.calculate_all(skip_warnings=True)
            # Results
            if calculation.succeeded:
                # Von Mises stresses
                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                
                # self-weight
                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 for gravity acceleration)

                res_paras.loc[i] = pd.Series({'sigvm':sigvm,'sfz':sfz})
            else:
                print(f"Calculation failed for mutation {i} with parameters {[x for x in para_df.loc[i]]}")
                res_paras.loc[i] = pd.Series({'sigvm':np.nan,'sfz':np.nan})

# Merge parameters and results for export and plotting
para_out = pd.concat([para_dfo,res_paras], axis=1)

# ---------------------- Export Results ---------------------
para_out.to_excel('./ParaS-VMStress_Thick-min_results.xlsx')

# --------------------- Plotting ---------------------
fig, ax = plt.subplots(figsize=(12, 9))
ax.scatter(para_out['sfz'], para_out['sigvm'], color='tab:blue', s=50, label='Mutations')
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'Yield Strength ({f_y} MPa)')
ax.set_title('Stress vs. Mass for Parameter Mutations')
ax.set_xlabel('Mass [kg]')
ax.set_ylabel('Von Mises Stress [MPa]')
ax.legend()
fig.tight_layout()
fig.savefig('./ParaS-VMStress_Thick-min_results.png', dpi=200)
plt.show()

这个简单参数研究的结果表明,与壁厚为 12 毫米和 8 毫米的初始配置相比,采用 8 毫米的壁厚,可以在不超过允许屈服强度的情况下节省约 30% 的材料。结果可以在下图中查看。

提示

在满足设计要求的同时通过减小总质量进行优化,也可以通过将“模型优化”模块与适当的设计模块结合使用来实现,无需任何编程知识:

示例 2:网格收敛研究

此示例已在关于网格收敛研究的专业文章中采用。这是一个进行线性屈曲分析的圆柱壳。包含更详细描述的专业文章链接和相关模型如下所示:

网格收敛研究的目标是调整网格密度,在结果不再发生显著变化的同时,避免单元数量过多,从而保证工作经济可行。为此,在现有模型中引入了一个全局参数 (lfe)。然后,它将作为网格尺寸传递给面网格细化。

提示

全局网格设置也可以直接通过应用程序编程接口进行调整,而无需使用全局参数:

在下面的脚本中,通过逐步改变目标有限元网格尺寸来进行网格收敛研究。其基本结构与第一个示例相同,因此这里仅说明不同之处。 为了分析网格密度对计算时间的影响,计算时间从计算调用过程中的进程时间确定。临界荷载系数从第一个荷载工况的稳定性分析中确定。接着,确定临界荷载系数相对于上一步的变化,以便在变化幅度较大时强制中止计算。在结果输出之前进行了收敛分析,评估结果的相对变化。


"""ParaS-LBA_MeshConvergence-min.py

Related RFEM 6 model: "MeshSensitivity-LBA_AlC.rf6"
Model: https://www.dlubal.com/en/downloads-and-information/examples-and-tutorials/models-to-download/006080

Run a mesh convergence study in RFEM by sweeping the global parameter
`l_fe`, collect critical load factor, compare variants, save and plot results.

Sections:
- Imports
- Configuration
- Main execution
- Plotting
"""

# --------------------- Imports ---------------------
# Standard library
import time

# Third-party
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from dlubal.api import rfem

# ------------------- Configuration -------------------
FE_SIZES_MM = [15, 12, 10, 9, 8, 7, 6, 5, 4, 3] # List of finite element sizes to sweep in millimetres
PARAM_NAME = "l_fe"                             # Name of the global parameter in the model
CONVERGENCE_THRESHOLD_PCT = 1.0                 # Convergence threshold in percentage
FCR_ANA = 1065                                  # Analytical critical load factor (for reference in the plot)

# ------------------- Functions -------------------
def set_glpa(dl_app, p_name, p_value,):
    """Sets the global parameter p_name to the specified value 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: Parameter '{p_name}' not found!")
    
# ------------------- Main execution -------------------
with rfem.Application() as rf_app:
    # Check connection and print model info
    app_info = rf_app.get_application_info()
    print("Application Info:", 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  # Convert mm to m for use in RFEM
        if not set_glpa(dl_app=rf_app, p_name=PARAM_NAME, p_value=l_fe_m):
            break
        # Remesh
        rf_app.delete_mesh()
        rf_app.generate_mesh(skip_warnings=True)
        n_elem = rf_app.get_mesh_statistics().surface_2D_finite_elements
        # Run calculation
        t0 = time.perf_counter()
        calculation = rf_app.calculate_all(skip_warnings=True)
        t1 = time.perf_counter()
        c_time = t1 - t0
        # Results
        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"Mutation {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"Calculation failed for mutation {i} with mesh size {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
                })

# Calculate 1 / n_elements for the second plot
results['inv_n_elements'] = 1.0 / results['n_elements']
# Calculate relative deviation from minimum critical load factor
results['rel_dev_f_cr_min'] = (results['f_cr'] - results['f_cr'].min()) / results['f_cr'].min() * 100

# Convergence analysis
print(f"\n  Convergence analysis (threshold: < {CONVERGENCE_THRESHOLD_PCT} % change):")
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"  Convergence reached at l_fe = {convergence_point[0]:.1f} mm")
    print(f"  f_cr = {convergence_point[2]:.2f}  |  n_elements = {convergence_point[1]}")
else:
    print("  Convergence not reached yet – finer mesh required.")

# ---------------------- Export Results ---------------------
results.to_excel('./ParaS-LBA_MeshConvergence-min_results.xlsx')

# ------------------- Plotting -------------------
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Plot 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('Number of Elements')
ax1.set_ylabel('Critical Load Factor [kN]')
ax1.set_title('Critical Load Factor vs. Number of Elements')
ax1.grid(True, alpha=0.3)
ax1.legend()

# Plot 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 / Number of Elements')
ax2.set_ylabel('Critical Load Factor [kN]')
ax2.set_title('Critical Load Factor vs. 1 / Number of Elements')
ax2.grid(True, alpha=0.3)
ax2.legend()

# Plot 3: Relative Deviation vs. Calculation Time
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('Calculation Time [s]')
ax3.set_ylabel('Relative Deviation [%]')
ax3.set_title('Relative Deviation vs. Calculation Time')
ax3.grid(True, alpha=0.3)

# Plot 4: Relative Deviation 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('FE Mesh Size [mm]')
ax4.set_ylabel('Relative Deviation [%]')
ax4.set_title('Relative Deviation vs. FE Mesh Size')
ax4.grid(True, alpha=0.3)

fig.suptitle('Mesh Convergence Analysis', fontsize=14, fontweight='bold')
fig.tight_layout()
fig.savefig('./ParaS-LBA_MeshConvergence-min_results.png', dpi=200)
plt.show()

下图显示了由程序脚本生成的图形,这些图形也在开头提到的专业文章中展示并进行了更详细的评估。


作者

Marc 任职于产品工程部门,专注岩土工程,同时兼顾客户支持工作。他凭借专业经验,能够有效参与并解决复杂的技术问题。

链接


;