FreeFEM 解説

1. 概要

FreeFEMはフランスの「Pierre et Marie Curie 大学」のスタッフによって 開発された偏微分方程式を解くための有限要素法のプログラムです。利用者は Gfemと呼ばれるPascal風の言語で、

  1) 境界の形状
  2) 境界条件
  3) 偏微分方程式
を記述するだけで、後はFreeFEMがメッシュの自動生成から数値解を求める 作業のすべてをやってくれますから、他のプログラムに比べて、広範囲の問題を極 めて容易に解くことができます。

Gfemの概念を理解するには、最初に実例を見るのが良いでしょう。例えば、 5D-2V同軸ケーブル(JIS C 3501)の電位分布を解析する場合は、次のような スクリプトを書きます。

/* 5D-2V (JIS C 3501) Coaxial Cable Example */
a1:=0.7e-3;	/* 内部導体半径 */
a2:=2.4e-3;	/* 外部導体半径 */
v1:=0; v2:=1;	/* 外部導体と内部導体の電位 */
/* 境界の形状 */
border(1,0,2*pi,20) {	/* 外部導体 */
 x:=a2*cos(t); y:=a2*sin(t)
};
border(2,0,2*pi,60) {	/* 内部導体 */
 x:=a1*cos(-t); y:=a1*sin(-t)
};
buildmesh(200);	/* メッシュ生成 */

solve(v) {	/* 偏微分方程式の指定 */
 onbdy(1) v=v1;	/* 境界条件 (外部導体) */
 onbdy(2) v=v2;	/* 境界条件 (内部導体) */
 pde(v) -laplace(v) = 0	/* ラプラス方程式 */
};
plot(v);	/* 等電位線の作図 */

このファイル名を引き数にfreefemを起動すると、まず、境界の頂点座標を 計算し、デローニ三角分割 (Delaunay-Voronoi Triangulation)を行ってその結果 を表示してから、要素マトリクスを作成し、連立1次方程式を解いて、すべての頂 点の電位を求めた後、その等電位線を描画します。

電界やキャパシタンス等の値は、Gfemで用意された微積分演算子を使って 計算することができます。

次に、もう少し面白い問題として、高さ 40 mm、幅 80 mm の長方形断面を持つ鋳鉄 の棒の長さ方向に -1 V/m のステップ状電圧を印加したときの渦電流の時間的変化 を計算してみましょう。渦電流問題は明確な境界条件が存在しないため積分方程式 でないと扱いにくいのですが、この場合は鋳鉄の外側の磁束が鋳鉄の外側の磁束に 比べて無視できますから、鋳鉄の外側に磁束が存在しないという境界条件で近似する と簡単になります。

渦電流を支配する偏微分方程式は、

  (d/dt)A - (d/dx)(n*(d/dx)(A)) - (d/dy)(n*(d/dy)(A)) = g*V

  ここに、
	A = ベクトル・ポテンシャル (z 軸方向)
	n = レラクタンス
	g = コンダクタンス
	V = 印加電圧 (z 軸方向)
ですが、FreeFEMではCrank-Nicolsonの解法を使うと、次のように 書くことができます。(注 1)

/* eddy current in rectangular conductor */
a1:=2e-2;
b1:=1e-2;
u1:=300*4e-7*pi;	/* permeability */
g1:=8.33e5;	/* conductivity */
n1:=1/u1/g1;
dt:=79e-6;	/* time step */
border(1,0,a1,20) { x:=t; y:=0 };
border(2,0,b1,20) { x:=a1; y:=t };
border(3,0,a1,20) { x:=a1-t; y:=b1 };
border(4,0,b1,20) { x:=0; y:=b1-t };
buildmesh(1000);

a=0;	/* vector potential */
nowait;
iter(100)
begin
 solve(w,1) {
  onbdy(2,3) w = 0;	/* vector potential at surface */
  pde(w) id(w)*2/dt-laplace(w)*n1 = 1+a*2/dt;
 };
 a=2*w-a;
 plot(a);
end;
対象性を生かして、右上の4半分だけを計算していますが、実行すると、最初は 渦電流により導体内部への侵入を抑えられていた磁束が時間の経過とともに導体 内部に浸透してゆく様子がアニメ表示で見られます。

今度は、往復円柱導体に直流を流した場合の磁気ベクトルポテンシャルを求めて見ます。

/* magnetic vector potential of twin lead */
a1:=0.225e-3;   /* conductor radius */
b1:=0.50e-3;    /* conductor separation */
c1:=b1*2;       /* ficsious boundary */
c2:=a1*2;       /* ficsious boundary */
u1:=4e-7*pi;    /* permeability */
t1:=pi/2;
n1:=30;
n2:=floor(n1*pi*a1/a1);
n3:=floor(n1*(c1-b1-a1)/a1);
n4:=floor(n1*c2/a1);
n5:=floor(n1*c1/a1);
n6:=floor(n1*c2/a1);
n7:=floor(n1*(b1-a1)/a1);

border(1,0,2*n1+n2,2*n1+n2+1) {
 if(t<=2*n1) { x:=b1-a1+t*a1/n1; y:=0; ib:=1 }
 else { t2:=(t-2*n1)*pi/n2; x:=b1+a1*cos(t2); y:=a1*sin(t2); ib:=0 };
};
border(2,0,n3+n4+n5+n6+n7,n3+n4+n5+n6+n7+1) {
 if(t<n3) { x:=b1+a1+t*(c1-b1-a1)/n3; y:=0; ib:=1 }
 else if(t<n3+n4) { x:=c1; y:=(t-n3)*c2/n4; ib:=2 }
 else if(t<n3+n4+n5) { x:=c1-(t-n3-n4)*c1/n5; y:=c2; ib:=3 }
 else if(t<=n3+n4+n5+n6) { x:=0; y:=c2-(t-n3-n4-n5)*c2/n6; ib:=4 }
 else { x:=(t-n3-n4-n5-n6)*(b1-a1)/n7; y:=0; ib:=1 };
};
buildmesh(20000,1);
i1:=1;  /* total current */
j2:=i1/pi/a1/a1;        /* current density (inner conductor) */
j=j2*(region==0);

/* vector potential */
solve(u) {
 onbdy(2,3) u = u1*i1/4/pi*log((x*x+y*y+b1*b1+2*b1*x)/(x*x+y*y+b1*b1-2*b1*x)); 
 onbdy(4) u = 0;
 pde(u) -laplace(u)/u1 = j;
};

