Tutorial

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

\[\begin{split}\begin{cases} \nabla \cdot \mathbf{u}_\pm = 0, \\ \nabla \cdot \sigma_\pm = 0, \\ \mathbf{u}_+ = \mathbf{u}_\infty. \end{cases}\end{split}\]

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

\[\begin{split}\begin{cases} [\![ \mathbf{u} ]\!] = 0, \\ \displaystyle [\![ \mathbf{n} \cdot \sigma ]\!] = \frac{1}{\mathrm{Ca}} \kappa \mathbf{n}, \end{cases}\end{split}\]

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

\[\mathcal{J} = \frac{1}{2} \int_\Sigma |\mathbf{u} \cdot \mathbf{n} - u_d| \,\mathrm{d}S.\]

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