2. Tutorial#
In this tutorial, we will attempt to solve a simple shape optimization problem, which will hopefully illustrate what this little code can do. The equations we work with are the two-phase static Stokes equations
The main usecase for these equations is to describe droplets suspended in an ambient fluid. At the interface, we pose the existence of a constant surface tension. This gives rise to the well-known Young-Laplace law. Specifically, we have the following jump conditions
where \(\kappa\) is the sum of the principal curvatures, \(\mathbf{n}\) is the normal exterior to the drop (here denoted by the \(-\)) and \(\mathrm{Ca}\) is the Capillary number.
Using these constraints, we propose to minimize the cost functional
The desired velocity field \(u_d\) is solved on a desired geometry \(\mathbf{X}_d\), which we will see at the end. We now proceed to setting up this problem in code as follows.
1% {{{ flow setup
2
3% As a first step, we will initialize a simple extensional flow.
4
5% The actual parameters don't matter that much for this example, but a unit
6% viscosity ratio will make it a lot faster (no need to solve a system)
7
8param.Ca = 0.1;
9param.lambda = 1.0;
10
11% We use a special function to get all the boundary conditions. There's
12% quite a few that need to be setup, so this is faster. The ``'fn'`` key
13% can also be a struct with all the boundary conditions we would like to
14% have for custom cases.
15
16bc = st_boundary_condition_options(...
17 'param', param, ...
18 'fn', 'FourRoller');
19
20% Next, we also initialize a special struct for the constant capillary
21% number Young-Laplace jump we are going to use. The jump is already defined
22% in the boundary conditions, but this struct will also contain expressions
23% for shape derivatives of this particular jump, so we don't have to worry
24% about it.
25
26bc.jump = st_jump_capillary('param', bc.param);
27
28% }}}
29
30% {{{ cost functional
31
32% For the cost functional, we will attempt to match the normal velocity on
33% a given geometry. Here, we just choose a nice UFO shaped geometry for fun.
34
35% As for the jump, calling :func:`st_cost_normal_velocity` also defines all
36% the required derivatives for this particular cost functional.
37
38curve = @(xi) st_mesh_curve(xi, 'ufo', 'R', 1.0, 'k', 2);
39cost = st_cost_normal_velocity(...
40 'param', bc.param, ...
41 'desired_curve', curve);
42
43% Furthermore, it also defines all the variables that are required to compute
44% the cost and all the derivatives and such. We need to aggregate these, so
45% we can construct all the quadratures later on.
46variables = unique([...
47 cost.variables, ...
48 cost.forward_variables, ...
49 cost.adjoint_variables]);
50
51% }}}
52
53% {{{ geometry setup
54
55% Now that the flow is setup, we define a simple geometry for our initial
56% guess in the optimization algorithm. We use a 4th order scheme here.
57
58% We also pass all the variables in the problem to :func:`st_geometry`, since
59% it knows how to construct quadrature rules for all of them. The singular
60% quadrature rules will have orders / nodes based on `nnodes` and some useful
61% defaults. However, one can also pass a `s_quad` key with specific values
62% for each one (this is passed to :func:`st_layer_quadrature`).
63
64geometry = st_geometry(...
65 'npanels', 32, ...
66 'nnodes', 4, ...
67 'curve', @(xi) st_mesh_curve(xi, 'circle', 'r', 1.0), ...
68 's_singularity', variables);
69
70x0 = geometry.x;
71y0 = geometry.y;
72
73% }}}
74
75% {{{ optimization setup
76
77% Next, we setup the parameters for the optimization routine. We use a
78% gradient descent method with a very simple backtracking line search based
79% on the Armijo rule. In general, shape optimization works best for small
80% deformations, so small step sizes will be common and a fancy optimization
81% method will nor fare substantially better.
82
83% There are two parameters below that require some explanation. The `alpha_max`
84% parameter gives and upper bound for the step size. We select it dynamically
85% in such a way that an update maintains the axisymmetric assumption that all
86% the points need to reside on the top half plane. Second, we also pass a
87% `filter_strength`, which is used to further smooth the gradient during
88% optimization.
89
90cg = st_optim_options(...
91 'display', true, ...
92 'beta_update', 'flecherreeves', ...
93 'maxit', 128, ...
94 'gtol', 1.0e-6, ...
95 'alpha_min', 1.0e-8, ...
96 'alpha_max', @st_optim_geometry_alpha_max, ...
97 'user', struct('filter_strength', 1.0e-2));
98
99% }}}
100
101% {{{ desired velocity field
102
103% Now that we have a geometry, we will use the same parameters to compute
104% the desired geometry in the cost functional. This will compute the points
105% themselves and other variables (e.g. curvature and normal) on the desired
106% curve. We will use this information to solve for the velocity on this
107% curve and then use that as part of the optimization.
108
109[xd, yd] = st_mesh_geometry_update(x0, y0, cost.desired_curve);
110cost.x = xd;
111cost.y = yd;
112
113% To solve, we simply call :func:`st_repr_density_solution` and tell it what
114% variables to compute and on what grid. This will give us the full velocity
115% field on the surface of the desired geometry.
116
117solution = st_repr_density_solution(xd, yd, bc, cost.variables);
118
119cost.user.udx = st_dot(solution.u, xd.n);
120cost.user.udy = st_dot(st_interpolate(xd, solution.u, yd.xi), yd.n);
121
122% }}}
123
124% {{{ optimization
125
126% We now have done all the (super tedious) setup for our problem and can just
127% pass it on to the optimizer and hope or the best!
128
129% However, before doing that, we'll also set up a cache and have it output data
130% at each iteratin for us. This is completely up to user control, so we just
131% define an output function for the optimization.
132
133cache = st_cache_create('tutorial', 'now', 'shape_optim');
134cg.output = @(info) optim_output(info, cache, geometry, bc, cost);
135
136% And on to the optimization!
137
138[x, stats] = st_optim_geometry(cg, geometry, bc, cost);
139[xf, yf] = st_mesh_geometry_update(x0, y0, x);
140
141% }}}
142
143% {{{ results
144
145% We now have a solution, so lets plot it and see how well we did!
146
147z0 = st_array_ax(x0.x);
148zd = st_array_ax(xd.x);
149zf = st_array_ax(xf.x);
150
151fig = figure();
152h1 = plot(real(z0), imag(z0), ':');
153h2 = plot(real(zd), imag(zd), 'k-');
154h3 = plot(real(zf), imag(zf), '--');
155axis('equal');
156grid('on');
157xlabel('$x$');
158ylabel('$\rho$');
159legend([h1, h2, h3], {'Original', 'Desired', 'Final'}, ...
160 'Location', 'EastOutside');
161st_print(fig, 'tutorial_shape_optimization.png')
162
163% }}}
164
165function optim_output(info, cache, geometry, bc, cost) %#ok
166 % TODO
167end