plot (u);
開領域ですから、人為的な境界が必要ですが、導体半径と導体を流れる電流、そして、 導体中心から境界までの距離から、境界上の磁気ベクトルポテンシャルが計算でき ますから、それを境界条件にしています。インダクタンスの値を求める場合は、 境界内部のエネルギを B*H/2 で計算し、境界外部のエネルギは境界上の (HxA) の 1周積分で計算するか、導体内部の A*j を積分するのが良いでしょう。A は z 軸 方向のベクトルポテンシャル、B は磁束密度、H は磁界の強さ、i は電流密度です。

2. Gfem の構文

Gfemの構文はPascal風ですが、コメント文はCの方式を採 用しています。また、複文のbeginendにもCと同じよう に{}を使うこともできます。Cのプログラマは、「;」 の扱いに注意してください。Pascalは「;」を「}」の前に付けません。

2.1. 予約語、関数、演算子

Gfemの予約語は次のとおりで、if .. then .. else, and, or の使いかた や複文の書き方はPascalと同じです。

  begin,end,{,} - ブロック定義
  if,then,else,or,and,iter - 条件文とループ
  x,y,t,ib,region,nx,ny - メッシュ変数
  log,exp,sqrt,abs,min,max,sin,asin,tan,
      atan,cos,acos,cosh,sinh,tanh - 数学関数
  complex,Re,Im - 複素数
  pi,I - 円周率、虚数単位
  buildmesh,savemesh,loadmesh,adaptmesh - メッシュ関数
  one,scal,dx,dy,dxx,dyy,dxy,dyx,convect - 数学演算子
  solve,pde,id,dnu,laplace,div,onbdy - 求解関数
  plot,plot3d - 作図関数
  save,load,saveall - 保存、復元関数
  wait,nowait,changewait - ウェイト状態の変更
  precise,halt,include,evalfct,exec,user - その他の関数

2.2. 制御文

制御文としては、条件判断のifと繰り返しのiterが用意されていま す。

  if (logical_expression) then {
   statement;
   ....;
   statement
  }
  else {
   .....
  };
論理式logical_expressionの値が 1 なら then 以下を実行、0 なら else 以下が実行されます。Cのプログラマは「;」に注意してください。
  iter(j){....};
の構文で繰り返しの指定ができ、時間軸による変化を見るのに便利です。j は iter(i*k) のような「式」でもかまいません。

2.3. 関数

次の数学関数が用意されています。

  max(p,q),min(p,q),abs(p),atan(p),sqrt(p),
  cos(p),sin(p),tan(p),acos(p),asin(p),one(p),
  cosh(p),sinh(p),tanh(p),log(p),exp(p)
これらのほとんどはC言語の関数と同じですが、one(p)は論理式 p が真なら 1、偽なら 0 になります。

また、Gfem固有の次の関数があります。

  x,y - 領域(ドメイン)内部の (x,y) 座標
  ib - 境界番号 (0,1,2,.., 領域内部では 0)
  nx,ny - 境界の法線(normal)成分 (nx^2+ny^2==1, 領域内部では 0)
  region - 頂点の領域番号 (0,1,2,..)
  id(f) - 関数 f の値

2.4. 演算子

次の演算子が使えます。

  +,-,*,/,^	4則演算、冪乗
  %		剰余
  and,or	論理演算
  <,<=,<,>=,==	比較演算
  :=,=		代入
^は冪乗で、例えば、x^2 は x*x と同じになります。

代入演算子は2つあって、:=は左辺がスカラ(scolor)変数であることを意味 し、=は左辺が(場所の)関数(function) f(x,y) であることを意味します。 単一変数としての「スカラ」と頂点分の配列変数としての「関数」の違いに注意し てください。例えば、

  a:=(1+sqrt(5))/2;
  f=x*cos(a*pi*y);
  b:=f(1.2,3.1);
では、a, b がスカラ変数、f は関数 f(x,y) になります。

また、Gfem固有の次のような微積分演算子があります。

  dx(f) - d(f)/dx (x による偏微分)
  dy(f) - d(f)/dy (y による偏微分)
  convect(f,u,v,dt) - f(x - u(x,y)*dt, y - v(x,y)*dt)
  intt[f] - 全領域の面積分
  intt(n1)[f] - 領域 n1 の面積分
  intt(n1,n2)[f] - 領域 n1,n2 の面積分
  intt(n1,n2,n3)[f] - 領域 n1,n1,n3 の面積分
  int(n1)[f] - 領域 n1 の境界の1周(線)積分
  int(n1,n2)[f] - 領域 n1,n2 の境界の1周(線)積分
  int(n1,n2,n3)[f] - 領域 n1,n2,n3 の境界の1周(線)積分
( ) 内のパラメータは3つまでしか指定できません。これは構文解析の実装方法 による制限です。この制限は他のすべてのパラメータに対して課せられますので 注意してください。4つ以上のパラメータを指定してもFreeFEMはエラー メッセージを出しません。

dx(f) は f の x による偏微分です。presice が指定されると要素内で一定になり、 presice の指定がない場合は接点の廻りの要素を使って要素内で1次連続になる ように補間します。

convect() は

  f(x - u(x,y)*dt, y - v(x,y)*dt)
の近似値で、有限要素法ではうまく扱えない、輸送項(convection term)が粘性項 (viscous term)にくらべて支配的なケースの対策として、 ラグランジュ風上差分(Lagrangean upwinding) に使います。 例えば、時間と場所の関数 f(x,y,t) で
  df/dt + u*df/dx + v*df/dy - div(mu*grad(f)) = g
