Skip to content

Commit 1dfe1ab

Browse files
Add Nutils participants for turek-hron-fsi3
1 parent b871707 commit 1dfe1ab

File tree

8 files changed

+702
-0
lines changed

8 files changed

+702
-0
lines changed

turek-hron-fsi3/fluid-nutils/clean.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env sh
2+
set -e -u
3+
4+
. ../../tools/cleaning-tools.sh
5+
6+
clean_nutils .
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Geometry file for the turek.py example.
2+
//
3+
// This is a generalized description of the setup defined by Turek and Hron,
4+
// which requires the following lenghts to be supplied externally, using the
5+
// numbers argument of mesh.gmsh or the -setnumber switch when invoking the
6+
// gmsh application directly:
7+
//
8+
// - channel_length: length of the fluid domain
9+
// - channel_height: height of the fluid domain
10+
// - x_center: horizontal position of the cylinder measured from the left edge
11+
// - y_center: vertical position of the cylinder measured from the bottom edge
12+
// - cylinder_radius: radius of the cylinder
13+
// - structure_length: length of the elastic structure measured from the cylinder wall
14+
// - structure_thickness: thickness of the elastic structure
15+
// - min_elemsize: mesh element size at the solid/fluid interface
16+
// - max_elemsize: mesh element size at the channel wall and far field
17+
//
18+
// The parameterization matches largely that of Table 1 of Turek and Hron 2006,
19+
// with the main difference that reference points A and B cannot be
20+
// independently placed but are always located at the tip of the elastic
21+
// structure and the leading edge of the cylinder, respectively.
22+
23+
SetFactory("OpenCASCADE");
24+
25+
Rectangle(1) = {0, 0, 0, channel_length, channel_height};
26+
Rectangle(2) = {x_center, y_center - structure_thickness/2, 0, cylinder_radius + structure_length, structure_thickness, 0};
27+
Disk(3) = {x_center, y_center, 0, cylinder_radius};
28+
BooleanDifference(4) = { Surface{2}; }{ Surface{3}; };
29+
BooleanDifference(5) = { Surface{1}; }{ Surface{2,3}; };
30+
A = newp; Point(A) = {x_center + cylinder_radius + structure_length, y_center, 0};
31+
B = newp; Point(B) = {x_center - cylinder_radius, y_center, 0};
32+
33+
// At this point surface 3 (cylinder), 4 (solid domain) and 5 (fluid domain) are
34+
// non-overlapping. Gmsh promises that the boolean fragments operation with
35+
// deletion will reuse the surface IDs for the new objects.
36+
37+
_() = BooleanFragments{ Surface{3,4,5}; Point{A,B}; Delete; }{};
38+
39+
// Fragments deduplicates boundary segments, which means that we can now
40+
// perform boolean operations on the index sets.
41+
42+
bnd_cylinder() = Abs(Boundary{ Surface{3}; });
43+
bnd_structure() = Abs(Boundary{ Surface{4}; });
44+
tmp = bnd_structure();
45+
bnd_structure -= bnd_cylinder();
46+
bnd_cylinder -= tmp();
47+
bnd_fluid() = Abs(Boundary{ Surface{5}; });
48+
bnd_fluid -= bnd_structure();
49+
bnd_fluid -= bnd_cylinder();
50+
51+
// After subtracting the inner boundaries, only the four boundary segments of
52+
// rectangle 1 remain in bnd_fluid, and we are going to assume that they are
53+
// ordered bottom, right, top, left.
54+
55+
Physical Surface("fluid") = {5};
56+
Physical Line("inlet") = {bnd_fluid(3)};
57+
Physical Line("outlet") = {bnd_fluid(1)};
58+
Physical Line("wall") = {bnd_fluid(0), bnd_fluid(2)};
59+
Physical Line("cylinder") = {bnd_cylinder()};
60+
Physical Line("structure") = {bnd_structure()};
61+
Physical Point("A") = {A};
62+
Physical Point("B") = {B};
63+
64+
// The element size is set to be uniformly min_elemsize inside the elastic
65+
// structure, and grow linearly to max_elemsize in the fluid domain over a
66+
// distance of half the channel height.
67+
68+
Mesh.MeshSizeFromPoints = 0;
69+
Mesh.MeshSizeFromCurvature = 0;
70+
Mesh.MeshSizeExtendFromBoundary = 0;
71+
Field[1] = Distance;
72+
Field[1].SurfacesList = {3,4};
73+
Field[2] = Threshold;
74+
Field[2].InField = 1;
75+
Field[2].DistMin = 0;
76+
Field[2].DistMax = channel_height/2;
77+
Field[2].SizeMin = min_elemsize;
78+
Field[2].SizeMax = max_elemsize;
79+
Background Field = 2;

