用FreeCAD做刚体和流体的物理仿真

Last updated: 2024-06-11
Table of contents

freecad-fem-and-cfd

太长不读版:把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-orgopenfoam-comopenfoam-com-preciceopenfoam-com-gitopenfoam-9-org。 怎么办呢,直接看最近更新过的把,想办法装新的。

去看openfoam-org的评论区,看到有人报编译问题,尚无明确解答。此外看到软件维护者置顶了说,在arch4edu仓库(类似archlinuxcn)有预编译版本。再一看软件编译依赖还有其它的AUR包,我有些不愿意自己编译。

我知道能用yayparu之类的装,但是看了下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这边复(照)现(抄)了一遍编译流程。除了kahipadios之类的辅助工具尚没有在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还依赖另外两个工具cfmeshhisa(此前都没察觉到)。

仔细看了看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上有两个相关的库(scotchparmetis)都只是按静态链接库编译,所以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-cfdofhisa两个插件

原本这两个插件都可以由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-cfdofhisa的源码来源并没有版本管理,为了让一切可以复现,将源码包一并附上。于是加载源码包的代码就变成了下面这样:

# 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的依赖都搜索了一遍,发现还能多装两个有限元分析求解器:calculixelmerfem。可用求解器数量大于零就不用我像之前给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教程,依次操作。

  1. 切换到FEM工作台 切换到FEM工作台
  2. 创建用于存放各种分析配置的分析容器实例 创建分析实例
  3. 创建所需约束,本案例仅需选择固定面、受力面。由于约束与面相关,按钮中间需要用某种方法单独划块受力面(比如画个草图pad一下)。此处配置受力面受力1N。 添加固定的侧面 添加受力面 完成图
  4. 添加模型材质。这里选择亚克力。 添加模型的材质设定,选择亚克力
  5. 生成模型网格。官方教程说建议把这步放在准备工作里的最后一步,因为牵扯到模型。这里使用gmsh生成网格。先选中模型,再点击按钮,在新的菜单中先点击Apply,再点击OK,单纯点OK不会更新网格模型。如果提示gmsh报warning了,可以多点几次Apply重试,不然可能影响到后续求解器计算。 生成模型网格
  6. 创建求解器对象。这里需要选择与你安装的求解器匹配的选项。如果使用了文末提供的nix-shell环境,则可以使用calculixelmer求解器。此处使用calculix创建求解器
  7. 双击刚刚创建的求解器对象,选择合适的分析类型后,依次点击Write .inp fileRun CalculiX按钮。计算结果就出来了,但现在什么都看不到,需要进一步处理。 运行求解器
  8. 选中结果,点击显示结果(Show Result)按钮,在打开的菜单中,选中Max Sheer Stress可以看到受力情况。在下方Displacement处勾选Show并调整Factor即可观察形变情况。 点击显示结果 查看仿真结果

至此已完成亚克力板受力仿真分析。

示例模型不适合加工

示例模型过于精细,与实物图例有较大差异,加工比较困难,良率应该不高,切勿直接套用。

导风漏斗气流仿真

这是我最初想解决的问题,怎么拖到了最后!这也没有现成的教程文档,有点难啊!

CfdOF项目README写道,还没有正式的文档可用,但有几个Demo可以看。考虑到我想做的基本是个风机管道(duct),我就打算参考duct示例来做做看了。点我获取本案例的模型文件

Demo中提供了几个宏程序,可以快速构建一个示例模型,只需要计算即可。但我肯定要自己手动在我自己的模型上操作一遍啦。基本流程和FEM差不多。

缺失的计算流体动力学知识

笔者并没有流体动力学专业背景,也没有积累相应的知识,因此不能深入理解一些配置选项,选取的数值可能很离谱,总之可能造成一些误解,务必小心。

  1. 构建气流管道模型。注意在仿真时用的模型是流体流过的地方。这里我制作了一个参数化的管道,并单独构建了气流区域的模型。 管道模型
  2. 切换到CfdOF工作台,添加分析方案容器。此时会多一个analysis-container,以及其下的四个配置项:物理模型、流体属性、流体初始态、CFD求解器。不过暂时先不改。 添加分析方案容器
  3. 将管道模型的面标记为对应类型,设置流体边界。在菜单中点击Add后点击需要添加的面。Inlet面设置的气压是1.28e+02 kPa,数值是我根据手上的管道风机猜的。此处分别添加wall、需注意由于文件中有多个模型,不建议设置默认面属性,以免造成影响。此外建议不要移动目标物体的位置,不然会有奇妙的视觉效果 :)。 设置流体边界 流体边界设置完毕
  4. 回到之前略过的,设置流体初始状态。实话说我不知道怎么讲解,设置了一下初始压强。 设置流体初始状态
  5. 设置物理模型。查了下术语后,觉得这里应该选个湍流模型。 物理模型设置菜单
  6. 设置流体属性。有个空气模板我就直接选了。 流体属性设置菜单
  7. 添加模型网格。先选中管道模型,再点击添加网格的按钮。在菜单中依次点击Write mesh caseRun mesher点击网格化 模型网格化菜单cfmesh 模型网格化菜单但是snappyHexMesh 网格结果
  8. 双击CFD求解器,接下来可以进行计算了。依次点击WriteRun进行计算。最后点击Paraview查看结果。流程基本到此 CFD求解器设置菜单

但是这也不是一帆风顺!我按自己的猜想对模型的面进行划分,结果却导致了求解器的计算错误:

Caught signal 8 (Floating point exception: tkill(2) or tgkill(2))

于是我选择简化并优先完成一个简单模拟。下图是最后的结果(我也看不明白……)。

简化的模型

CFD最终结果

看起来需要补一些专业知识了,不然仿真计算的意义也不大。

总结与补充说明

为了用FreeCAD进行流体仿真计算,尝试了各种方法,后面成功使用nix-shell构建了一个稳定可复现的软件环境,顺带修了CfdOF和FreeCAD的小错误。

在完成FreeCAD可复现环境的构建后,根据现实需求,给出了两个FreeCAD物理仿真案例及其流程。

完整的nix-shell环境项目公开在我的github仓库,使用MIT协议授权分发,欢迎star!

关于项目许可证

虽然项目使用MIT授权分发,但是项目也附带了cfmeshhisa的源码zip包,它们是按GPL3协议授权的!

后记

因为这里的博客都是写给自己看的,所以我经常写得很简略。其中也有自己急躁的因素就是了。此次难得补充一篇,想稍微写细致一点,不过没想到极大量篇幅给了nix。希望读者们能读得开心。

趁此机会,我把这个站点的脚本收拾了一遍,也用nix-shell写了声明式软件环境配置。至少是从NixOS那边汲取了有用的东西。也希望能启发各位读者。

nix和rust还真有几分相似呢

另外之前遇到的FreeCAD玄学小问题好像都消失了٩( ᐛ )و