を解く場合は、
  df/dt + u*(df/dx) + v*(df/dy)
    -> (f(x,y,t) - f(x - u(x,y)*dt, y - v(x,y)*dt, t - dt)/dt
と、div(mu*grad(f)) が laplace(f)*mu になることに注意して、
  iter(n) {
   id(f)/dy - laplace(f)*mu = g + convect(oldf,u,v,dt)/dt;
   oldf = f
  };
として計算すれば不安定になりません。

なお、dx(), dy(), convect(), intt[], intt()[], int[], int()[] の計算には全 ての頂点のデータが必要ですから、

  g=dx(2*f); intt[f+g]; dx(dy(g)); dx(sin(f)); convect(2*u...),
  f=convect(f,u,v,dt)
といった記述ができないことに注意してください。関数配列に計算途中の値をしま う場所がありませんから、次のように書く必要があります。
  g=2*f;
  h=dx(g)*dy(f);

2.5. 複素数

  complex;
の宣言で複素数が使えるようになり、通常の計算や代入の他に、次の定数と関数で 複素数を扱うことができます。
  I - sqrt(-1)
  Re(z) - z の実部
  Im(z) - z の虚部
  cos(z)
  sin(z)
  exp(z,x) - x は実数

共役関数(conjugate)は conj=Re(z)-I*Im(z) で作れます。

2.6. 構造記述と三角分割

border()polygon()で幾何学的形状を記述し、buildmesh() でデローニ三角分割を指示します。

  border(ib,t_min,t_max,nb_t) { ...x:=f(t); ...y:=g(t) };
  buildmesh(nb_max[,flag]);
三角分分割すべき領域(domain)の形状を t を媒介変数(parameter)として記述 します。t の値は t_min から t_max まで、(t_max - t_min)/(nb_t + 1) 刻みで 増加しますから、nb_t でメッシュの大きさを制御することができます。ib は後で 境界条件を指定したり線積分の経路を指示するときに参照する境界番号 (1,2,3..) です。境界を指示する (x,y) 座標は時計回りのとき穴(hall)と見なされますので、 内部の解析が必要な境界は反時計回りに指定します。これは、すべての有限要素法 のプログラムで使われているルール(習慣)です。

例えば、中心に穴のある楕円なら、

  a:=3; b:=2; c:=1;
  border(1,0,2*pi,50) { x:=a*cos(t); y:=b*sin(t) };
  border(2,0,2*pi,20) { x:=c*cos(-t); y:=c*sin(-t) };

と書けます。また、次のようにすると、面積と周長が計算できます。

  a:=3; b:=2; c:=1;
  border(1,0,2*pi,50) { x:=a*cos(t); y:=b*sin(t) };
  border(2,0,2*pi,20) { x:=c*cos(-t); y:=c*sin(-t) };
  buildmesh(2000);
  solve(v) {
    onbdy(1) v = 1;
    pde(v) id(v) = 1;
  };
  plot(v);
  s1:=int(1)[v];	/* 楕円の周長 */
  s2:=int(2)[v];	/* 円の周長 */
  s3:=intt(0)[v];	/* 面積 */

長方形の場合はif文を使うと簡単になります。

  border(2,0,4,60) {
   if (t<=1) then { x:=t; y:=0 }
   else if (t<2) then { x:=1; y:=t-1; ib:=1 }
   else if (t<=3) then { x:=3-t; y:=1 }
   else { x:=0; y:=4-t };
  };

このように、ibという要素変数を使うとborder()で指定した頂点の 境界番号に上書きすることができ、閉曲線の一部に別の境界条件を与えることができ、 if 文を使った領域定義に便利です。

境界条件を必要としない内部境界(inner boundaries)は ib=0 にします。これは境 界(domain)の中に材質が違うサブドメイン(sub domain)を作ったり、三角分割の頂 点や辺を特定の場所に強制したりするのに使いますが、閉曲線の場合は右向きで終 るように書く必要があります。

buildmeshはデローニ(Delauney)三角分割の指示で、nb_max が不用意に多 量の要素を作らないための、分割の最大値になります。省略可能な2つ目の引数 flagを 1 にすると要素マトリクスを小さくするための境界の角に於ける対 角線の入れ換え(diagonal swapping)を行いません。これは、隣接した円等で、 サブドメインの分割がおかしくなるときに使います。 flagを省略するか 0 にすると、対角線を入れ換えます。

2.7. 領域番号

誘電率やヤング率といった要素単位の材料定数を定義するために、border() が呼ばれる度に内部的な領域番号 (ngt = 0,1,2,..) をインクリメントし、それを サブドメイン内部の三角要素に割り当て、gegionという配列で参照すること ができます。

最後の辺の左側の三角要素とそれに隣接する三角要素に領域番号を割り当てると いうアルゴリズムのため、穴の場合は予測の難しい割り当てが行われますので、 buildmeshの結果を見て、実際にどう割り当てられたかを確認してから使う ようにしてください。領域を外側から指定するとか、少し工夫すると見通しのよい 割り当てができます。

2.8. 複雑な領域形状

数学関数とif文による表現が困難な形状については、borderの代 わりに

  polygon(ib,'file',nb_p);
を使うことが出来ます。ib は境界番号、file は頂点の (x,y) 座標を書き込んだ ファイル名で、例えば、file の内容が
 0. 0.
 1. 0.
 1. 1.
 0. 1.
 0. 0.
なら正方形になります。各辺は (nb_p + 1) に分割されますので、これで分割の 細かさが調整できます。

2.9. 偏微分方程式の求解

偏微分方程式と境界条件は下記の形式で指示します。

  solve(u[,i]) {
   onbdy()...;
   onbdy()...;
   ...;
   pde(u)...
  };

通常 solve() は要素マトリクスを組み立て、「LU 分解」(Gauss factorization) してから指定された方程式を解きますが、要素マトリクスが変わらず、初期条件等、 右辺の値だけが変わる場合は、省略可能な2つめのパラメータ i に 1,2,3,.. の 自然数を指定することにより、後から同じマトリクスを使って、別のパラメータで 再計算することで、計算時間の節約がができます。例えば、次のとおりです。

  solve(u,1);
  ..
  solve(v,2);
  ..
  solve(u,-1); /* u の再計算 */
  ..
  solve(v,-2); /* v の再計算 */
要素マトリクス番号 i を省略すると 1 になります。

境界条件は

  onbdy(ib1, ib2,...) id(u)+*dnu(u) = g;
  onbdy(ib1, ib2,...) u = g;
のいずれかで指定します。ib1,ib2,.. はborder()で指定した境界番号、 id(u) はその場所に於ける関数 u の値、 は一般の式、dnu(u) は conormal(余法線)、g は一般の関数です。id(u) を省略するとノイマン (Neumann) 境界条件になります。ディリクレ(Dirichlet)境界条件は二つ目の 構文で指定します。

conormalというのは、偏微分方程式を a*u-div(C.grad(u))、法線ベクタを n としたとき、(Cgrad(u))*n という意味です。

内部的には、すべての境界条件は

  id(u) * a + dnu(u) * b = c;
というmixed Fourier/Robin conditionに変換され、ディリクレ条件の場 合は a を 1.0e12 に固定し、c も 1.0e12 倍して、id(u)*a の項が無視できるよ うにしています。ノイマン条件の場合は a=0 になります。

関数 a, b, c は preciseが指定されないときは要素内で連続、指定すると不連続になりま す。

Dirichlet-Neumann corner(1辺がディリクレ、他の辺がノイマンの角) ではpreciseが指定されないときは、関数 a, b, c を要素内で連続にし て、角をディリクレ条件で扱い、preciseが指定されたときは、a, b, c を不連続にして、ノイマン条件の辺が正しく処理されるようにします。

preciseが指定されたときは、評価関数に1次でなく2次式を使い、各頂点 の少し内側の3点(Gauss point)を使ってガウス積分を使うようになります。

偏微分方程式は

  pde(u) [+-]op1(u)[*/]exp1 [+-]op2[*/]exp2...=exp3;
の構文で指定しますが、op1,op2,.. で使えるのは次の演算子です。
  id(u) - u(x,y) の値
  dx(u) - (d/dx)u(x,y) .. x で偏微分
  dy(u) - (d/dy)u(x,y) .. y で偏微分
  laplace(u) - ((d/dx)^2+(d/dy)^2)u(x,y) .. ラプラシアン
  dxx(u) - (d/dx)((d/dx)u(x,y)) .. x で2回偏微分
  dxy(u) - (d/dx)((d/dy)u(x,y)) .. y で偏微分した後 x で偏微分
  dyx(u) - (d/dy)((d/dx)u(x,y)) .. x で偏微分した後 y で偏微分
  dyy(u) - (d/dy)((d/dy)u(x,y)) .. y で2回偏微分
後置形で書かれた exp1,exp2,.. の式は2度の微分の中間で実行されますので注意 してください。例えば、
  laplace(u)*v
  laplace(u)*v - (d/dx+d/dy)(v(x,y)*((d/dx+d/dy)f(x,y))
と計算されます。

2.10. 作図

  plot(u);
  plot3d(u);
で関数 u(x,y) の等高線を表示します。plot3d() は3次元表示になります。

複素変数のplot(f),plot3d(f)はデフォルトで u の実部を描画します ので、虚部を描画する場合は plot(Im(f)) と指定します。

2.11. 結果の保存

下記のようなデータの保存とロード関数が用意されています。

  save('file',f) - file に f を保存
  savemesh('file') - file にメッシュデータを保存
  load('file',f) - file から f をロード
  loadmesh('file') - file からメッシュデータをロード

save() で f が関数の場合は、次の形式のデータが書き込まれます。

  ns
  f[0]
  f[1]
  ....
  f[ns-1]
ns はデータ数です。f がスカラのときは、file にその値が追加(append)され ます。

savemesh() では次の形式のデータが作られます。

  ns nt
  q[0].x q[0].y ib[0]
  ...
  q[ns-1].x q[ns-].y ib[ns-1]
  me[0][0] me[0][1] me[0][2] ngt[0]
  ..
  me[nt-1][0] me[nt-1][1] me[nt-1][2] ngt[nt-1]
ns は頂点数、nt は三角形の数です。me[][] の頂点インデックスは Fortran 方式で 1,2,3,.. ですから、C のプログラムを書くときは注意してください。

メッシュの適応制御などで、連番を付けたファイル名がほしいときは、下記の構文 を使います。

  save('file',f,c)
  savemesh('file',c)
  load('file',f,c)
  loadmesh('file',c)
cはカウンタ変数で、実際のファイル名は、c が 1 なら「file-1」、c が 17 なら「file-17」になります。

2.12. 連立方程式

次の方法で連立偏微分方程式が解けますが、コンピュータの負担は極端に増えま すから、単純な繰り返しで解くことができないことを確認してから使ってくださ い。

  solve(u,v) {
   onbdy(..) ...dnu(u)...=... /* u の境界条件 */
   onbdy(..) u=...            /* u の境界条件 */
   pde(u) ...                 /* u の偏微分方程式 */
   onbdy(..)<v=... または ...dnu(v)...> /* v の境界条件 */
   pde(v) ...                 /* v の偏微分方程式 */
  };

solveの構文はスカラの場合と同じで solve(u,v,1) 等も使えます。方程式 の順序は問いません。onbdy()はいくつでも書けます。onbdy()の 構文もスカラの時と同じですが、dnu()は一つしか書けません。

2.13. 弱形式 (Weak formulation)

次の構文で「弱形式」(weak form)による偏微分方程式の記述ができます。

  varsolve(<unknown function list>;<test function list>,<<int>>)
	<<instruction>> : <<expression>>;

<unknown function list> と <test function list> は1つないし 2つの関数をコンマで区切って指定します。<int> は正か負の整数です。 <instruction> は単文か複文、<expression> は実数または複素数を返す 式です。<< >> で囲まれた項は省略可能です。

次の例を見てください。

varsolve(u;w)   /* ディリクレ問題 -laplace(u) =x*y */
begin
  onbdy(1) u = 0;
  f = dx(u)*dx(w) + dy(u)*dy(w)
  g = x*y;
end : intt[f] - intt[g,w];


varsolve(u;w,-1)   /* 同じ (組み立て済みマトリクスを使用) */
begin
  onbdy(1) u = 0;
  f = dx(u)*dx(w) + dy(u)*dy(w)
  g = x*y;
end : intt[f] - intt[g,w];


varsolve(u;w)   /* ノイマン問題 u-laplace(u) = x*y */
begin
  f = dx(u)*dx(w) + dy(u)*dy(w) -x*y;
  g = x;
end : intt[f] + int[u,w] - int(1)[g,w];


varsolve(u,v;w,s) /* Lame 方程式 */
begin
   onbdy(1) u=0;
   onbdy(1) v=0;
   e11 = dx(u);
   e22 = dy(v);
   e12 = 0.5*(dx(v)+dy(u));
   e21 = e12;
   dive = e11 + e22;
   s11w=2*(lambda*dive+2*mu*e11)*dx(w);
   s22s=2*(lambda*dive+2*mu+e22)*dy(s);
   s12s = 2*mu*e12*(dy(w)+dx(s));
   s21w = s12s;
   a = s11w+s22s+s12s+s21w +0.1*s;
end : intt[a];
インタプリータは「:」の後の式をすべての三角要素の3頂点に対して評価します。 「;」の前にインストラクションがあれば、それも同様に評価しますが、これは、 未知関数の1つと試験関数の1つについて、1つの頂点を 1、残りの2頂点を 0 にした状態で評価しますので、これが線形システムに対するその三角要素の影響に なります。右辺は各頂点について、全ての未知数を 0、試験関数を1にして構成し、 積分はその三角要素についてのみ計算します。

varsolvesolveに比べて計算に時間がかかりますが、これは、 dx(u) のような微分がsolveの 1 度に対して、varsolveは 9 回 も必要になるためです。

2.14. その他の関数

以上に加えて、次のような関数があります。

                         _
  scal(f,g) - int(f(x,y)*g(x,y))*dx*dy .. int() は面積分
  wait - 結果を表示してマウスの左釦が押されるのを待つ
  nowait - マウスの左クリックを待たない
  changewait - wait/nowait を反転
  precise - 1次でなく2次の評価関数を使う

a:=scal(f,g) は三角分解された領域について、次の積分(f(x,y) と g(x,y) の 共役複素数の積)を実行します。

                _
  a = ∫(f(x,y)*g(x,y))*dx*dy

preciseFreeFEMに2次の積分公式を使った高精度の計算を行う ように指示します。積分は個々の3角形の少し内側の3(Gause)点で行われ ますから、積分を行う関数は、より多くのメモリ(関数あたり ns が 3*nt、つまり、 9 倍)を消費しますが、これは、偏微分方程式 (PDE) が必要とするメモリに比べれ ばわずかなものです。

preciseconvectや不連続な非線形システムで使うのは、とても よい考えです。

FreeFEMplotコマンドは描画後、ユーザがその結果をよく見られる ようにプログラムを一次停止させ、マウスの左釦がクリックされるのを待ちますが、 nowaitを実行すると、一次停止しないようになり、waitが実行される と再び停止するようになります。changewaitはこの2つの状態を反転させま す。また、X11を使ったシステムでは、マウスの右釦をクリックすると、プ ログラムの実行を停止します。 wait

2.15. 外部関数とのリンク

exec('command')Unixshに指定されたcommandを 実行させます。

user(what,f)FreeFEM内部の C++ のプログラムで、下記の動作を 行います。

  for (int j = 0; j < nquad; j++)
    creal gfemuser(creal what, creal* f, int j);
crealcomplexが設定されていれば複素数、そうでなければスカラ (float)で、nquadpreciseが指定されていれば nquad=3*nt、そう でなければ nquad=ns になります。

whatは複数のユーザ定義関数を必要とするとき、どの関数を使うかを if文等で選べるようにするための引数です。

gfemuser()の中では、三角分解(ns,nt,me,q,ng,ngt,area,..)等、全ての グローバル関数が参照可能です。src/fem.cxx にgfemuserの実装例があり ますので、見てください。

2.16. saveall()

下記の構文で、すべてのデータをまとめてファイルに書き込むことができ、ユーザ が自分の解析プログラムを書けるようになっています。

  saveall('fime_name',var_name1,...);
構文は最初の引数がファイル名になる他はsolve(,)と同じです。他のパラ メータは未知関数が何であるかをインタプリータに知らせるために使われます。 スカラ方程式
u=p   ディリクレ
c u+dnu(u)=g ノイマン
b u-dx(nuxx dx(u))-dx(nuxy dy(u))-dy(nuyx dx))-dy(nuyy dy(u))
 + a1 dx(u) + a2 dy(u) =f
に対するファイル・フォーマットは、laplace を nuxx, nuyy で表すことにする と、頂点 (x,y) の於ける f,g,p,b, c,a1,a2,nuxx,nuxy,nuyx,nuyy を行単位に並 べたものになります。

実際の C++ のルーチンは次のとおりです。

int saveparam(fcts *param, triangulation* t, char *filename, int N)
{
  int k, ns = t->np;
  ofstream file(filename);
  file<<ns<<"    "<<N<<endl;
  for(k=0; k<ns; k++)
  {
                  file << (param)->f[k]<<" " ; file<<"             ";
                  file << (param)->g[k]<<" " ; file<<"             ";
                  file << (param)->p[k]<<" " ; file<<"             ";
                  file << (param)->b[k]<<" " ; file<<"             ";
                  file << (param)->c[k]<<" " ; file<<"             ";
                  file << (param)->a1[k]<<" " ; file<<"            ";
                  file << (param)->a2[k]<<" " ; file<<"            ";
                  file << (param)->nuxx[k]<<" " ; file<<"          ";
                  file << (param)->nuxy[k]<<" " ; file<<"          ";
                  file << (param)->nuyx[k]<<" " ; file<<"          ";
                  file << (param)->nuyy[k]<<" " ; file<<"          ";
    file << endl;
  }
}
複素数についても、「<<」演算子のオーバーローディングを使って、同じ関数が 使われます。
friend ostream& operator<<(ostream& os, const complex& a)
 {
   os<<a.re<<" " << a.im<<"               ";
   return os;
 }
連立方程式も同じようにして処理されます。
ostream& operator<<(ostream& os, cvect& a)
  {
    for(int i=0; i<N;i++)
      os<<a[i]<<"   ";
    return os;
  }
ostream& operator<<(ostream& os, cmat& a)
  {
    for(int i=0; i<N;i++)
     for(int j=0; j<N;j++)
        os<<a(i,j)<<"   ";
    return os;
  }

ここに N=2

2.17. メッシュの適応制御

adaptmeshの予約語を使うと、最初の計算結果に基づいて、メッシュの品質 を高めるための「適応制御」(mesh adaptation)を行うことができます。次の例を 見てください。

Navier stokes 方程式の後退ステップです。

nowait;
border(1,0,1,6,5)   begin x:=0;       y:=1-t  end;
border(2,0,1,15,2) begin x:=2*t;     y:=0     end;
border(2,0,1,10,2) begin x:=2;       y:=-t    end;
border(2,0,1,20,2) begin x:=2+3*t;   y:=-1    end;
border(2,0,1,35,2) begin x:=5+15*t; y:=-1     end;
border(4,0,1,10,2) begin x:=20;      y:=-1+2*t end;
border(5,0,1,35,5) begin x:=5+15*(1-t); y:=1  end;
border(5,0,1,40,5) begin x:=5*(1-t);y:=1      end;


buildmesh(1500);
nu := 0.001; dt := 0.4;


/* initial values */
u = y*(1-y)*one(y>0);
v = 0;
p = 2*nu*x*one(y>0);
un = u; vn = v;
i:=1; j:=2; k:=3;count:=1;
iter(500)
begin
 if (count % 100 == 0) then { /* adapt every 100 iterations */
   adaptmesh(u,p);
   i:=1; j:=2;k:=3;
   wait; /* wait after each displayed solution */
   plot(u);
   plot(v);
   plot(p);
   nowait; /* do not wait anymore */
 };
 /* increment the counter */
 count:=count+1;


 f=convect(un,u,v,dt);   g=convect(vn,u,v,dt);


 solve(u,i){          /*Horizontal velocity*/
   onbdy(1) u = y*(1-y);
   onbdy(2,5) u = 0;
   onbdy(4)dnu(u)=0;
   pde(u) id(u)/dt-laplace(u)*nu = f/dt -dx(p);
 };


 solve(v,j){         /* Vertical velocity */
   onbdy(1,2,4,5) v = 0;
   pde(v) id(v)/dt-laplace(v)*nu = g/dt -dy(p);
 };


 solve(p,k) {       /*   Pressure */
 onbdy(1,2,5) dnu(p) = 0;
 onbdy(4) p=0;
 pde(p)   -laplace(p)= -(dx(f) + dy(g))/dt;
 };
 un = u;   vn = v; i:=-1; j:=-2; k:=-3;
end ;
wait;
plot(u);plot(v);plot(p);

500 回の繰り返し中、100 回毎に適応制御を行っています。

  w=0;
  plot(w);
は適応制御を行った後でメッシュを再表示するためのトリックです。

borderに新しい引数が追加されていることに注意してください。この最後 の引数(オプション)はこの境界の起点と見なされます。メッシュの適応制御を行う ときはこれを指定してください。さもないと、未定義の辺や頂点ができることがあ ります。

2.18. インタプリータの構造

インタプリータが使うtokenは lexical.h で定義され、字句解析は lexical.cxx の netsym 関数で行われます。

構文解析は syntaxic.cxx の instruction 関数が行いますが、構文は LL(1) で 設計されています。

構文解析プログラムは入力プログラムを plante 関数により構文解析木に変換し、 最後に eval 関数を使って、その root から実行を始めます。

FreeFEMに新しい命令を追加する場合は、次のルールを守る必要があります。

構文解析木のノードで使われている構造体は、下記のような極めて簡単なものです。

  typedef struct noeud {
    Symbol symb;
    creal value;
    ident *name;
    long junk;
    char *path;
    struct noeud *l1, *l2, *l3, *l4;
  } noeud, *arbre;

3. 例

3.1 三角分解(Triangulations examples)

o リング

  twopi := 2*pi;
  border(1,0,twopi,60) { x := cos(t); y := sin(t) };
  border(2,0,twopi,20) { x := 0.25*cos(-t); y := 0.25*sin(-t) };
  buildmesh(400);
有限要素法に習慣では、解析対象の領域を左に見ながら進む(反時計廻り)ように 境界を記述します。領域を右見ながら進む(時計廻り)境界線は穴と見なされます。 この記述では、外径が 2.0、内径が 0.5、外周の境界番号が 1、内周の境界番号が 2 の穴のあいた円になります。内部の領域番号は 0 になることに注意してください。

o 長方形 [(0,0),(0,2),(2,1),(0,1)]

  border(1,0,2,20) { x:= t; y:= 0 };
  border(1,0,1,10) { x:= 2; y:= t };
  border(1,0,2,20) { x:= 2-t; y:= 1 };
  border(1,0,1,10) { x:= 0; y:= 1-t };
  buildmesh(400);
境界番号はすべて 1 になりますが、領域番号は border を実行する都度増加します ので、0, 1, 2, 3 が混在することに注意してください。

o 境界番号を細かく割り当てた正方形

  border(1,0,4,41)
  {
    if(t<=1) then { x:=t; y:=0 };
    if((t>1)and(t<2)) then { x:=1; y:=t-1; ib:= 2 };
    if((t>=2)and(t<=3)) then { x:=3-t; y:=1; ib:= 3 };
    if(t>3) then { x:=0; y:=4-t; ib:= 4 }
  };
  buildmesh(400);
ib変数を使って境界番号を指定しています。border() は1つですから、 内部の領域番号はすべて 0 になります。

o 複数の領域を持つ円

  border(1,0,2,17) { x:= cos(pi*t); y:= sin(pi*t)};
  border(0,-1,1,7) { x:= t; y:=0; };
  border(0,0,1,4) { x:=0;y:=t };
  buildmesh(300);
  /* observe the value of "region" by using "show triangle numbers" */
境界番号「0」を使って、円の内部を3つのサブドメインに分けています。 領域番号は border() が実行される都度インクリメントされますから、下半分が 0、 右上が 1、左上が 2 になります。「show triangle numbers」命令を使って、 領域(region)番号を確認してください。

3.2. スカラ変数

o キャパシタ(電気コンデンサ)

  /* a circle of radius 5 centered at (0,0) */
  border(1,0,2*pi,60) { x:=5*cos(t); y:=5*sin(t) };
  /* The rectangle on the right */
  border(2,0,1,4) { x:=1+t; y:=3 };
  border(2,0,1,24) { x:=2; y:=3-6*t };
  border(2,0,1,4) { x:=2-t; y:=-3 };
  border(2,0,1,24) { x:=1; y:=-3+6*t };
  /* The rectangle on the left */
  border(3,0,1,4) { x:=-2+t; y:=3 };
  border(3,0,1,24) { x:=-1; y:=3-6*t };
  border(3,0,1,4) { x:=-1-t; y:=-3 };
  border(3,0,1,24) { x:=-2; y:=-3+6*t };
  buildmesh(800);

  /* Boundary conditions and PDE */
  solve(v) {
    onbdy(1) v = 0;
    onbdy(2) v = 1;
    onbdy(3) v = -1;
    pde(v) -laplace(v) = 0;
  };
  plot(v);
シールドされた平行平板のコンデンサの電位分布を求めています。円筒シールドの 電位が 0、2つの電極の電位はそれぞれ +1, -1 V です。Laplace方程式で 記述される静電界の解析は電気工学の基本です。

o 熱伝達と放射

  border(1,0,22,89)
  {
    if(t<=10)then { x:= t; y:=0 ; ib:=3 };
    if((t>10)and(t<11))then { x:=10; y:=t-10; ib:=2 };
    if((t>=11)and(t<=21))then { x:=21-t; y:=1; ib:=4 };
    if(t>21)then { x:=0; y:=22-t; ib:=1 };
  };
  buildmesh(800);

  changewait;
  t0 := 10;   t1 := 100;   te := 25; b=0.1; c = 5.0e-8;
  w = (b + 2*c * (te+546)*(te+273)*(te+273));
  solve(v,1) {
    onbdy(1) v=t0;
    onbdy(2) v = t1;
    onbdy(3) dnu(v)=0;
    onbdy(4) id(v) * w + dnu(v) = te * w;
    pde(v) -laplace(v) * y =0;
  };
  iter(10) {
    u=v;
    w = (b + c * (u+te + 546)*((u+273)*(u+273) + (te+273)*(te+273)));
    solve(v,-1) {
      onbdy(1) v=t0;
      onbdy(2) v = t1;
      onbdy(3) dnu(v)=0;
      onbdy(4) id(v)*w + dnu(v)= te * w;
      pde(v) -laplace(v) * y = 0
    };
  };
  plot(v);
厚さ 1、幅 10 の長方形断面の無限平板の左側の温度が 10 C、右側の温度が 100 C、 上側が室温 15 C への放射、下側が断熱という条件の熱伝導問題です。

対流による熱伝達の簡単なモデル

  (d/dn)(u) = -b*(u - te)
と黒体輻射の放射モデル

  (d/dn)(u) - c*((u + 273)^4 - (te + 273)^4)) = 0