turek-hron-fsi3/fluid-nutils/fluid.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Turek benchmark fluid solver, modified from https://examples.nutils.org/official-turek/
2+
3+
from precice import Participant
4+
from nutils import cli, export, function
5+
from nutils.mesh import gmsh
6+
from nutils.solver import System
7+
from nutils.SI import Length, Density, Viscosity, Velocity, Time, Stress, Acceleration
8+
from nutils.expression_v2 import Namespace
9+
from collections import defaultdict, deque
10+
from dataclasses import dataclass
11+
from typing import Optional
12+
from pathlib import Path
13+
import treelog as log
14+
import numpy
15+
16+
17+
# reference lenghts used in communication
18+
precice_length = Length('m')
19+
precice_time = Time('s')
20+
precice_stress = Stress('Pa')
21+
22+
23+
@dataclass
24+
class Domain:
25+
channel_length: Length = Length('2.5m')
26+
channel_height: Length = Length('.41m')
27+
x_center: Length = Length('.2m')
28+
y_center: Length = Length('.2m')
29+
cylinder_radius: Length = Length('5cm')
30+
structure_length: Length = Length('35cm')
31+
structure_thickness: Length = Length('2cm')
32+
min_elemsize: Length = Length('4mm')
33+
max_elemsize: Length = Length('4cm')
34+
35+
def generate_mesh(self):
36+
u = Length('m') # temporary reference length
37+
38+
topo, geom = gmsh(Path(__file__).parent/'fluid.geo', dimension=2, order=2, numbers={
39+
'channel_length': self.channel_length/u,
40+
'channel_height': self.channel_height/u,
41+
'x_center': self.x_center/u,
42+
'y_center': self.y_center/u,
43+
'cylinder_radius': self.cylinder_radius/u,
44+
'structure_length': self.structure_length/u,
45+
'structure_thickness': self.structure_thickness/u,
46+
'min_elemsize': self.min_elemsize/u,
47+
'max_elemsize': self.max_elemsize/u})
48+
49+
# consistency check
50+
#zeros = topo.boundary['inlet,wall,outlet,cylinder,structure'].integrate(function.normal(geom) * function.J(geom, 1), degree=2)
51+
#numpy.testing.assert_allclose(zeros, 0, atol=1e-14, err_msg='boundaries do not form a watertight hull')
52+
53+
bezier = topo.sample('bezier', 2)
54+
bezier_structure = topo['fluid'].boundary['structure'].sample('bezier', 3)
55+
bezier_cylinder = topo['fluid'].boundary['cylinder'].sample('bezier', 3)
56+
A = topo.points['A'].sample('gauss', 1).eval(geom)
57+
with export.mplfigure('mesh.jpg', dpi=150) as fig:
58+
ax = fig.add_subplot(111)
59+
export.triplot(ax, bezier.eval(geom), hull=bezier.hull)
60+
export.triplot(ax, bezier_structure.eval(geom), hull=bezier_structure.tri, linewidth=1, linecolor='r')
61+
export.triplot(ax, bezier_cylinder.eval(geom), hull=bezier_cylinder.tri, linewidth=1, linecolor='b')
62+
ax.set_xlim(0, 2*self.channel_height/u)
63+
64+
return topo, geom * u
65+
66+
67+
@dataclass
68+
class Fluid:
69+
density: Density = Density('1kg/L')
70+
viscosity: Viscosity = Viscosity('1Pa*s')
71+
velocity: Velocity = Velocity('1m/s')
72+
73+
def reynolds(self, reference_length):
74+
return self.density * self.velocity * reference_length / self.viscosity
75+
76+
77+
@dataclass
78+
class Dynamic:
79+
init: Time = Time('2s')
80+
window: Time = Time('1s')
81+
gamma: float = .8
82+
beta: float = .4
83+
84+
def set_timestep(self, timestep):
85+
self.timestep = timestep
86+
self.timeseries = defaultdict(deque(maxlen=round(self.window / timestep)).copy)
87+
88+
@property
89+
def times(self):
90+
'Return all configured time steps for the simulation.'
91+
92+
t = Time('0s')
93+
while True:
94+
t += self.timestep
95+
yield t
96+
97+
def ramp_up(self, t):
98+
return .5 - .5 * numpy.cos(numpy.pi * min(t / self.init, 1))
99+
100+
def rate(self, v1, subs):
101+
dt = function.field('dt') * precice_time
102+
v0 = function.replace_arguments(v1, subs)
103+
return (v1 - v0) / dt
104+
105+
def add_and_plot(self, name, t, v, ax):
106+
'Add data point and plot time series for past window.'
107+
108+
d = self.timeseries[name]
109+
d.append((t, v))
110+
times, values = numpy.stack(d, axis=1)
111+
ax.plot(times, values)
112+
ax.set_ylabel(name)
113+
ax.grid()
114+
ax.autoscale(enable=True, axis='x', tight=True)
115+
116+
# The Newmark-beta scheme is used for time integration, taking displacement
117+
# (solid) or velocity (fluid) as primary variable with time derivatives
118+
# introduced via helper arguments that are updated after every solve.
119+
#
120+
# d = d0 + δt u0 + .5 δt^2 aβ, where aβ = (1-2β) a0 + 2β a
121+
# => δd = δt u0 + δt^2 [ .5 a0 + β δa ]
122+
# => δa = [ δd / δt^2 - u0 / δt - .5 a0 ] / β
123+
#
124+
# u = u0 + δt aγ, where aγ = (1-γ) a0 + γ a
125+
# => δu = δt [ a0 + γ δa ]
126+
# => δa = [ δu / δt - a0 ] / γ
127+
128+
def newmark_defo_args(self, d, d0=0., u0δt=0., a0δt2=0., **args):
129+
δaδt2 = (d - d0 - u0δt - .5 * a0δt2) / self.beta
130+
uδt = u0δt + a0δt2 + self.gamma * δaδt2
131+
aδt2 = a0δt2 + δaδt2
132+
return dict(args, d=d+uδt+.5*aδt2, d0=d, u0δt=uδt, a0δt2=aδt2)
133+
134+
def newmark_defo(self, d):
135+
D = self.newmark_defo_args(d, *[function.replace_arguments(d, [('d', t)]) for t in ('d0', 'u0δt', 'a0δt2')])
136+
return D['u0δt'] / self.timestep, D['a0δt2'] / self.timestep**2
137+
138+
def newmark_velo_args(self, u, u0=0., a0δt=0., **args):
139+
aδt = a0δt + (u - u0 - a0δt) / self.gamma
140+
return dict(args, u=u+aδt, u0=u, a0δt=aδt)
141+
142+
def newmark_velo(self, u):
143+
D = self.newmark_velo_args(u, *[function.replace_arguments(u, [('u', t)]) for t in ('u0', 'a0δt')])
144+
return D['a0δt'] / self.timestep
145+
146+
147+
def main(domain: Domain = Domain(), fluid: Fluid = Fluid(), dynamic: Dynamic = Dynamic()):
148+
149+
participant = Participant('Fluid', '../precice-config.xml', 0, 1)
150+
151+
log.info('Re:', fluid.reynolds(2*domain.cylinder_radius))
152+
153+
topo, geom = domain.generate_mesh()
154+
155+
d = topo.field('d', btype='std', degree=1, shape=[2]) * precice_length
156+
157+
sqr = topo.boundary['structure'].sample('bezier', 2).integral((d - geom) @ (d - geom)) / 'm2'
158+
r_points = System(sqr, trial='d').solve_constraints(droptol=1e-10)['d']
159+
r_where = numpy.isfinite(r_points).any(1)
160+
assert not numpy.isnan(r_points[r_where]).any()
161+
r_name = "Fluid-Mesh-Nodes"
162+
r_ids = participant.set_mesh_vertices(r_name, r_points[r_where])
163+
164+
w_name = "Fluid-Mesh-Centers"
165+
w_sample = topo.boundary['structure'].sample('gauss', degree=1)
166+
w_ids = participant.set_mesh_vertices(w_name, w_sample.eval(geom) / precice_length)
167+
168+
participant.initialize()
169+
170+
timestep = participant.get_max_time_step_size()
171+
dynamic.set_timestep(timestep * precice_time)
172+
173+
ns = Namespace()
174+
ns.δ = function.eye(2)
175+
ns.ρf = fluid.density
176+
ns.μf = fluid.viscosity
177+
178+
ns.xref = geom
179+
ns.define_for('xref', gradient='∇ref', jacobians=('dVref', 'dSref'))
180+
181+
ns.d = d
182+
ns.x_i = 'xref_i + d_i'
183+
ns.F_ij = '∇ref_j(x_i)' # deformation gradient tensor
184+
ns.C_ij = 'F_ki F_kj' # right Cauchy-Green deformation tensor
185+
ns.J = numpy.linalg.det(ns.F)
186+
187+
ns.define_for('x', gradient='∇', normal='n', jacobians=('dV', 'dS'))
188+
189+
ns.urel = topo.field('u', btype='std', degree=2, shape=(2,)) * fluid.velocity
190+
ns.v, ns.a = dynamic.newmark_defo(ns.d)
191+
ns.arel = dynamic.newmark_velo(ns.urel)
192+
ns.u_i = 'v_i + urel_i'
193+
ns.DuDt_i = 'a_i + arel_i + ∇_j(u_i) urel_j' # material derivative
194+
195+
ns.p = topo.field('p', btype='std', degree=1) * fluid.viscosity * fluid.velocity / domain.cylinder_radius
196+
ns.σ_ij = 'μf (∇_j(u_i) + ∇_i(u_j)) - p δ_ij' # fluid stress tensor
197+
198+
y = ns.xref[1] / domain.channel_height
199+
uin = 6 * fluid.velocity * y * (1 - y)
200+
sqr = topo.boundary['wall,cylinder,structure'].integral(ns.urel @ ns.urel, degree=4) / 'm2/s2'
201+
sqr += topo.boundary['wall,cylinder,inlet,outlet'].integral(ns.d @ ns.d, degree=4) / 'm2'
202+
sqr += topo.boundary['inlet'].integral((ns.urel[0] - uin)**2 * ns.dSref, degree=4) / 'm3/s2'
203+
sqr += topo.boundary['inlet,outlet'].integral(ns.urel[1]**2, degree=4) / 'm2/s2'
204+
cons = System(sqr, trial='u,d').solve_constraints(droptol=1e-10)
205+
ucons = cons['u']
206+
cons['u'] = ucons * 0
207+
208+
# ρf DuDt = div σ => ∀q: 0 = ∫ q·(ρf DuDt - div σ) = ∫ (ρf q·DuDt + ∇q:σ)
209+
ns.utest = function.replace_arguments(ns.urel, 'u:utest') / (fluid.viscosity * fluid.velocity**2)
210+
ns.ptest = function.replace_arguments(ns.p, 'p:ptest') / (fluid.viscosity * fluid.velocity**2)
211+
res = topo.integral('(utest_i ρf DuDt_i + ∇_j(utest_i) σ_ij + ptest ∇_k(u_k)) dV' @ ns, degree=4)
212+
sys_up = System(res, trial='u,p', test='utest,ptest')
213+
214+
# t = -σ·n => ∀q: ∮ -q·t = ∮ q·σ·n = ∫ div(q·σ) = ∫ (∇q:σ + q·div σ) = ∫ (∇q:σ + ρf q·DuDt)
215+
ns.t = topo.field('t', btype='std', degree=2, shape=[2]) * (fluid.viscosity * fluid.velocity / domain.cylinder_radius)
216+
res += topo.boundary['cylinder,structure'].integral('utest_i t_i dS' @ ns, degree=4)
217+
sys_t = System(res, trial='t', test='utest')
218+
sqr = topo.boundary['cylinder,structure'].integral('t_i t_i' @ ns, degree=4) / 'Pa2'
219+
cons['t'] = numpy.isnan(System(sqr, trial='t').solve_constraints(droptol=1e-10)['t'])
220+
F = topo.boundary['cylinder,structure'].integral('t_i dS' @ ns, degree=4)
221+
222+
# mesh continuation with jacobian based stiffness
223+
sqr = topo.integral('C_kk - 2 log(J)' @ ns, degree=4)
224+
sys_d = System(sqr, trial='d')
225+
226+
# initial values
227+
args = {a: numpy.zeros(function.arguments_for(res)[a].shape) for a in 'ud'}
228+
229+
bezier = topo['fluid'].sample('bezier', 3)
230+
bezier = bezier.subset(bezier.eval(geom[0]) < 2.2*domain.channel_height)
231+
x_bz = bezier.bind(ns.x)
232+
u_bz = bezier.bind(ns.u)
233+
p_bz = bezier.bind(ns.p) - topo.points['B'].sample('gauss', 1).bind(ns.p)[0]
234+
235+
bbezier = topo['fluid'].boundary['cylinder,structure'].sample('bezier', 3)
236+
x_bbz = bbezier.bind(ns.x)
237+
238+
assert participant.requires_writing_checkpoint()
239+
checkpoint = args
240+
241+
with log.iter.plain('timestep', dynamic.times) as times:
242+
t = next(times)
243+
244+
while participant.is_coupling_ongoing():
245+
246+
if participant.requires_reading_checkpoint():
247+
args = checkpoint
248+
249+
cons['d'][r_where] = participant.read_data(r_name, 'Displacement', r_ids, timestep)
250+
cons['u'] = ucons * dynamic.ramp_up(t)
251+
252+
args = dynamic.newmark_defo_args(**args)
253+
args = dynamic.newmark_velo_args(**args)
254+
args = sys_d.solve(arguments=args, constrain=cons, tol=1e-10) # mesh extension
255+
args = sys_up.solve(arguments=args, constrain=cons, tol=1e-10) # Navier-Stokes time step
256+
args = sys_t.solve(arguments=args, constrain=cons, tol=1e-10) # project traction
257+
258+
participant.write_data(w_name, 'Stress', w_ids, w_sample.eval(ns.t, args) / precice_stress)
259+
participant.advance(timestep)
260+
261+
if participant.requires_writing_checkpoint():
262+
checkpoint = args
263+
264+
x, xb, u, p = function.eval([x_bz, x_bbz, u_bz, p_bz], args)
265+
with export.mplfigure('solution.jpg', dpi=150) as fig:
266+
pstep = 25 * fluid.viscosity * fluid.velocity / domain.channel_height
267+
ax = fig.add_subplot(111, title=f'flow at t={t:.3s}, pressure contours every {pstep:.0Pa}', ylabel='[m]')
268+
vmax = 2 * fluid.velocity * dynamic.ramp_up(t)
269+
im = export.triplot(ax, x/'m', numpy.linalg.norm(u/'m/s', axis=1), tri=bezier.tri, cmap='inferno', clim=(0, vmax/'m/s'))
270+
ax.tricontour(*(x/'m').T, bezier.tri, p/pstep, numpy.arange(*numpy.quantile(numpy.ceil(p / pstep), [0,1])),
271+
colors='white', linestyles='solid', linewidths=1, alpha=.33)
272+
fig.colorbar(im, orientation='horizontal', label=f'velocity [m/s]')
273+
export.triplot(ax, xb/'m', hull=bbezier.tri, linewidth=1)
274+
ax.set_xlim(0, 2*domain.channel_height/'m')
275+
ax.set_ylim(0, domain.channel_height/'m')
276+
277+
D, L = function.eval(F, arguments=args)
278+
log.info(f'lift: {L:N/m}')
279+
log.info(f'drag: {D:N/m}')
280+
with export.mplfigure('force.jpg', dpi=150) as fig:
281+
dynamic.add_and_plot('lift [N/m]', t/'s', L/'N/m', ax=fig.add_subplot(211))
282+
dynamic.add_and_plot('drag [N/m]', t/'s', D/'N/m', ax=fig.add_subplot(212, xlabel='time [s]'))
283+
284+
t = next(times)
285+
286+
287+
if __name__ == '__main__':
288+
cli.run(main)

turek-hron-fsi3/fluid-nutils/run.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env bash
2+
set -e -u
3+
4+
. ../../tools/log.sh
5+
exec > >(tee --append "$LOGFILE") 2>&1
6+
7+
python3 -u fluid.py
8+
9+
close_log

turek-hron-fsi3/solid-nutils/clean.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env sh
2+
set -e -u
3+
4+
. ../../tools/cleaning-tools.sh
5+
6+
clean_nutils .

0 commit comments

Comments
 (0)