1. 项目概述:从零构建一个二维杆单元有限元分析核心
最近在整理自己过去做的一些结构分析小工具,翻到了一个挺有意思的“老物件”——一个完全从零手搓的二维杆单元有限元计算类,我把它命名为Bar2D2Node。这可不是那种调用某个商业或开源库 API 的“套壳”项目,而是从最基本的力学原理、矩阵推导,到 C++ 的类设计、算法实现,一步步自己搭建起来的。做这个东西的初衷很简单,就是想彻底搞明白有限元法(FEM)里最基础的杆单元到底是怎么“算”出来的,把书本上那些抽象的矩阵公式,变成屏幕上实实在在跑通的代码和计算结果。
对于很多刚开始接触有限元的朋友,或者像我一样有“造轮子”癖好的工程师来说,直接使用成熟的 ANSYS、Abaqus 或者 OpenSees 固然高效,但有时候就像开自动挡车,虽然能到达目的地,却少了些对发动机变速箱工作原理的掌控感。自己实现一个最简单的二维杆单元,就像是亲手组装一台模型发动机,每一个螺丝、每一根连杆都清清楚楚。它能帮你深刻理解单元刚度矩阵的组装、坐标变换、边界条件处理、方程求解这一整套有限元分析的核心流程。这个Bar2D2Node类,就是这样一个“模型发动机”。它麻雀虽小,五脏俱全,涵盖了从材料属性(弹性模量、横截面积)、几何信息(节点坐标),到形成整体刚度矩阵、施加荷载与约束,最终求解节点位移和单元内力的完整过程。无论你是想夯实有限元理论基础,还是准备面试时被问到“有限元程序最核心的步骤是什么”,亦或是想为自己的某个特定问题定制一个小型求解器,这个自研的过程都会让你受益匪浅。
2. 核心理论与算法设计思路拆解
在动手写代码之前,我们必须把理论框架搭清楚。二维杆单元,是所有有限元单元中最简单的一种,它只承受轴向力,每个单元有两个节点,每个节点有两个平动自由度(x, y方向)。我们的目标是:输入一堆这样的单元和节点信息,程序能自动算出每个节点在受力后的位移,以及每个杆件内部的内力。
2.1 单元刚度矩阵的推导:从胡克定律到矩阵形式
一切的基础是材料力学中的胡克定律:应力 = 弹性模量 × 应变。对于一根长度为 L,横截面积为 A,弹性模量为 E 的等截面直杆,其轴向刚度 k = E*A / L。在有限元中,我们需要将这种一维的杆件关系,扩展到一个二维平面坐标系中,并用矩阵来表示力与位移的关系,这就是单元刚度矩阵。
推导过程可以简化为几个关键步骤:
- 局部坐标系:首先在杆件自身的轴线方向建立局部坐标系 (x’, y’),其中 x’ 轴沿杆件方向。在局部坐标系下,杆单元只有轴向变形,其刚度矩阵非常简单,是一个 2x2 的矩阵,描述了局部坐标系下两个节点轴向位移与轴向力的关系。
- 坐标变换:我们的节点坐标和最终位移都是在全局坐标系 (x, y) 下定义的。因此,必须将通过方向余弦(杆件与全局坐标轴的夹角余弦)构造的变换矩阵 T,将局部坐标系下的刚度矩阵变换到全局坐标系下。这是二维杆单元实现中最关键的数学步骤之一。
- 全局单元刚度矩阵:经过坐标变换后,我们得到一个 4x4 的矩阵(因为每个节点有2个自由度,共4个自由度),这就是该杆单元在整体结构中的贡献,记为
[k_e]。这个矩阵是对称且奇异的(在没有约束的情况下,杆件可以发生刚体位移)。
在代码设计中,Bar2D2Node类的核心职责之一,就是根据给定的两个节点坐标 (x1, y1), (x2, y2),以及材料属性 E 和 A,实时计算出这个 4x4 的全局单元刚度矩阵。这里的一个计算技巧是,先计算杆长 L 和方向余弦 cosθ, sinθ,这些量会反复用到。
2.2 整体刚度矩阵的组装:从“单元贡献”到“系统方程”
单个单元的刚度矩阵只描述了这个单元自身的力学特性。一个结构由很多单元通过节点连接而成,我们需要将所有单元的贡献“组装”起来,形成一个描述整个结构力学特性的整体刚度矩阵 [K],并建立系统平衡方程:[K] * {U} = {F}。其中{U}是所有节点的位移向量,{F}是所有节点上的荷载向量。
组装过程是有限元程序的核心算法,其本质是“对号入座”:
- 自由度编号:为每个节点的每个自由度分配一个唯一的全局编号。例如,节点1的x方向为自由度1,y方向为自由度2;节点2的x方向为自由度3,以此类推。
- “镶嵌”操作:遍历每一个单元。对于该单元的 4x4 刚度矩阵
[k_e]中的每一个元素k_e(i, j),它关联着该单元的某两个局部自由度。我们需要根据该单元的节点编号,查找到这两个局部自由度对应的全局自由度编号,假设为m和n。然后,就将k_e(i, j)的值,累加到整体刚度矩阵[K]的第m行、第n列的位置上。 - 处理叠加:如果多个单元共享同一个节点,那么该节点对应的自由度在整体刚度矩阵中的值,就是来自所有相关单元贡献的叠加。这正好体现了结构在节点处的力平衡。
在实现上,整体刚度矩阵[K]通常是一个规模为(总节点数*2) x (总节点数*2)的大型稀疏对称矩阵。为了效率和内存,我们通常不会直接分配一个巨大的二维数组,而是使用稀疏矩阵存储格式,例如 CSR(Compressed Sparse Row)格式。但在我们这个教学性质的Bar2D2Node项目中,为了优先保证逻辑清晰,可以先使用普通的二维vector<vector<double>>来存储,待核心流程跑通后,再优化为稀疏格式。组装函数的伪代码逻辑非常清晰,就是一个多重循环。
2.3 边界条件处理与方程求解:让系统“站稳”
组装得到的整体刚度矩阵[K]是奇异的,意味着结构可以发生刚体运动(平移或旋转),方程组有无穷多解。我们必须引入边界条件来消除刚体位移,使问题有唯一解。边界条件通常以位移约束的形式给出,例如某个节点的x方向被固定(位移为0),某个节点的y方向被铰支(位移为0)。
处理位移约束最常用、最直观的方法是置“1”法(或称为直接修改法):
- 对于被约束的自由度(假设是第
d个自由度),我们将整体刚度矩阵[K]的第d行和第d列的全部非对角元素设为 0。 - 将
[K]矩阵中(d, d)这个对角线元素设为 1。 - 同时,将荷载向量
{F}中第d个元素的值,修改为指定的位移值(通常为0)。
这样做的物理意义是:强行令方程中第d个方程变为U_d = 0,同时消除了该约束自由度对其他方程的影响。处理完所有约束后,[K]就变成了一个正定矩阵,可以求解了。
方程[K]{U} = {F}是一个大型线性方程组。求解方法的选择至关重要。对于中小规模问题,直接法如 LU 分解、Cholesky 分解(针对对称正定矩阵)非常稳定可靠。对于大规模稀疏矩阵,迭代法如共轭梯度法 (CG) 更节省内存。在我们的自研项目中,可以先从简单的直接法开始,例如使用 Eigen 库的LDLT或LLT求解器,或者自己实现一个高斯消元法(用于理解原理)。这是Bar2D2Node类需要与其他线性代数求解模块交互的地方。
2.4 后处理:从位移到内力
求解得到节点位移向量{U}后,工作只完成了一半。工程师更关心的是杆件是否安全,即内力(这里是轴力)。后处理就是将全局位移“翻译”回每个单元内部响应的过程。
对于每个杆单元:
- 根据其节点编号,从全局位移向量
{U}中提取出该单元四个自由度对应的位移{u_e}。 - 利用之前计算好的方向余弦,将全局位移
{u_e}转换到局部坐标系下,得到杆件两端的轴向位移。 - 根据局部坐标系下的单元刚度关系(其实就是最简单的
力 = 刚度 * 位移差),计算杆件的轴力N = (E*A/L) * (轴向位移差)。结果为正表示拉力,为负表示压力。
至此,一个完整的静力分析流程就走通了。Bar2D2Node类需要提供接口,能够根据求解出的位移,计算并返回每个单元的轴力。
3. Bar2D2Node 类的详细设计与实现要点
有了清晰的理论路线图,我们就可以着手设计 C++ 类了。我们的目标是设计一个职责清晰、接口友好、便于扩展的类。
3.1 类的成员变量设计:如何描述一个杆单元
一个二维杆单元需要哪些基本属性?我认为以下几项是核心:
int node1_id, node2_id;:单元所连接的两个节点的全局编号。这是单元与整体模型连接的桥梁。double E;:弹性模量,材料属性。double A;:横截面积,几何属性。double length;:杆件长度。这是一个派生属性,可以根据节点坐标计算,但存储下来可以提高效率。double cos_theta, sin_theta;:杆件方向的方向余弦。同样是派生属性,但频繁使用,值得存储。std::vector<std::vector<double>> ke_global;:该单元在全局坐标系下的 4x4 刚度矩阵。可以设计成在构造时或首次请求时计算并缓存。
此外,还可以考虑存储单元的内力、应力等后处理结果。
class Bar2D2Node { private: int id_; // 单元自身编号 int node1_id_, node2_id_; // 节点编号 double E_, A_; // 材料与几何属性 double length_; // 杆长 double cx_, cy_; // 方向余弦 (cosθ, sinθ) std::array<std::array<double, 4>, 4> ke_global_; // 4x4全局单元刚度矩阵 double axial_force_; // 计算得到的轴力 public: // 构造函数:通过节点编号、材料属性、以及一个能提供节点坐标的“模型”引用或坐标本身来初始化 Bar2D2Node(int id, int n1, int n2, double E, double A, const Node& node1, const Node& node2); // 或者 Bar2D2Node(int id, int n1, int n2, double E, double A, double x1, double y1, double x2, double y2); // 核心方法 void computeGeometry(const Node& node1, const Node& node2); // 计算长度和方向余弦 const std::array<std::array<double, 4>, 4>& getGlobalStiffnessMatrix() const; // 获取单元刚度矩阵 std::array<int, 4> getGlobalDOFIndices(int dof_per_node) const; // 获取本单元自由度对应的全局索引,用于组装 double computeAxialForce(const std::vector<double>& global_displacement) const; // 根据全局位移计算轴力 // 获取器 double getAxialForce() const { return axial_force_; } // ... 其他 getters };这里有一个关键设计选择:单元是否应该存储节点坐标?我个人倾向于不存。单元只存储节点编号,坐标由外部的“模型”或“网格”类统一管理。这样更符合数据管理的单一职责原则,也避免了坐标数据在不同对象间的重复和潜在不一致。
3.2 单元刚度矩阵的计算实现
这是Bar2D2Node类的技术核心。计算过程应封装在一个独立的方法中,如computeStiffnessMatrix。
void Bar2D2Node::computeStiffnessMatrix() { // 1. 计算局部坐标系下的轴向刚度 double k_local = (E_ * A_) / length_; // 2. 局部刚度矩阵 (2x2, 对应于局部轴向位移) // [ k_local, -k_local] // [-k_local, k_local] // 但我们需要的是4x4的全局矩阵,所以通常直接构造全局矩阵 // 3. 构造全局单元刚度矩阵 ke_global_ (4x4) // 公式: ke_global = T^T * ke_local * T,其中T是变换矩阵。 // 对于杆单元,可以化简为直接使用方向余弦计算每个元素。 double C = cx_ * cx_; double S = cy_ * cy_; double CS = cx_ * cy_; double factor = k_local; ke_global_ = {0}; // 清零 // 矩阵是对称的,我们只计算上三角或按公式填充 ke_global_[0][0] = factor * C; ke_global_[0][1] = factor * CS; ke_global_[0][2] = -factor * C; ke_global_[0][3] = -factor * CS; ke_global_[1][1] = factor * S; ke_global_[1][2] = -factor * CS; ke_global_[1][3] = -factor * S; ke_global_[2][2] = factor * C; ke_global_[2][3] = factor * CS; ke_global_[3][3] = factor * S; // 利用对称性填充下三角 for(int i=0; i<4; ++i) { for(int j=0; j<i; ++j) { ke_global_[i][j] = ke_global_[j][i]; } } }注意:这里为了清晰展示了直接填充法。实际上,
ke_global的每个元素都有明确的物理意义和公式。确保你的方向余弦计算正确:cx_ = (x2-x1)/length,cy_ = (y2-y1)/length。一个常见的错误是符号弄反,导致刚度矩阵不对称或物理意义错误。
3.3 与整体求解器的交互:组装与后处理接口
Bar2D2Node类不应该自己完成整体矩阵组装和方程求解,那是“模型”或“求解器”类的工作。它需要提供清晰的接口来配合这些工作。
组装接口:
getGlobalStiffnessMatrix()返回计算好的 4x4 矩阵。同时,getGlobalDOFIndices(int dof_per_node)方法至关重要。它根据本单元的node1_id_和node2_id_,以及每个节点的自由度数(对于二维杆是2),返回一个包含4个整数的数组,指明本单元刚度矩阵中第0、1、2、3行/列分别对应整体刚度矩阵的哪一行/列。这样,求解器就可以高效地进行“对号入座”的累加操作。std::array<int, 4> Bar2D2Node::getGlobalDOFIndices(int dof_per_node) const { std::array<int, 4> dof_indices; int start1 = node1_id_ * dof_per_node; // 节点1的第一个自由度全局索引 int start2 = node2_id_ * dof_per_node; // 节点2的第一个自由度全局索引 dof_indices[0] = start1; // node1, x-dir dof_indices[1] = start1 + 1; // node1, y-dir dof_indices[2] = start2; // node2, x-dir dof_indices[3] = start2 + 1; // node2, y-dir return dof_indices; }后处理接口:
computeAxialForce(const std::vector<double>& global_displacement)是另一个核心方法。它接收求解器得到的全局位移向量,提取出本单元相关位移,进行坐标变换,并计算轴力。double Bar2D2Node::computeAxialForce(const std::vector<double>& U) const { auto dof = getGlobalDOFIndices(2); // 假设每个节点2个自由度 // 提取位移 double u1x = U[dof[0]], u1y = U[dof[1]]; double u2x = U[dof[2]], u2y = U[dof[3]]; // 计算局部坐标系下的位移差 // 局部轴向位移差 = (u2x - u1x)*cx + (u2y - u1y)*cy double delta_u_local = (u2x - u1x) * cx_ + (u2y - u1y) * cy_; // 轴力 = 轴向刚度 * 局部轴向位移差 double force = (E_ * A_ / length_) * delta_u_local; axial_force_ = force; // 存储下来 return force; }
4. 从单元到系统:完整求解流程的C++实现
有了强大的Bar2D2Node类,我们还需要一个“导演”来调度整个分析过程。这个角色通常由一个FEModel或Solver类来承担。
4.1 模型与求解器类的设计
这个类负责管理所有节点、单元,组装总刚,处理荷载和约束,调用求解器,并驱动后处理。
class SimpleFEModel2D { private: std::vector<Node> nodes_; // 节点列表,Node包含id, x, y std::vector<Bar2D2Node> elements_; // 单元列表 std::vector<std::pair<int, double>> loads_; // 荷载列表: <自由度编号, 荷载值> std::vector<int> constrained_dofs_; // 受约束的自由度编号列表 std::vector<double> global_displacement_; // 求解结果:全局位移 // 稀疏矩阵存储(简单示例用二维vector) std::vector<std::vector<double>> global_K_; std::vector<double> global_F_; public: void addNode(double x, double y); void addElement(int elem_id, int node1_id, int node2_id, double E, double A); void addLoad(int node_id, int direction /*0 for x, 1 for y*/, double value); void addConstraint(int node_id, int direction); void assembleGlobalStiffnessMatrix(); void applyLoadsAndConstraints(); bool solve(); // 求解线性方程组 void postProcess(); // 计算所有单元内力 void printResults() const; };4.2 整体刚度矩阵组装的具体实现
这是求解器中最像“搬砖”但至关重要的部分。我们使用最直观的双层循环遍历单元的方法。
void SimpleFEModel2D::assembleGlobalStiffnessMatrix() { int total_dof = nodes_.size() * 2; // 初始化总刚矩阵和荷载向量 global_K_.assign(total_dof, std::vector<double>(total_dof, 0.0)); global_F_.assign(total_dof, 0.0); for (const auto& elem : elements_) { // 1. 获取该单元的全局刚度矩阵和自由度索引 auto ke = elem.getGlobalStiffnessMatrix(); // 返回4x4数组的引用 auto dof_indices = elem.getGlobalDOFIndices(2); // 返回包含4个索引的数组 // 2. 将该单元刚度矩阵“组装”到总刚矩阵中 for (int i_local = 0; i_local < 4; ++i_local) { int i_global = dof_indices[i_local]; for (int j_local = 0; j_local < 4; ++j_local) { int j_global = dof_indices[j_local]; global_K_[i_global][j_global] += ke[i_local][j_local]; } } } }实操心得:在调试阶段,务必在组装后打印出小规模模型(如两个单元)的总刚矩阵,并与手工推导或商业软件的结果进行逐项对比。这是发现自由度编号错误、坐标变换符号错误、组装逻辑错误的最有效方法。一个3节点2单元的简单桁架是最好的调试用例。
4.3 边界条件处理与方程求解的实现
组装好总刚global_K_和荷载向量global_F_(通过addLoad方法填充)后,需要处理约束。
void SimpleFEModel2D::applyLoadsAndConstraints() { // 先施加荷载(已在addLoad中存入loads_列表,这里汇总到global_F_) for (const auto& [dof, value] : loads_) { if (dof < global_F_.size()) global_F_[dof] = value; } // 处理位移约束:使用置“1”法 for (int constrained_dof : constrained_dofs_) { // 1. 修改总刚矩阵的行和列 for (int j = 0; j < global_K_.size(); ++j) { global_K_[constrained_dof][j] = 0.0; global_K_[j][constrained_dof] = 0.0; // 保持对称性 } // 2. 将对角线元素设为1 global_K_[constrained_dof][constrained_dof] = 1.0; // 3. 将荷载向量对应位置设为约束位移值(通常为0) global_F_[constrained_dof] = 0.0; } }处理完约束后,就可以求解global_K_ * U = global_F_。对于学习目的,我们可以集成一个轻量级的线性代数库,例如Eigen。这是工业级的选择,稳定且高效。
#include <Eigen/Dense> #include <Eigen/Sparse> bool SimpleFEModel2D::solve() { int n = global_K_.size(); if (n == 0) return false; // 将 vector<vector<double>> 转换为 Eigen 矩阵 Eigen::MatrixXd K_eigen(n, n); Eigen::VectorXd F_eigen(n); for (int i = 0; i < n; ++i) { F_eigen(i) = global_F_[i]; for (int j = 0; j < n; ++j) { K_eigen(i, j) = global_K_[i][j]; } } // 使用 LDLT 分解求解(适用于对称矩阵,即使不是正定,处理约束后通常也是) Eigen::LDLT<Eigen::MatrixXd> solver; solver.compute(K_eigen); if (solver.info() != Eigen::Success) { std::cerr << "矩阵分解失败!" << std::endl; return false; } Eigen::VectorXd U_eigen = solver.solve(F_eigen); if (solver.info() != Eigen::Success) { std::cerr << "求解失败!" << std::endl; return false; } // 将结果存回 global_displacement_.assign(U_eigen.data(), U_eigen.data() + n); return true; }如果不想引入外部库,自己实现一个高斯消元法或LU分解也是一个极好的练习,但对于超过几十个自由度的系统,性能和稳定性就可能成为问题。
4.4 后处理与结果验证
求解成功后,调用每个单元的computeAxialForce方法。
void SimpleFEModel2D::postProcess() { for (auto& elem : elements_) { elem.computeAxialForce(global_displacement_); } }最后,设计一个简单的静定或超静定桁架算例进行验证。例如,一个跨中受竖向集中力的简支梁(用桁架模拟),或者一个简单的三角形桁架。将你的程序计算出的节点位移和杆件轴力,与材料力学解析解或商用软件(如 SAP2000、Midas Civil 的教育版)的结果进行比对。误差应在可接受的数值精度范围内(如1e-6量级)。这是证明你的Bar2D2Node类和整个求解流程正确性的唯一标准。
5. 常见问题、调试技巧与性能优化实录
在从零实现这个项目的过程中,我踩过不少坑,也总结出一些调试和优化的经验。
5.1 刚体位移与奇异矩阵问题
- 问题描述:在施加边界条件前,求解线性方程组时程序报错(如矩阵奇异,无法求逆)。
- 排查思路:这是最经典的问题。根本原因是结构存在刚体位移,总刚矩阵是奇异的,没有逆。
- 解决步骤:
- 检查约束:首先,也是最常见的,检查你的位移约束
addConstraint是否足够且正确。一个二维结构至少需要消除三个刚体自由度(两个平动,一个转动)。例如,一个节点固定(约束x,y),另一个节点约束垂直于杆件方向(或约束x或y),通常就够了。对于更复杂的结构,需要判断其几何不变性。 - 打印总刚矩阵:在施加约束前,将小模型的总刚矩阵打印出来。计算它的行列式(对于小矩阵)或特征值。如果存在零特征值,就对应着刚体模式。
- 可视化节点和单元:编写一个简单的函数,将节点坐标和单元连接关系输出为文件(如 CSV),然后用 Python 的 Matplotlib 或任何绘图工具画出来。肉眼检查结构是否几何稳定,约束是否施加在正确的节点和方向上。
- 检查约束:首先,也是最常见的,检查你的位移约束
5.2 计算结果与理论值或软件结果不符
- 问题描述:程序能跑通,但算出来的位移或内力差了几个数量级,或者符号不对。
- 排查思路:这是精度和逻辑错误。需要逐层排查。
- 解决步骤:
- 单元测试:单独测试
Bar2D2Node::getGlobalStiffnessMatrix()。用一个已知的杆单元(例如,沿x轴,长度1m,E=1e9, A=0.01),手工计算其全局单元刚度矩阵,与程序输出逐项对比。确保方向余弦计算正确(cx = (x2-x1)/L)。 - 检查组装:用一个两个单元共享一个节点的简单模型,手工推导总刚矩阵,与程序组装出的总刚矩阵对比。重点检查
getGlobalDOFIndices函数返回的索引是否正确。 - 检查荷载和约束向量:确认你施加的荷载值、荷载方向(正负号)、约束自由度编号是否正确。荷载向量
global_F_在组装后、约束处理前是什么样子? - 检查约束处理:施加约束后,打印被修改的行和列,看看是否按“置1法”正确修改了。有时候错误地修改了不该修改的非对角元,或者对角线元没设成1。
- 检查求解器:如果你用的是自实现的求解器,用一个小而稠密的已知线性方程组(如
[[2,1],[1,2]] * [x,y] = [5,4])来测试求解器本身的正确性。 - 后处理验证:位移解出来后,手动验证平衡。对于静定结构,可以用节点平衡方程反算杆件内力,看是否与程序计算的轴力一致。
- 单元测试:单独测试
5.3 性能瓶颈与优化方向
当节点数量成百上千时,最初的简单实现会变得非常慢,主要瓶颈在内存和计算上。
- 稀疏矩阵存储:整体刚度矩阵
global_K_非常稀疏(非零元素比例很低)。使用vector<vector<double>>存储大量零元素是巨大的浪费。应改用稀疏矩阵格式,如CSR (Compressed Sparse Row)或CSC。Eigen 库的SparseMatrix<double>是绝佳选择。组装过程变为向稀疏矩阵中插入非零元。 - 高效求解器:对于稀疏矩阵,应使用针对稀疏矩阵的求解器。Eigen 提供了
SimplicialLDLT或SparseLU用于直接法,以及ConjugateGradient等迭代法。迭代法对于大规模问题通常更快,且内存占用更少。 - 并行计算:单元刚度矩阵的计算和组装过程是相互独立的,可以很容易地用 OpenMP 进行并行化。
#pragma omp parallel for for (size_t i = 0; i < elements_.size(); ++i) { // 计算每个单元的刚度矩阵,并暂存起来 } // 注意:将暂存的结果组装到全局矩阵时,如果使用动态稀疏矩阵插入,可能需要加锁或使用线程私有的组装然后合并。 - 对象复用与内存预分配:在循环中避免频繁创建和销毁临时
std::vector或std::array。在构造函数或reserve方法中预分配足够空间。
5.4 扩展性与设计模式思考
当前的SimpleFEModel2D和Bar2D2Node耦合较紧。为了支持更多类型的单元(如梁单元、三角形单元),可以考虑以下设计:
- 抽象基类:定义一个
FiniteElement抽象基类,包含纯虚函数computeStiffnessMatrix(),getDOFIndices(),computeInternalForces()等。Bar2D2Node作为其派生类。 - 工厂模式:根据单元类型字符串或枚举,创建对应的单元对象。
- 依赖注入:将线性代数求解器抽象为接口,便于切换不同的求解库(Eigen, PETSc, MKL等)。
实现一个正确、清晰的二维杆单元求解器,是理解有限元编程精髓的完美起点。它强迫你关注从物理模型、数学公式到计算机代码的每一个转换细节。当你看到自己编写的程序成功算出一个复杂桁架的变形和内力,并与权威软件结果吻合时,那种成就感是单纯调用 API 无法比拟的。这个Bar2D2Node类,就像一把钥匙,为你打开了自主开发专用有限元分析工具的大门。