を組み合わせると

  (d/dn)(u) + b*(u-te) - c*((u+273)^4 - (te+273)^4)) = 0
になりますが、非線形問題ですから、反復法(iteration)で解くことになって、 直前の u を u、その次の u を U とすれば、

  a^4 - b^4 = (a - b)*(a + b)*(a^2 + b^2)
を考慮して

  (d/dn)(U) + b*(U-te) - c*(U-te)*(u+te+546)*((u+273)^2 + (te+273)^2)) = 0
を解けばよいことになりますが、プログラムでは

  v = u - te
を初期値 0 で計算しています。

o 熱伝導: 一様でない材料

  h0:=1.0; h1:=2.0;
  border(1,0,22,89) {
    if(t<10)then { x:=t; y:=0; ib:=3 };
    if((t>=10)and(t<=11))then { x:=10; y:=h1*(t-10); ib:=2 };
    if((t>11)and(t<21))then { x:=21-t; y:=h1; ib:=4 };
    if(t>=21)then { x:=0; y:=h1*(22-t)i; ib:=1 };
  };
  border(0,0,10,41) { x:=t; y:=h0 };
  buildmesh(800);

  t0=10; t1=100; alpha:=1;
  kappa=0.01 + one(y>h0);
  solve(v) {
    onbdy(1) v=t0;
    onbdy(2) v=t1;
    onbdy(4) dnu(v)=0;
    onbdy(3) dnu(v)=0;
    pde(v) -laplace(v)*kappa*y + id(v)*kappa*y = 0;
  };
  plot(v);
