See these LinkedIn posts to see some comments and discussions:
- https://www.linkedin.com/feed/update/urn:li:activity:6831291311832760320/
- https://www.linkedin.com/feed/update/urn:li:activity:6973982270852325376/
Other people’s maze-related posts:
- https://www.linkedin.com/feed/update/urn:li:activity:6972370982489509888/
- https://www.linkedin.com/feed/update/urn:li:activity:6972949021711630336/
- https://www.linkedin.com/feed/update/urn:li:activity:6973522069703516160/
- https://www.linkedin.com/feed/update/urn:li:activity:6973921855275458560/
- https://www.linkedin.com/feed/update/urn:li:activity:6974663157952745472/
- https://www.linkedin.com/feed/update/urn:li:activity:6974979951049519104/
- https://www.linkedin.com/feed/update/urn:li:activity:6982049404568449024/
- https://www.linkedin.com/feed/update/urn:li:activity:6982049404568449024/
- https://www.linkedin.com/feed/update/urn:li:activity:7206676495879028736/
Say you are Homer Simpson and you want to solve a maze drawn in a restaurant’s placemat, one where both the start and end are known beforehand. In order to avoid falling into the alligator’s mouth, you can exploit the ellipticity of the Laplacian operator to solve any maze (even a hand-drawn one) without needing any fancy AI or ML algorithm. Just FeenoX and a bunch of standard open source tools to convert a bitmapped picture of the maze into an unstructured mesh.
PROBLEM laplace 2D # pretty self-descriptive, isn't it?
READ_MESH maze.msh
# boundary conditions (default is homogeneous Neumann)
BC start phi=0
BC end phi=1
SOLVE_PROBLEM
# write the norm of gradient as a scalar field
# and the gradient as a 2d vector into a .msh file
WRITE_MESH maze-solved.msh \
sqrt(dphidx(x,y)^2+dphidy(x,y)^2) \
VECTOR dphidx dphidy 0
$ gmsh -2 maze.geo
[...]
$ feenox maze.fee
$
Instead of solving a steady-state en exploiting the ellipticity of Laplace’s operator, let us see what happens if we solve a transient instead.
PROBLEM laplace 2D
READ_MESH maze.msh
(x,y) = 0 # inital condition
phi_0end_time = 100 # some end time where we know we reached the steady-state
alpha = 1e-6 # factor of the time derivative to make it advance faster
BC start phi=if(t<1,t,1) # a ramp from zero to avoid discontinuities with the initial condition
BC end phi=0 # homogeneous BC at the end (so we move from top to bottom)
SOLVE_PROBLEM
PRINT t
WRITE_MESH maze-tran-td.msh phi sqrt(dphidx(x,y)^2+dphidy(x,y)^2) VECTOR -dphidx(x,y) -dphidy(x,y) 0
$ feenox maze-tran-td.fee
0
0.00433078
0.00949491
0.0170774
0.0268599
[...]
55.8631
64.0819
74.5784
87.2892
100
$ gmsh maze-tran-td-anim.geo
# all frames dumped, now run
ffmpeg -y -framerate 20 -f image2 -i maze-tran-td-%03d.png maze-tran-td.mp4
ffmpeg -y -framerate 20 -f image2 -i maze-tran-td-%03d.png maze-tran-td.gif
$ ffmpeg -y -framerate 20 -f image2 -i maze-tran-td-%03d.png maze-tran-td.mp4
[...]
$ ffmpeg -y -framerate 20 -f image2 -i maze-tran-td-%03d.png maze-tran-td.gif
[...]
Now let us see what happens if we travel the maze from the exit up to the inlet. It looks like the solver tries a few different paths that lead nowhere until the actual solution is found.
PROBLEM laplace 2D
READ_MESH maze.msh
(x,y) = 0
phi_0end_time = 100
alpha = 1e-6
BC end phi=if(t<1,t,1)
BC start phi=0
SOLVE_PROBLEM
PRINT t
WRITE_MESH maze-tran-bu.msh phi sqrt(dphidx(x,y)^2+dphidy(x,y)^2) VECTOR -dphidx(x,y) -dphidy(x,y) 0
$ feenox maze-tran-bu.fee
0
0.00402961
0.00954806
0.0180156
0.0285787
[...]
65.3715
72.6894
81.8234
90.9117
100
$ gmsh maze-tran-bu-anim.geo
# all frames dumped, now run
ffmpeg -y -framerate 20 -f image2 -i maze-tran-bu-%03d.png maze-tran-bu.mp4
ffmpeg -y -framerate 20 -f image2 -i maze-tran-bu-%03d.png maze-tran-bu.gif
$ ffmpeg -y -framerate 20 -f image2 -i maze-tran-bu-%03d.png maze-tran-bu.mp4
[...]
$ ffmpeg -y -framerate 20 -f image2 -i maze-tran-bu-%03d.png maze-tran-bu.gif
[...]
The Laplace equation can be used to model potential flow, as illustrated with this example from Prof. Enzo Dari for his course “Fluid Mechanics” at Instituto Balseiro.
For the particular case of a airfoil profile, the Dirichlet condition at the wing has to satisfy the Kutta condition.
This example
nx
and ny
tx
and ty
using the
circle’s equationnx
and nz
over the circle, respectively.import sys
import math
import cmath
import gmsh
gmsh.initialize()
# size of the domain [-a,+a]x[-b,+b]
= 20
a = 10
b
# characteristic element sizes
= 0.6 # near the boundary
lc0 = 0.1*lc0 # near the wing
lc
# number of points of definition
= 50
nperf # attack angle
= (float(sys.argv[1]) if len(sys.argv) > 1 else 10) * math.pi/180
alpha
# Joukowsky parameters
= (float(sys.argv[2]) if len(sys.argv) > 2 else -0.11)
x0 = 0
y0 = math.sqrt((x0-1)**2+y0**2)
r
# append points
= []
pts = []
coords for i in range(nperf):
= i * 2*math.pi/nperf
theta = complex(x0 + r*math.cos(theta), y0 + r*math.sin(theta))
dz = (dz + 1/dz)*cmath.exp(complex(0, -alpha))
z # remember the coords of these three points
if i == 0 or i == 1 or i == nperf-1:
coords.append([z.real, z.imag])0, lc))
pts.append(gmsh.model.occ.addPoint(z.real, z.imag,
# create a closed loop of points
0])
pts.append(pts[
# add a spline
= []
spl
spl.append(gmsh.model.occ.addSpline(pts))= gmsh.model.occ.addCurveLoop(spl)
wing
# compute the location where to evaluate the Kutta condition
# this is ugly and not pythonic at all!
= [-(coords[1][0] - coords[0][0]),
vsup -(coords[1][1] - coords[0][1])]
= math.sqrt(vsup[0]**2 + vsup[1]**2)
norm 0] /= norm
vsup[1] /= norm
vsup[
= [-(coords[0][0] - coords[2][0]),
vinf -(coords[0][1] - coords[2][1])]
= math.sqrt(vinf[0]**2 + vinf[1]**2)
norm 0] /= norm
vinf[1] /= norm
vinf[
= vsup + vinf
vmed = math.sqrt(vmed[0]**2 + vmed[1]**2)
norm 0] /= norm
vmed[1] /= norm
vmed[
= 0.04
eps = [coords[0][0] + eps * vmed[0],
p 0][1] + eps * vmed[1]]
coords[# this is just to show the point in case one opens the .brep
= gmsh.model.occ.addPoint(p[0], p[1], 0, lc)
kutta
# f = open("p.fee", "w")
# f.write( "VECTOR p[2] DATA %g %g\n" % (p[0], p[1]))
# f.close()
# add the external boundary
= gmsh.model.occ.addPoint(-a, -b, 0, lc0)
p1 = gmsh.model.occ.addPoint(+a, -b, 0, lc0)
p2 = gmsh.model.occ.addPoint(+a, +b, 0, lc0)
p3 = gmsh.model.occ.addPoint(-a, +b, 0, lc0)
p4 = gmsh.model.occ.addLine(p1, p2)
bottom = gmsh.model.occ.addLine(p2, p3)
right = gmsh.model.occ.addLine(p3, p4)
top = gmsh.model.occ.addLine(p4, p1)
left = gmsh.model.occ.addCurveLoop([bottom, right, top, left])
boundary = gmsh.model.occ.addPlaneSurface([boundary, wing])
air
# create a circle to compute the circulation
= 3
r_circle = gmsh.model.occ.addCircle(0, 0, 0, r_circle)
circle
# we could embed the kutta point into the mesh but
# then the geometrical entities
= gmsh.model.occ.fragment([(2, air)], [(0, kutta), (1,circle)])
ov, ovv print(ov)
print(ovv)
gmsh.model.occ.synchronize()
# physical groups
# after the boolean fragment we lost the ids
# gmsh.model.addPhysicalGroup(1, [bottom], name="bottom")
# gmsh.model.addPhysicalGroup(1, [right], name="right")
# gmsh.model.addPhysicalGroup(1, [top], name="top")
# gmsh.model.addPhysicalGroup(1, [left], name="left")
# gmsh.model.addPhysicalGroup(1, [wing], name="wing")
# gmsh.model.addPhysicalGroup(2, [air], name="air")
0, [kutta], name="kutta")
gmsh.model.addPhysicalGroup(1, [6], name="circle")
gmsh.model.addPhysicalGroup(1, [7], name="bottom")
gmsh.model.addPhysicalGroup(1, [9], name="right")
gmsh.model.addPhysicalGroup(1, [10], name="top")
gmsh.model.addPhysicalGroup(1, [8], name="left")
gmsh.model.addPhysicalGroup(1, [11], name="wing")
gmsh.model.addPhysicalGroup(2, [1,2], name="air")
gmsh.model.addPhysicalGroup(
# dump the brep (or xao) just in case
"airfoil.brep")
gmsh.write("airfoil.xao")
gmsh.write(
# mesh
"Mesh.RecombineAll", 1)
gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", 2*math.pi * 0.25 * r_circle/lc)
gmsh.option.setNumber("Mesh.Algorithm", 6)
gmsh.option.setNumber(2, 1, 8);
gmsh.model.mesh.setAlgorithm(2, 2, 6);
gmsh.model.mesh.setAlgorithm("Mesh.Smoothing", 10)
gmsh.option.setNumber(
2)
gmsh.model.mesh.generate("Netgen")
gmsh.model.mesh.optimize("Laplace2D")
gmsh.model.mesh.optimize(2)
gmsh.model.mesh.setOrder("HighOrderFastCurving")
gmsh.model.mesh.optimize("HighOrder")
gmsh.model.mesh.optimize("airfoil.msh")
gmsh.write(
# gmsh.fltk.run()
gmsh.finalize()
PROBLEM laplace 2D
static_steps = 20
READ_MESH airfoil.msh
# boundary conditions constant -> streamline
BC bottom phi=0
BC top phi=1
# initialize c = streamline constant for the wing
DEFAULT_ARGUMENT_VALUE 1 0.5
IF in_static_first
= $1
c ENDIF
BC wing phi=c
SOLVE_PROBLEM
PHYSICAL_GROUP kutta DIM 0
= phi(kutta_cog[1], kutta_cog[2]) - c
e PRINT TEXT "\# "step_static %.6f c %+.1e e HEADER
# check for convergence
done_static = done_static | (abs(e) < 1e-8)
IF done_static
PHYSICAL_GROUP left DIM 1
(x,y) = +dphidy(x,y) * left_length
vx(x,y) = -dphidx(x,y) * left_length
vy
# write post-processing views
WRITE_MESH $0-converged.msh phi VECTOR vx vy 0
WRITE_MESH $0-converged.vtk phi VECTOR vx vy 0
# circulation on profile
INTEGRATE vx(x,y)*(+ny)+vy(x,y)*(-nx) OVER wing RESULT circ_profile
PRINTF "circ_profile = %g" -circ_profile
# function unit tangent
PHYSICAL_GROUP circle DIM 1
= circle_cog[1]
cx = circle_cog[2]
cy (x,y) = -(y-cy)/sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy))
tx(x,y) = +(x-cx)/sqrt((x-cx)*(x-cx)+(y-cy)*(y-cy))
ty
# circulation on the circumference with tangent
INTEGRATE vx(x,y)*tx(x,y)+vy(x,y)*ty(x,y) OVER circle RESULT circ_circle_t
PRINTF "circ_circle_t = %g" -circ_circle_t
# circulation on the outer box
INTEGRATE vx(x,y)*(+ny)+vy(x,y)*(-nx) OVER circle RESULT circ_circle_n
PRINTF "circ_circle_n = %g" -circ_circle_n
INTEGRATE -vx(x,y) OVER top GAUSS RESULT inttop
INTEGRATE +vx(x,y) OVER bottom GAUSS RESULT intbottom
INTEGRATE -vy(x,y) OVER left GAUSS RESULT intleft
INTEGRATE +vy(x,y) OVER right GAUSS RESULT intright
= inttop + intleft + intbottom + intright
circ_box PRINTF "circ_box = %g" -circ_box
# pressure forces
(x,y) = 1.0 - vx(x,y)^2 + vy(x,y)^2
pINTEGRATE p(x,y)*(nx) OVER circle RESULT pFx
PRINTF "pressure_drag = %g" pFx
INTEGRATE p(x,y)*(ny) OVER circle RESULT pFy
PRINTF "pressure_lift = %g" pFy
ELSE
# update c
IF in_static_first
= c
c_last = e
e_last = c_last - 0.1
c ELSE
= c_last - e_last * (c_last-c)/(e_last-e)
c_next = c
c_last = e
e_last = c_next
c ENDIF
ENDIF
$ python airfoil.py
[...]
Info : Writing 'airfoil.msh'...
Info : Done writing 'airfoil.msh'
$ feenox airfoil.fee
# # step_static c e
# 1 0.500000 -3.4e-03
# 2 0.400000 +3.7e-03
# 3 0.452067 +2.0e-09
circ_profile = 2.49918
circ_circle_t = 2.49975
circ_circle_n = 2.49907
circ_box = 2.50379
pressure_drag = -0.00168068
pressure_lift = 2.60614
$