用FreeCAD做刚体和流体的物理仿真
Table of contents
太长不读版:把FreeCAD插件CfdOF用到的软件用Nix打包好,得到可靠的软件环境后,分别做了亚克力弹性组件和导风漏斗的物理仿真。
大量篇幅给了nix软件打包,想读物理仿真实战可以直接从目录跳过去。
一切的起源:自制导风漏斗反射了风扇气流¶
想用12cm机箱风扇做一个排风系统,直接用FreeCAD凭感觉建了个导风漏斗模型,但没想到用3D打印的实物测试发现,风扇的气流几乎都从扇叶末端的缝隙中反射出来了,变得很诡异。
FreeCAD作为一款强大的参数建模工具,。据闻有一些公司在基于FreeCAD做二次开发。听从业网友说,“SolidWorks能做的FreeCAD也能做,做不了的都不能做,平时工作用UG多,FreeCAD缺点在运行卡顿”。这个软件也很自然地成为了以ArchLinux为主力操作系统的我所使用的机械设计软件。
我以前用FreeCAD做过亚克力模型的刚体仿真,自然而然就会想怎样用FreeCAD做流体仿真。实际上我几乎没有流体力学相关知识,想先把软件环境搭建起来,之后应该船到桥头自然直吧!
首先是失败的工作环境配置尝试!¶
我使用的是ArchLinux。FreeCAD在官方软件源里有,直接可以装,CfdOF工作台也可以通过Addon-Manager安装,所以比较麻烦的地方是安装CfdOF依赖的流体计算软件。
CfdOF依赖的流体分析相关软件包括:
- OpenFOAM,流体动力学计算软件
- cfmesh,算是OpenFOAM的附件,CfdOF维护者(oliveroxtoby)提供了一份能配合最新OpenFOAM编译的版本
- hisa,也算是OpenFOAM的附件,源码貌似也源自CfdOF维护者(oliveroxtoby)另外提供的渠道,但是改没改过不知道
初次尝试:直接在Arch上安装OpenFOAM AUR包¶
作为忠实的Arch用户,我一定会去看AUR的。不过由于见到过AUR包捆绑了一堆“垃圾”的案例,我还是对AUR包进行了审核。
一看有不少OpenFOAM的用户软件包:openfoam-org
、openfoam-com
、openfoam-com-precice
、openfoam-com-git
、openfoam-9-org
。
怎么办呢,直接看最近更新过的把,想办法装新的。
去看openfoam-org
的评论区,看到有人报编译问题,尚无明确解答。此外看到软件维护者置顶了说,在arch4edu
仓库(类似archlinuxcn
)有预编译版本。再一看软件编译依赖还有其它的AUR包,我有些不愿意自己编译。
我知道能用yay
、paru
之类的装,但是看了下OpenFOAM官方仓库编译说明里写的1小时编译时间,我选择暂时搁置这一路径。
尝试偷懒:希望能直接用arch4edu
仓库编译好的软件包¶
参考arch4edu的官方说明,我给自己的Arch加上了arch4edu仓库。随后我直接就装上了openfoam-org
。
走这步需要在终端开FreeCAD,因为OpenFOAM套件环境要source一个脚本加载。软件包也在/etc/profile.d
加了那个脚本,但我暂时不想重新登陆。先试试,按下面列出的指令操作:
source /etc/profile.d/openfoam-11.sh
ofoam # 这是个加载环境的指令,实际是 source /opt/OpenFOAM/OpenFOAM-11/etc/bashrc
freecad
暂时不管加载环境时可能的报错(后面发现也不用管),加载CfdOF工作台后,进入preferences,左侧栏选择CfdOF,在其中找到Run dependency checker
的按钮,点击检查运行环境。告诉我这个:
Checking CFD workbench dependencies...
Checking FreeCAD version
FreeCAD version: 0.21
Checking for OpenFOAM:
System: Linux
Runtime: Posix
OpenFOAM directory: /opt/OpenFOAM/OpenFOAM-11
Running echo $WM_PROJECT_VERSION
Executing: echo $WM_PROJECT_VERSION
11
OpenFOAM installation found, but unable to run command: 'PySide6.QtCore.QProcess' object has no attribute 'Timedout'
命令行显示的错误:
File "/home/user/.local/share/FreeCAD/Mod/CfdOF/./CfdOF/CfdConsoleProcess.py", line 153, in waitForFinished
if self.process.error() != self.process.Timedout:
^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'PySide6.QtCore.QProcess' object has no attribute 'Timedout'
Why? 搞不明白。怎么会有个Python+QT的报错?总之这看上去很不妙啊……这条路大概率行不通,或者走到最后会很麻烦(´_>`)。
果断把相关包删了,把arch4edu移除,也不想试别的了。因为我知道有一种方法能更稳定地复现环境——nix
。
完美可复现的开发环境:使用nix-shell¶
这一步也一波三折,但还是成功了!
同类项目
当我完成这个项目后,我才发现有人做了类似的工作,且还在维护。preCICE adapters and solvers packaged with the Nix package manager,感兴趣可以一看。不过它用了Nix的实验性功能flakes(这功能长久没稳定下来),可能没法直接给nix-shell用(不过可以看看nix shell
)。
我有一段时间用过NixOS,它的完全可复现性给我留下了深刻印象。不过我还是用不惯NixOS,在临时要改一些细小配置时显得太麻烦。
本节内容只是过程记录
想使直接使用完成的环境,请看文末总结与补充说明提到的git仓库。本节存在一定错误尝试记录,切勿盲从(除非想一起踩一遍坑)。
nix-shell & nix-env
貌似nix-shell、nix-env二者有不少相似之处,只不过后者是持久化的。后续感兴趣可以研究下。
基本的nix-shell环境¶
官方教程用shell.nix的实现声明式shell环境写得不错,比直接在搜索引擎里搜nix-shell tutorial
找到的东西靠谱。正式写配置前最好再通读一遍nix基本语法。
由于搜索引擎的搜索质量不佳,我一开始这样写nix:
# freecad-cfd.nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = [ pkgs.freecad ];
}
但我想,这样还得在系统里配置nix-channel,不算完全固定的配置,于是根据前面提到的教程改成了下面这样:
# freecad-cfd.nix
let
nixpkgs_ball = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
sha256 = "1q7y5ygr805l5axcjhn0rn3wj8zrwbrr0c6a8xd981zh8iccmx0p";
};
pkgs = import nixpkgs_ball { config = { allowUnfree = true; }; };
in
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = [ pkgs.freecad ];
}
区别在于,前者需要输入参数,后者不接受参数并自行定义了用到的数据(从网络获取,并锁定了内容的哈希值)。
如何获取内容哈希值
nix计算哈希的方法和命令行工具(sha256sum)不同。对于fetchTarball
这种会自动解压的操作,可以使用nix-prefetch-url --unpack $url
命令来获取内容哈希值。当然也可以直接写个错的哈希值,看错误报告写的实际哈希值是什么(可能会以哈希类型-
开头,比如sha256-
,可以一起复制),再添进配置。
同理把nixgl也在下面加上,这个我也是读了前述教程才明白怎么配置。
# freecad-cfd.nix
let
nixpkgs_ball = fetchTarball {
url = "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
sha256 = "1q7y5ygr805l5axcjhn0rn3wj8zrwbrr0c6a8xd981zh8iccmx0p";
};
nixgl_ball = fetchTarball {
url = "https://github.com/nix-community/nixGL/tarball/310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
sha256 = "1crnbv3mdx83xjwl2j63rwwl9qfgi2f1lr53zzjlby5lh50xjz4n";
};
pkgs = import nixpkgs_ball { config = { allowUnfree = true; }; };
nixgl = import nixgl_ball {};
in
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = [
pkgs.freecad
nixgl.nixGLIntel # Mesa OpenGL implementation (intel, amd, nouveau, ...).
];
# 看到nixgl仓库有人开issue提到了这个,nix-shell workaround
shellHook = ''
export LD_LIBRARY_PATH=$(nixGLIntel printenv LD_LIBRARY_PATH):$LD_LIBRARY_PATH
'';
}
nixGL wrapper的选择
可以读一下nixGL官方仓库的介绍,选用合适自己的wrapper。此处所用的是适用于Intel显卡、AMD显卡、或Nouveau驱动下NVIDIA显卡的wrapper。
用下面的指令构建并进入所配置的shell环境,运行FreeCAD,如果FreeCAD正常运行就算小功告成!
# 运行后会构建并打开一个shell环境
nix-shell freecad-cfd.nix
# 要让图形系统正常工作,需要套个nixgl
nixGLIntel freecad
打包OpenFOAM¶
看了OpenFOAM官方的编译说明,挺简洁的,但是没写编译环境依赖。于是我参考两个AUR包(主要是openfoam-com
)、在nix这边复(照)现(抄)了一遍编译流程。除了kahip
、adios
之类的辅助工具尚没有在nixpkgs里的包,其它基本都有。由于nix包的位置不固定,需要多写一小段shell脚本打印openfoam的环境位置,方便其它程序使用。我直接在nixpkgs里找包来抄stdenv.mkDerivation
的用法。
# openfoam/default.nix
{ lib, stdenv
, cmake
, boost
, bzip2
, cgal
, fftw
, flex
, openmpi
, paraview
, parmetis
, scotch
, zlib
}:
stdenv.mkDerivation rec {
pname = "openfoam";
version = "2312";
src = fetchTarball {
url = "https://develop.openfoam.com/Development/openfoam/-/archive/OpenFOAM-v${version}/openfoam-OpenFOAM-v${version}.tar.gz";
};
buildInputs = [ boost cgal paraview parmetis scotch openmpi fftw zlib ];
nativeBuildInputs = [ cmake bzip2 flex ];
configurePhase = ''
echo "# Preferences for arch-linux
export WM_COMPILER_TYPE=system
export WM_MPLIB=SYSTEMOPENMPI
# End" \
> etc/prefs.sh
./bin/tools/foamConfigurePaths \
-adios adios-system \
-boost boost-system \
-cgal cgal-system \
-fftw fftw-system \
-metis metis-system \
-paraview paraview-system \
-scotch scotch-system \
;
'';
buildPhase = ''
export FOAM_CONFIG_MODE="o"
unset FOAM_SETTINGS
source ./etc/bashrc || echo "Ignore spurious sourcing error"
./Allwmake -j
wclean all
wmakeLnIncludeAll
'';
installPhase = ''
install -d $out/opt/OpenFOAM/ThirdParty-v${version} $out/etc/profile.d
cp -r $(pwd) $out/opt/OpenFOAM/OpenFOAM-v${version}
chmod -R 755 $out/opt/OpenFOAM/OpenFOAM-v${version}/bin
chmod 755 $out/opt/OpenFOAM/OpenFOAM-v${version}/etc/*
install -d $out/bin
echo 'echo $(readlink -f "$(dirname $0)/../opt/OpenFOAM/OpenFOAM-v${version}")' | \
install -Dm755 /dev/stdin $out/bin/openfoam-home.sh
'';
meta = with lib; {
description = "The free, open source CFD software developed primarily by OpenCFD Ltd since 2004";
homepage = "https://www.openfoam.com/";
license = licenses.gpl3;
platforms = platforms.all;
};
}
然后把这个包加到freecad-cfd.nix里:
# freecad-cfd.nix
let
...
openfoam = pkgs.callPackage (import ./openfoam) { };
...
in
pkgs.mkShell {
buildInputs = [
...
openfoam
...
];
...
}
在中间buildPhase遇到了脚本明明存在却无法执行的问题,提示required file not found
,感觉很奇怪,打开对应文件一看:
#! /bin/sh
...
<(=╯▽╰=)>原来是shebang的问题……要时刻记住nix不遵守FHS,可我该怎么修呢?好消息是nix环境下有提供patch shebang的工具:patchShebangs
,它能自动递归地修补所有检测到的shebang。本着最小化修改的原则,我找出了必须修改的脚本,在configurePhase的开头加上这些修改流程:
# openfoam/default.nix
configurePhase = ''
patchShebangs wmake/w*
patchShebangs wmake/scripts
patchShebangs Allwmake
...
''
编译完后运行CfdOF的依赖检测,告诉我尚不支持OpenFOAM 2312版本。我在此时才注意到了CfdOF还依赖另外两个工具cfmesh
和hisa
(此前都没察觉到)。
仔细看了看CfdOF的说明文档:
Prerequisites:
- OpenFOAM Foundation versions 9-10 or ESI-OpenCFD versions 2006-2306
顺便看看arch4edu里的openfoam软件包的版本:
arch4edu/openfoam-com v2312-1
The open source CFD toolbox (www.openfoam.com)
arch4edu/openfoam-org 11.20240116-1
The open source CFD toolbox (www.openfoam.org)
这也不对!那也就更没必要回去看AUR包的方法了。Arch这点是这样,软件只有最新,有点烦。
我需要降级!还好此时的降级只是把版本号、源码包、源码包的hash调整一下。借机还可以把版本号和hash参数化:
# openfoam/default.nix
{...
, version ? "2306"
, hash ? "1z0sna5jxlyfz8s7vi28m47iwjjjbzg9ycz5maz2gymchg7lw6v7"
}:
stdenv.mkDerivation rec {
pname = "openfoam";
inherit version;
foam_hash = hash;
src = fetchTarball {
url = "https://develop.openfoam.com/Development/openfoam/-/archive/OpenFOAM-v${version}/openfoam-OpenFOAM-v${version}.tar.gz";
sha256 = foam_hash;
};
...
}
OpenFOAM的部分就差不多搞定了。最后在shellHook里加上自动加载OpenFOAM配置的代码:
# freecad-cfd.nix
shellHook = ''
...
source ${openfoam}/opt/OpenFOAM/OpenFOAM-v${openfoam.version}/etc/bashrc || true
...
'';
更多修补¶
到这里本来都编译正常,但是后面跑CfdOF例程时出了状况:
Decomposing mesh
Create mesh
Calculating distribution of cells
Decomposition method scotch [4] (region region0)
Selecting decompositionConstraint preserveBaffles
preserveBaffles : setting constraints to preserve baffles
--> FOAM FATAL ERROR: (openfoam-2306)
Attempted to use <scotch> without the scotchDecomp library loaded.
This message is from the dummy scotchDecomp stub library instead.
Please install <scotch> and ensure libscotch.so is in LD_LIBRARY_PATH.
The scotchDecomp library can then be built from src/parallel/decompose/scotchDecomp.
Dynamically loading or linking this library will add <scotch> as a decomposition method.
From virtual Foam::labelList Foam::scotchDecomp::decompose(const Foam::polyMesh&, const Foam::labelList&, const Foam::pointField&, const Foam::scalarField&) const
in file dummyScotchDecomp.C at line 111.
FOAM exiting
缺少scotch
的动态库,疑似需要调整scotch的cmake参数。检查结果发现nix上有两个相关的库(scotch
,parmetis
)都只是按静态链接库编译,所以OpenFOAM动态链接了个寂寞。
调整两个包之后再次尝试编译OpenFOAM,发现依赖还是在编译期间用上。不得已检查了OpenFOAM的构建系统是如何判断库的位置。先从既有编译日志的“错误”信息入手搜索:
=> skip scotch (no header)
直接搜索skip scotch
,这么巧,一次就给我找到了:
# Search
# $1 : prefix (*_ARCH_PATH, system, ...)
#
# On success, return 0 and export variables
# -> HAVE_SCOTCH, SCOTCH_ARCH_PATH, SCOTCH_INC_DIR, SCOTCH_LIB_DIR
search_scotch() {
...
}
接着找调用它的代码,最后确定它的搜索行为受SCOTCH_ARCH_PATH
变量的影响。搜索这个变量发现,bin/tools/foamConfigurePaths
这个脚本会设置它。到最后只是我的编译配置阶段写错了么……其它库也得一起改,所以正确的配置是:
# freecad-cfd.nix
configurePhase = ''
...
./bin/tools/foamConfigurePaths \
-boost-path ${boost} \
-cgal-path ${cgal} \
-fftw-path ${fftw} \
-metis-path ${parmetis} \
-paraview-path ${paraview} \
-scotch-path ${scotch} \
;
'';
从日志来看,相关动态库终于用上了,但是涉及cgal的代码编译失败了!报错基本是下面这种:
include/CGAL/type_traits.h:34:20: error: 'remove_cv_t' in namespace 'std' does not name a template type; did you mean 'remove_cv'?
34 | typedef std::remove_cv_t<std::remove_reference_t<T>> type;
| ^~~~~~~~~~~
| remove_cv
include/CGAL/Rational_traits.h:83:56: error: 'std::enable_if_t' has not been declared
83 | Rational make_rational(const N& n, const D& d,std::enable_if_t<is_implicit_convertible<N,RT>::value&&is_implicit_convertible<D,RT>::value,int> = 0) const
|
include/CGAL/Rational_traits.h:83:67: error: expected ',' or '...' before '<' token
83 | Rational make_rational(const N& n, const D& d,std::enable_if_t<is_implicit_convertible<N,RT>::value&&is_implicit_convertible<D,RT>::value,int> = 0) const
|
随便搜索一下报错,得到的是node相关的编译问题,加上cgal再次搜索,发现需要提高一下C++标准版本。默认情况下是C++11,改到C++14后编译通过了:
- 网上搜索找到了OpenFOAM C++编译配置文件位置为
src/wmake/rules/darwin64Clang/c++
- 发现了
FOAM_EXTRA_CXXFLAGS
这个可疑变量 - 进而在
src/etc/bashrc
发现了这个变量的说明,可由用户配置 - 最后在configurePhase中将
export FOAM_EXTRA_CXXFLAGS='-std=c++14'
加入src/etc/prefs.sh
中,作为用户的额外配置
吐槽一下网上把新版代码改回旧版来解决问题的做法
问题解决了,好方法;但是没理解问题的本质,以后再遇到可能会有更大的坑。
也生成了对应的编译产物。这下总行了吧!好吧,进了终端还是不行。提示找不到libmpi.so.40
。
检查发现,openmpi相关库的位置是通过执行mpicc --showme:link
来动态获取,mpicc
在openmpi这个包里,但作为运行环境需要一直存在。所以在OpenFOAM包中还需要配置下面的内容,带上openmpi一起(不然就要单独在外面加这个包,依赖关系错乱了):
# openfoam/default.nix
...
stdenv.mkDerivation rec {
...
# 没错我连paraview也一起带上了
propagatedBuildInputs = [ openmpi paraview ];
...
}
宿主环境会影响nix-shell环境
通过nix-shell使用交互式终端,一样会source宿主的bashrc;而当Arch宿主安装FreeCAD时,同样会装openmpi,所以我之前没能发觉openmpi的问题。构建过程中宿主不会影响nix构建环境内部。
至此OpenFOAM真的打包好了。至少demo能正常运行。
打包cfmesh-cfdof
、hisa
两个插件¶
原本这两个插件都可以由CfdOF工作台自己安装,但本着创建可复现环境的心,尝试把它们也打包成nix的软件包(derivation)。打算先复用OpenFOAM软件包的框架,改改编译指令看看能不能成功。
下载了源码,看了下都用和OpenFOAM一致的wmake
系统来构建,那估计编译也是执行根目录的Allwmake
脚本。但是没法一下子看懂编译产物究竟放哪里去了。
先直接用装好OpenFOAM的nix-shell环境编译一遍看看(注意要记得加载OpenFOAM环境配置),查查最后的可执行文件(hisa
)都去哪里了。结果是/home/user/OpenFOAM/user-v2306/platforms/linux64GccDPInt32Opt/bin
,那我猜编译产物会自动丢到/home/user/OpenFOAM/user-v2306/platforms/linux64GccDPInt32Opt
。从环境变量的值来看,实际上编译产物会自动安装到$WM_PROJECT_USER_DIR/platforms/$WM_OPTIONS/{bin,lib}
中,而$WM_PROJECT_USER_DIR
与$HOME
有一定关联。考虑到在nix构建derivation过程中的$HOME
实际并不存在,则考虑修改$HOME
为临时文件夹应该就可以修改安装位置。最后将编译产物复制到$out
合适的位置中即可。
脚本改改改……buildInputs
里加上openfoam
,毕竟这些东西要依赖OpenFOAM的工具编译、绑定对应的版本。运行一下nix-shell试试看。
直接构建提示缺zlib?zlib.h
找不到了?看来是编译依赖没写全。把OpenFOAM的复制过来……啊不用,其实有更简洁的方法:
buildInputs = [ openfoam ] ++ openfoam.buildInputs;
把OpenFOAM的依赖一起加进去!
由于所使用的cfmesh-cfdof
及hisa
的源码来源并没有版本管理,为了让一切可以复现,将源码包一并附上。于是加载源码包的代码就变成了下面这样:
# In both cases, the zip is automatically extracted
src =
if (version == "bundled") then
./cfmesh-cfdof.zip
else
fetchzip {
url = "https://sourceforge.net/projects/cfmesh-cfdof/files/cfmesh-cfdof.zip/download";
}
;
stdenv.mkDerivation默认会自动解压源码
如果提供一个压缩包给src
属性,stdenv.mkDerivation
会尝试自动解压,此时需确保nativeBuildInputs
提供了解压所需工具。压缩包内单独存在的顶级文件夹会被缩简,默认工作目录会变成源码根目录。我曾因为一些操作和巧合,只编译了一半hisa
。
使用stdenv.mkDerivation遇到的默认状况问题
一开始没改动configurePhase
,没想到默认的居然会自动用cmake配置,一定记得视情况手动调整,用不到的话可以加一个dontUseCmakeConfigure = true;
。同理buildPhase
之类也有自己的默认配置,需要注意。
总之到最后搞定了。
不知为何写到这里已经没什么动力继续写环境配置的细节问题了。
一些小修小补¶
除了特殊配置,甚至还要修一些bug,真是没想到。
FreeCAD的数据存储位置最好不要在家目录吧¶
想让整个环境portable的话,肯定不能丢家目录了。经freecad -h
指引,我找到了FreeCAD的官方启动配置文档,添加了几个环境变量。
shellHook = ''
...
export FREECAD_USER_HOME=$(pwd)/freecad-state/home
export FREECAD_USER_DATA=$(pwd)/freecad-state/userdata
export FREECAD_USER_TEMP=$(pwd)/freecad-state/temp
...
'';
CfdOF检查paraview版本时得到了错误的版本信息¶
CfdOF得到的paraview版本号是"canberra-gtk-module"
,没错还包括引号。很奇怪啊。
直接在命令行看看paraview的版本号:
> paraview --version
Gtk-Message: 16:27:34.014: Failed to load module "canberra-gtk-module"
Gtk-Message: 16:27:34.015: Failed to load module "canberra-gtk-module"
paraview version 5.11.2
> paraview --version 2>/dev/null
paraview version 5.11.2
看样子检测版本的时候把标准错误输出流的内容也处理了,通常情况下不需要读stderr才对。
修改了一下代码,正常了。
--- a/CfdOF/CfdTools.py
+++ b/CfdOF/CfdTools.py
@@ -1178,7 +1178,7 @@ def checkCfdDependencies(msgFn):
proc.setProcessEnvironment(env)
proc.start()
if proc.waitForFinished():
- pvversion = proc.readAllStandardOutput() + proc.readAllStandardError()
+ pvversion = proc.readAllStandardOutput()
pvversion = QTextStream(pvversion).readAll().split()
# The --version flag doesn't seem to work on Winodws, so quietly ignore if nothing returned
if len(pvversion):
把几个软件包丢一个包里¶
nixpkgs能这么做,nixgl能这么做,我也要这么做!
相当于搓了一个库,刚好也可以复用既有代码支持多个版本:
{ pkgs }:
let
openfoam-2306 = pkgs.callPackage (import ./openfoam) { };
openfoam-2312 = openfoam-2306.override { version = "2312"; hash = "0wgfyz4q7xr0vlv4znfxpxyp8jb53q5pl17k312dyb3x8gkdx2z4"; };
cfmesh-cfdof = pkgs.callPackage (import ./cfmesh-cfdof) { openfoam = openfoam-2306; };
hisa = pkgs.callPackage (import ./hisa) { openfoam = openfoam-2306; };
cfmesh-cfdof-unstable = cfmesh-cfdof.override { version = "unstable"; };
hisa-unstable = hisa.override { version = "unstable"; };
in {
inherit openfoam-2306 openfoam-2312;
inherit cfmesh-cfdof hisa;
inherit cfmesh-cfdof-unstable hisa-unstable;
}
不过用overlay的方式好像更合适。暂时不改了,除非之后要加kahip
。
FEM工作台检测不到已装的求解器¶
想着流体分析的环境都搞定了,一块把刚体计算的部分补上呗,反正就多装几个求解器。FreeCAD dependencies列出了很有用的信息,我在Nixpkgs搜索站点把FEM的依赖都搜索了一遍,发现还能多装两个有限元分析求解器:calculix
和elmerfem
。可用求解器数量大于零就不用我像之前给OpenFOAM打包那样了。
把依赖加进去后构建shell,结果发现FreeCAD检测不到我装的求解器。由于nix不遵守FHS,我没法写死求解器路径,所以只好想办法修bug。
FreeCAD有选项开启自动从常见位置找求解器可执行文件,我猜它就是从PATH
环境变量里搜索那些东西。calculix的可执行文件是ccx
,以此出发,在FreeCAD源码中搜索,最终找到查找可执行文件的代码,其中有一段是这样:
// first check the environment paths by QFileInfo
if (QFileInfo::exists(QString::fromLatin1(binaryName.c_str()))) {
return binaryName;
}
binaryName
是可执行文件的名称,例如ccx
。这QT方法看着怪怪的,不像是找可执行文件的方法,倒像加载所给路径的方法(像python的sys.path)。
在自建LLM、搜索引擎、QT官方文档等的联合帮助下(火热的ChatGPT恰好失效了),我搓了一个基本的测试程序验证了我的想法(毕竟直接重新编译FreeCAD并验证太费劲):
// main.cpp
#include <QFileInfo>
#include <QStandardPaths>
#include <QString>
#include <iostream>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <path to executable>" << std::endl;
return 1;
}
QString executableName(argv[1]);
QString executablePath = QStandardPaths::findExecutable(executableName);
QFileInfo fileInfo(executablePath);
if ( fileInfo.exists() && fileInfo.isExecutable()) {
std::cout << "The binary exists and is executable." << std::endl;
std::cout << "Binary location: " << executablePath.toStdString() << std::endl;
std::cout << "FreeCAD's code test result: ";
if (QFileInfo::exists(executableName))
std::cout << "found" << std::endl;
else
std::cout << "not found" << std::endl;
} else {
std::cout << "The binary does not exist or is not executable." << std::endl;
}
return 0;
}
所以后面用QStandardPaths::findExecutable
来替代了QFileInfo::exists
,修正了相关问题。
但仍然有个问题:如何整合进nix脚本?
结果使用了overrideAttrs
这个方法来改写原本包的属性,加上自己的补丁:
# some bug hotfix, only for freecad 0.21.2
freecad-patched = pkgs.freecad.overrideAttrs (finalAttrs: previousAttrs: {
patches = previousAttrs.patches ++ [
./patches/0001-Fem-fix-searching-3rd-party-binaries-in-system-path.patch
];
});
patches属性为什么能生效?
nix的软件包用到了nix标准库中的stdenv.mkDerivation
这个工具,可以理解为某种函数或编译脚本,根据参数生成编译成果。
物理仿真实战¶
折腾了那么久软件环境,总该干点实在的对吧!
软件环境声明
以下内容均基于FreeCAD 0.21.2,所有软件环境可用文末提到的nix-shell配置复现。
亚克力弹性按钮仿真¶
亚克力/PMMA是常用外壳板材。通过合适的切割,可以用亚克力实现一些方便的弹性组件。不过这个对切割精度有一定要求,有些亚克力厂可能做不了,或者因为怕损失不接单。
作为示例,我就简单绘制一个弹性按钮3D模型作为示范。如何建模我就不详细写了,完整的示例模型见模型附件。参考材质为3mm厚的PMMA板材。
参考FreeCAD官方的FEM教程,依次操作。
- 切换到FEM工作台
- 创建用于存放各种分析配置的分析容器实例
- 创建所需约束,本案例仅需选择固定面、受力面。由于约束与面相关,按钮中间需要用某种方法单独划块受力面(比如画个草图pad一下)。此处配置受力面受力1N。
- 添加模型材质。这里选择亚克力。
- 生成模型网格。官方教程说建议把这步放在准备工作里的最后一步,因为牵扯到模型。这里使用
gmsh
生成网格。先选中模型,再点击按钮,在新的菜单中先点击Apply,再点击OK,单纯点OK不会更新网格模型。如果提示gmsh报warning了,可以多点几次Apply重试,不然可能影响到后续求解器计算。 - 创建求解器对象。这里需要选择与你安装的求解器匹配的选项。如果使用了文末提供的nix-shell环境,则可以使用
calculix
和elmer
求解器。此处使用calculix
。 - 双击刚刚创建的求解器对象,选择合适的分析类型后,依次点击
Write .inp file
、Run CalculiX
按钮。计算结果就出来了,但现在什么都看不到,需要进一步处理。 - 选中结果,点击显示结果(
Show Result
)按钮,在打开的菜单中,选中Max Sheer Stress
可以看到受力情况。在下方Displacement处勾选Show并调整Factor即可观察形变情况。
至此已完成亚克力板受力仿真分析。
示例模型不适合加工
示例模型过于精细,与实物图例有较大差异,加工比较困难,良率应该不高,切勿直接套用。
导风漏斗气流仿真¶
这是我最初想解决的问题,怎么拖到了最后!这也没有现成的教程文档,有点难啊!
CfdOF项目README写道,还没有正式的文档可用,但有几个Demo可以看。考虑到我想做的基本是个风机管道(duct),我就打算参考duct示例来做做看了。点我获取本案例的模型文件。
Demo中提供了几个宏程序,可以快速构建一个示例模型,只需要计算即可。但我肯定要自己手动在我自己的模型上操作一遍啦。基本流程和FEM差不多。
缺失的计算流体动力学知识
笔者并没有流体动力学专业背景,也没有积累相应的知识,因此不能深入理解一些配置选项,选取的数值可能很离谱,总之可能造成一些误解,务必小心。
- 构建气流管道模型。注意在仿真时用的模型是流体流过的地方。这里我制作了一个参数化的管道,并单独构建了气流区域的模型。
- 切换到CfdOF工作台,添加分析方案容器。此时会多一个
analysis-container
,以及其下的四个配置项:物理模型、流体属性、流体初始态、CFD求解器。不过暂时先不改。 - 将管道模型的面标记为对应类型,设置流体边界。在菜单中点击
Add
后点击需要添加的面。Inlet面设置的气压是1.28e+02 kPa
,数值是我根据手上的管道风机猜的。此处分别添加wall、需注意由于文件中有多个模型,不建议设置默认面属性,以免造成影响。此外建议不要移动目标物体的位置,不然会有奇妙的视觉效果 :)。 - 回到之前略过的,设置流体初始状态。实话说我不知道怎么讲解,设置了一下初始压强。
- 设置物理模型。查了下术语后,觉得这里应该选个湍流模型。
- 设置流体属性。有个空气模板我就直接选了。
- 添加模型网格。先选中管道模型,再点击添加网格的按钮。在菜单中依次点击
Write mesh case
和Run mesher
。 - 双击CFD求解器,接下来可以进行计算了。依次点击
Write
、Run
进行计算。最后点击Paraview查看结果。流程基本到此
但是这也不是一帆风顺!我按自己的猜想对模型的面进行划分,结果却导致了求解器的计算错误:
Caught signal 8 (Floating point exception: tkill(2) or tgkill(2))
于是我选择简化并优先完成一个简单模拟。下图是最后的结果(我也看不明白……)。
看起来需要补一些专业知识了,不然仿真计算的意义也不大。
总结与补充说明¶
为了用FreeCAD进行流体仿真计算,尝试了各种方法,后面成功使用nix-shell构建了一个稳定可复现的软件环境,顺带修了CfdOF和FreeCAD的小错误。
在完成FreeCAD可复现环境的构建后,根据现实需求,给出了两个FreeCAD物理仿真案例及其流程。
完整的nix-shell环境项目公开在我的github仓库,使用MIT协议授权分发,欢迎star!
关于项目许可证
虽然项目使用MIT授权分发,但是项目也附带了cfmesh
和hisa
的源码zip包,它们是按GPL3协议授权的!
后记¶
因为这里的博客都是写给自己看的,所以我经常写得很简略。其中也有自己急躁的因素就是了。此次难得补充一篇,想稍微写细致一点,不过没想到极大量篇幅给了nix。希望读者们能读得开心。
趁此机会,我把这个站点的脚本收拾了一遍,也用nix-shell写了声明式软件环境配置。至少是从NixOS那边汲取了有用的东西。也希望能启发各位读者。
nix和rust还真有几分相似呢
另外之前遇到的FreeCAD玄学小问题好像都消失了٩( ᐛ )و