厚さ 2、幅 10 の無限平板の熱伝導問題で、左端の温度が 10 C、右端の温度が 100 C、 上下は断熱ですが、板の上半分と下半分で熱伝導率が異なります。領域番号 0 の border() で材料定数の違う部分に境界条件のない内部境界を作っています。

o 圧縮性ポテンシャル流れ

  changewait;/* gamma = 1.4, outer circle radius is 5 */
  mach1 := 1/sqrt(6); machinfty = 0.85*mach1;
  rhoinfty=sqrt((1-machinfty^2)^5);
  solve(phi) begin
    onbdy(1) dnu(phi) = rhoinfty*machinfty*x/5; onbdy(2) dnu(phi) = 0;
    pde(phi) id(phi)*0.0001-laplace(phi) = 0;
  end;
  u1 = dx(phi); u2 = dy(phi); rho=sqrt((1-(u1^2+ u2^2))^5); plot(phi);
  iter(5)
  begin
    solve(phi) begin
      onbdy(1) dnu(phi)=rhoinfty*machinfty*x/5;
      onbdy(2) dnu(phi) = 0;
      pde(phi) id(phi)*0.0001-laplace(phi)*rhop=0;
    end;
    u1 = dx(phi); u2 = dy(phi); rho=sqrt((1-(u1^2+ u2^2))^5);
    rhop = convect(rho,u1,u2,0.1); plot(rho)
  end;
  plot(sqrt((u1^2+u2^2))/mach1);
オリジナルドキュメントのこの例題は不完全で、意図がよくわかりません。

o ナビエ・ストークス方程式

  /* Poor but better than none algorithm*/
  changewait;
  border(1,0,1,6)  { x:=0;          y:=1-t    };
  border(2,0,1,15) { x:=2*t;        y:=0      };
  border(2,0,1,10) { x:=2;          y:=-t     };
  border(2,0,1,20) { x:=2+3*t;      y:=-1     };
  border(2,0,1,35) { x:=5+15*t;     y:=-1     };
  border(3,0,1,10) { x:=20;         y:=-1+2*t };
  border(4,0,1,35) { x:=5+15*(1-t); y:=1      };
  border(4,0,1,40) { x:=5*(1-t);    y:=1      };
  buildmesh(900);

  nu = 0.002; dt := 0.4;

  /* initial pressure */
  solve(p,1) {
    onbdy(1)dnu(p) =-2*nu;
    onbdy(3) p=0; onbdy(2,4) dnu(p) = 0;
    pde(p) - laplace(p)= 0;
  };
  /* initial horizontal velocity */
  solve(u,2) {
    onbdy(1) u = y*(1-y);
    onbdy(3) dnu(u) = 0; onbdy(2,4) u = 0;
    pde(u) id(u)/dt-laplace(u)*nu = -min(y*y-y,0)/dt;
  };
  /* initial vertical velocity */
  solve(v,3){
    onbdy(1,3)v = 0; onbdy(2,4) v = 0;
    pde(v) id(v)/dt-laplace(v)*nu =0;
  };
  un = u; vn = v;
  iter(80)
  {
    f=convect(un,u,v,dt); g=convect(vn,u,v,dt);
    /*Horizontal velocity*/
    solve(u,-2) {
      onbdy(1) u = y*(1-y); onbdy(2,4) u = 0;
      onbdy(3)dnu(u)=0;
      pde(u) id(u)/dt-laplace(u)*nu = f/dt -dx(p);
    };
    plot(u);
    /* Vertical velocity */
    solve(v,-3) {
      onbdy(1,2,3,4) v = 0;
      pde(v) id(v)/dt-laplace(v)*nu = g/dt -dy(p);
    };
    /* Pressure */
    solve(p,-1) {
      onbdy(1)dnu(p) =-2*nu;
      onbdy(3) p=0; onbdy(2,4) dnu(p) = 0;
      pde(p) -laplace(p)= -(dx(f) + dy(g))/dt;
    };
    un = u; vn = v;
  };
  save('u.dta',u); save('v.dta',v); save('p.dta',p); plot(u);
Navier Stokes方程式は流体力学の足場の1つです。

3.3. 複素変数

  complex; nowait;
  border(1,0,1,10) { x:=t; y:=0; };
  border(1,0,1,10) { x:=1; y:=t; };
  border(2,0,1,10) { x:=1-t; y:=1; };
  border(1,0,1,10) { x:=0; y:=1-t; };
  buildmesh(200);

  solve(u)   /* observe than Re(u) = Im(u) */
  {
    onbdy(1,2) u=0;
    pde(u) id(u)-laplace(u)=1+I ;
  };
  v=Im(u);
  plot(u);plot(v);plot(u-v);
解の実部と虚部が一致することを確認しています。プログラムの検査用でしょうか。

3.4. 連立方程式

  /* This is a 2-system example for which the solution is know
  analytically, thus the precision of Gfem can be checked */
  nowait;
  ns:=40;
  border(1,0,2*pi,2*ns) { x:= 3*cos(t); y:= 2*sin(t); };
  border(2,0,2*pi,ns)    { x:= cos(-t); y:= sin(-t); };
  buildmesh(ns*ns);

  ue= sin(x+y);
  ve = ue;
  p = ue;
  nx = -x;
  ny =- y;
  dxue = cos(x+y);

  c = 0.2;
  a1 = y;
  a2 =x;
  nu = 1;
  nu11 = 1;
  nu22 = 2;
  nu21 =0.3;
  nu12 =0.4;
  b=1;

  dnuue=dxue*(nu*(nx+ny) +
  (nu11 + nu12)*nx + (nu21+ nu22)*ny);
  g = ue*c+dnuue;
  f = b*ue+dxue*(a1+a2) +ue*(2*nu+nu11+nu12+nu21+nu22);

  solve(u,v) {
    onbdy(1) u = p;
    onbdy(1) v = p;
    onbdy(2) id(u)*c/2 + id(v)*c/2 + dnu(u) = g;
    onbdy(2) id(v)*c + dnu(v) = g;
    pde(u) id(u)*b + dx(u)*a1 + dy(u)*a2
             -laplace(u)*nu - dxx(u)*nu11 -
             dxy(u)*nu12 - dyx(u)*nu21 - dyy(u)*nu22 =f;

    pde(v) id(v)*b/2+id(u)*b/2
             + dx(v)*a1 + dy(v)*a2 -laplace(v)*nu
             - dxx(v)*nu11 - dxy(v)*nu12
             - dyx(v)*nu21 - dyy(v)*nu22 = f;
  };

  plot(abs(u-ue) + abs(v-ve));

4. 注

このドキュメントはFreeFEMのオリジナル・ドキュメント FreeFEM Documentationを書き換えたものです。説明不足と思われる部分は わかる範囲で補足しましたが、省略した部分もあります。

4.1 注 1

  (d/dt)A - (d/dx)(n*(d/dx)(A)) + (d/dy)(n*(d/dy)(A)) = g*V
の時間軸を離散化して、Crank-Nicolson 解法を適用すると、
  (Am+1 - Am)/Dt - (n/2)*laplace(Am+1 + Am) = g*V
ですが、ここで、

  w = (Am+1 + Am)/2
と置き換えると、

  (2/Dt)*(w - Am) - n*lalplace(w) = g*V
  Am+1 = 2*w - Am
になって、

  solve(w) {
    pde(w) id(w)*2/dt-laplace(w)*n = g*V + a*2/dt
  };
  a = 2*w - a
の繰り返しとしてプログラミングできます。

5. ローカルな機能追加

以下、FreeFEMのオリジナルにはなくて、ローカル版で私が自分で使うために 追加した機能です。

save
save() のファイル名として「-」を指定すると、stdout に書き出します。結果の 表示に便利です。
数学関数
C と同じ、atan2(y,x), fmod(x,y), floor(x) を追加しました。
while() 文
while(logical_expr) 文を追加しました。
for() 文
for(instruction; logical_expr; instruction) を追加しました。
++, --, +=, -=, *=, /=, &&, || 演算子
「++」「--」「+=」「-=」「*=」「/=」「&&」「||」の演算子を追加しました。
delay() 関数の追加
delay(n) で n マイクロ秒の遅延が得られます。連続表示のタイミング調整に使います。
border() の機能
border(n,...) の n に定数だけでなく、式も使えるようにしました。
要素分割の領域(region)表示
「-r」オプションを指定するか要素数が少ないとき、region 番号も表示するよう にしました。
要素分割の境界(boundary)番号表示
「-b」オプションを指定すると境界番号も表示するようにしました。
printf(fmt,p1,p2,p3)
C と同じ printf() 文を追加しました。パラメータは最大3つまでしか使えません。
onbdy(b)
onbdy(b) の b に式も使えるようにしました。
plotmesh
「Introduction to Scientific Computing」で使われている plotmesh を追加 しました。

また、オリジナル版のバグと思える、次の点を修正しました。

  1) intt[u] が intt(0)[u] だけでなく、全ての要素を積分するようにした
  2) save('file',f) の f が実数のとき複素数形式で書かないようにした
  3) iter() のパラメータは定数だけでなく、式も使えるようにした
  4) 「%」演算子が fmod() として機能するようにしました
  5) 「!=」演算子の token を認識するようにした
  6) exec() 関数が実際に使えるようにした

別のテキストを見ると、既に 2) は未公開版で修正されているようです。

平林浩一, 1998-08-26, 1999-10-25