I am trying to plot some phase planes with limit circles. I have the differential equations in polar form:
drdt = r(r^2 -1) dθdt = 1
is there a way to plot them from this using matplotlib? I was able to plot one that I have the original system form by using the original system like this:
xvalues, yvalues = np.meshgrid(np.arange(-2, 2, 0.1), np.arange(-2, 2, 0.1))
xdot = xvalues + yvalues - xvalues * (xvalues ** 2 + yvalues ** 2)
ydot = -xvalues + yvalues - yvalues * (xvalues ** 2 + yvalues ** 2)
plt.streamplot(xvalues, yvalues, xdot, ydot)
plt.grid();
plt.show()
this system is
dxdt = x + y - x(x^2 + y2)
dydt = -x + y - y(x^2 + y2)
and its polar form is:
drdt = r(1-r^2) dθdt = 1
But it would be tedious to covert them all to the original system, if its even possible. Appreciate any help I can get.
Update
So I came up with a method that comes close enough by solving the differential with scipy for a range of initial radii, and then plotting on X, Y with the conversion formula
times = np.linspace(0, 10)
r0s = np.arange(0, 3, 0.2)
for r0 in r0s:
z0 = [r0, r0]
solution = odeint(rmodel, z0, times)
x = solution[:,0] * np.cos(solution[:,1])
y = solution[:,0] * np.sin(solution[:,1])
plt.plot(x, y)
This works well enough for now but I would really like a better solution.
So I came up with a method that comes close enough by solving the differential with scipy for a range of initial radii, and then plotting on X, Y with the conversion formula
times = np.linspace(0, 10)
r0s = np.arange(0, 3, 0.2)
for r0 in r0s:
z0 = [r0, r0]
solution = odeint(rmodel, z0, times)
x = solution[:,0] * np.cos(solution[:,1])
y = solution[:,0] * np.sin(solution[:,1])
plt.plot(x, y)
This works well enough for my purposes.
Related
I need help to create a torus out of a circle by revolving it about x=2r, r is the radius of the circle.
I am open to either JULIA code or Python code. Whichever that can solve my problem the most efficient.
I have Julia code to plot circle and the x=2r as the axis of revolution.
using Plots, LaTeXStrings, Plots.PlotMeasures
gr()
θ = 0:0.1:2.1π
x = 0 .+ 2cos.(θ)
y = 0 .+ 2sin.(θ)
plot(x, y, label=L"x^{2} + y^{2} = a^{2}",
framestyle=:zerolines, legend=:outertop)
plot!([4], seriestype="vline", color=:green, label="x=2a")
I want to create a torus out of it, but unable, meanwhile I have solid of revolution Python code like this:
# Calculate the surface area of y = sqrt(r^2 - x^2)
# revolved about the x-axis
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
x = sy.Symbol("x", nonnegative=True)
r = sy.Symbol("r", nonnegative=True)
def f(x):
return sy.sqrt(r**2 - x**2)
def fd(x):
return sy.simplify(sy.diff(f(x), x))
def f2(x):
return sy.sqrt((1 + (fd(x)**2)))
def vx(x):
return 2*sy.pi*(f(x)*sy.sqrt(1 + (fd(x) ** 2)))
vxi = sy.Integral(vx(x), (x, -r, r))
vxf = vxi.simplify().doit()
vxn = vxf.evalf()
n = 100
fig = plt.figure(figsize=(14, 7))
ax1 = fig.add_subplot(221)
ax2 = fig.add_subplot(222, projection='3d')
ax3 = fig.add_subplot(223)
ax4 = fig.add_subplot(224, projection='3d')
# 1 is the starting point. The first 3 is the end point.
# The last 200 is the number of discretization points.
# help(np.linspace) to read its documentation.
x = np.linspace(1, 3, 200)
# Plot the circle
y = np.sqrt(2 ** 2 - x ** 2)
t = np.linspace(0, np.pi * 2, n)
xn = np.outer(x, np.cos(t))
yn = np.outer(x, np.sin(t))
zn = np.zeros_like(xn)
for i in range(len(x)):
zn[i:i + 1, :] = np.full_like(zn[0, :], y[i])
ax1.plot(x, y)
ax1.set_title("$f(x)$")
ax2.plot_surface(xn, yn, zn)
ax2.set_title("$f(x)$: Revolution around $y$")
# find the inverse of the function
y_inverse = x
x_inverse = np.power(2 ** 2 - y_inverse ** 2, 1 / 2)
xn_inverse = np.outer(x_inverse, np.cos(t))
yn_inverse = np.outer(x_inverse, np.sin(t))
zn_inverse = np.zeros_like(xn_inverse)
for i in range(len(x_inverse)):
zn_inverse[i:i + 1, :] = np.full_like(zn_inverse[0, :], y_inverse[i])
ax3.plot(x_inverse, y_inverse)
ax3.set_title("Inverse of $f(x)$")
ax4.plot_surface(xn_inverse, yn_inverse, zn_inverse)
ax4.set_title("$f(x)$: Revolution around $x$ \n Surface Area = {}".format(vxn))
plt.tight_layout()
plt.show()
Here is a way that actually allows rotating any figure in the XY plane around the Y axis.
"""
Rotation of a figure in the XY plane about the Y axis:
ϕ = angle of rotation
z' = z * cos(ϕ) - x * sin(ϕ)
x' = z * sin(ϕ) + x * cos(ϕ)
y' = y
"""
using Plots
# OP definition of the circle, but we put center at x, y of 4, 0
# for the torus, otherwise we get a bit of a sphere
θ = 0:0.1:2.1π
x = 4 .+ 2cos.(θ) # center at (s, 0, 0)
y = 0 .+ 2sin.(θ)
# add the original z values as 0
z = zeros(length(x))
plot(x, y, z, color=:red)
# add the rotation axis
ϕ = 0:0.1:π/2 # for full torus use 2π at stop of range
xprime, yprime, zprime = Float64[], Float64[], Float64[]
for a in ϕ, i in eachindex(θ)
push!(zprime, z[i] + z[i] * cos(a) - x[i] * sin(a))
push!(xprime, z[i] * sin(a) + x[i] * cos(a))
push!(yprime, y[i])
end
plot!(xprime, yprime, zprime, alpha=0.3, color=:green)
Here is a way using the Meshes package for the construction of the mesh and the MeshViz package for the visualization. You'll just have to translate to fulfill your desiderata.
using Meshes
using MeshViz
using LinearAlgebra
using GLMakie
# revolution of the polygon defined by (x,y) around the z-axis
# x and y have the same length
function revolution(x, y, n)
u_ = LinRange(0, 2*pi, n+1)[1:n]
j_ = 1:(length(x) - 1) # subtract 1 because of periodicity
function f(u, j)
return [x[j] * sin(u), x[j] * cos(u), y[j]]
end
points = [f(u, j) for u in u_ for j in j_]
topo = GridTopology((length(j_), n), (true, true))
return SimpleMesh(Meshes.Point.(points), topo)
end
# define the section to be rotated: a circle
R = 3 # major radius
r = 1 # minor radius
ntheta = 100
theta_ = LinRange(0, 2*pi, ntheta)
x = [R + r*cos(theta) for theta in theta_]
y = [r*sin(theta) for theta in theta_]
# make mesh
mesh = revolution(x, y, 100)
# visualize mesh
viz(mesh)
EDIT: animation
using Meshes
using MeshViz
using LinearAlgebra
using GLMakie
using Makie
using Printf
function revolutionTorus(R, r, alpha; n1=30, n2=90)
theta_ = LinRange(0, 2, n1+1)[1:n1]
x = [R + r*cospi(theta) for theta in theta_]
y = [r*sinpi(theta) for theta in theta_]
full = alpha == 2
u_ = LinRange(0, alpha, n2 + full)[1:n2]
function f(u, j)
return [x[j] * sinpi(u), x[j] * cospi(u), y[j]]
end
points = [f(u, j) for u in u_ for j in 1:n1]
topo = GridTopology((n1, n2 - !full), (true, full))
return SimpleMesh(Meshes.Point.(points), topo)
end
# generates `nframes` meshes for alpha = 0 -> 2 (alpha is a multiple of pi)
R = 3
r = 1
nframes = 10
alpha_ = LinRange(0, 2, nframes+1)[2:(nframes+1)]
meshes = [revolutionTorus(R, r, alpha) for alpha in alpha_]
# draw and save the frames in a loop
for i in 1:nframes
# make a bounding box in order that all frames have the same aspect
fig, ax, plt =
viz(Meshes.Box(Meshes.Point(-4.5, -4.5, -2.5), Meshes.Point(4.5, 4.5, 2.5)); alpha = 0)
ax.show_axis = false
viz!(meshes[i])
scale!(ax.scene, 1.8, 1.8, 1.8)
png = #sprintf "revolutionTorus%02d.png" i
Makie.save(png, fig)
end
# make GIF with ImageMagick
comm = #cmd "convert -delay 1x2 'revolutionTorus*.png' revolutionTorus.gif"
run(comm)
I'm interested in plotting a real-valued function f(x,y,z)=a, where (x,y,z) is a 3D point on the sphere and a is a real number. I calculate the Cartesian coordinates of the points of the sphere as follows, but I have no clue on how to visualize the value of f on each of those points.
import plotly.graph_objects as go
import numpy as np
fig = go.Figure(layout=go.Layout(title=go.layout.Title(text=title), hovermode=False))
# Create mesh grid for spherical coordinates
phi, theta = np.mgrid[0.0:np.pi:100j, 0.0:2.0 * np.pi:100j]
# Get Cartesian mesh grid
x = np.sin(phi) * np.cos(theta)
y = np.sin(phi) * np.sin(theta)
z = np.cos(phi)
# Plot sphere surface
self.fig.add_surface(x=x, y=y, z=z, opacity=0.35)
fig.show()
I would imagine/expect/like a visualization like this
Additionally, I also have the gradient of f calculated in closed-form (i.e., for each (x,y,z) I calculate the 3D-dimensional gradient of f). Is there a way of plotting this vector field, similarly to what is shown in the figure above?
Here's an answer that's far from perfect, but hopefully that's enough for you to build on.
For the sphere itself, I don't know of any "shortcut" to do something like that in plotly, so my approach is simply to manually create a sphere mesh. Generating the vertices is simple, for example like you did - the slightly more tricky part is figuring out the vertex indices for the triangles (which depends on the vertex generation scheme). There are various algorithms to do that smoothly (i.e. generating a sphere with no "tip"), I hacked something crude just for the demonstration. Then we can use the Mesh3d object to display the sphere along with the intensities and your choice of colormap:
N = 100 # Sphere resolution (both rings and segments, can be separated to different constants)
theta, z = np.meshgrid(np.linspace(-np.pi, np.pi, N), np.linspace(-1, 1, N))
r = np.sqrt(1 - z ** 2)
x = r * np.cos(theta)
y = r * np.sin(theta)
x = x.ravel()
y = y.ravel()
z = z.ravel()
# Triangle indices
indices = np.arange(N * (N - 1) - 1)
i1 = np.concatenate([indices, (indices // N + 1) * N + (indices + 1) % N])
i2 = np.concatenate([indices + N, indices // N * N + (indices + 1) % N])
i3 = np.concatenate([(indices // N + 1) * N + (indices + 1) % N, indices])
# Point intensity function
def f(x, y, z):
return (np.cos(x * 2) + np.sin(y ** 2) + np.sin(z) + 3) / 6
fig = go.Figure(data=[
go.Mesh3d(
x=x,
y=y,
z=z,
colorbar_title='f(x, y, z)',
colorscale=[[0, 'gold'],
[0.5, 'mediumturquoise'],
[1, 'magenta']],
intensity = f(x, y, z),
i = i1,
j = i2,
k = i3,
name='y',
showscale=True
)
])
fig.show()
This yields the following interactive plot:
To add the vector field you can use the Cone plot; this requires some tinkering because when I simply draw the cones at the same x, y, z position as the sphere, some of the cones are partially or fully occluded by the sphere. So I generate another sphere, with a slightly larger radius, and place the cones there. I also played with some lighting parameters to make it black like in your example. The full code looks like this:
N = 100 # Sphere resolution (both rings and segments, can be separated to different constants)
theta, z = np.meshgrid(np.linspace(-np.pi, np.pi, N), np.linspace(-1, 1, N))
r = np.sqrt(1 - z ** 2)
x = r * np.cos(theta)
y = r * np.sin(theta)
x = x.ravel()
y = y.ravel()
z = z.ravel()
# Triangle indices
indices = np.arange(N * (N - 1) - 1)
i1 = np.concatenate([indices, (indices // N + 1) * N + (indices + 1) % N])
i2 = np.concatenate([indices + N, indices // N * N + (indices + 1) % N])
i3 = np.concatenate([(indices // N + 1) * N + (indices + 1) % N, indices])
# Point intensity function
def f(x, y, z):
return (np.cos(x * 2) + np.sin(y ** 2) + np.sin(z) + 3) / 6
# Vector field function
def grad_f(x, y, z):
return np.stack([np.cos(3 * y + 5 * x),
np.sin(z * y),
np.cos(4 * x - 3 * y + z * 7)], axis=1)
# Second sphere for placing cones
N2 = 50 # Smaller resolution (again rings and segments combined)
R2 = 1.05 # Slightly larger radius
theta2, z2 = np.meshgrid(np.linspace(-np.pi, np.pi, N2), np.linspace(-R2, R2, N2))
r2 = np.sqrt(R2 ** 2 - z2 ** 2)
x2 = r2 * np.cos(theta2)
y2 = r2 * np.sin(theta2)
x2 = x2.ravel()
y2 = y2.ravel()
z2 = z2.ravel()
uvw = grad_f(x2, y2, z2)
fig = go.Figure(data=[
go.Mesh3d(
x=x,
y=y,
z=z,
colorbar_title='f(x, y, z)',
colorscale=[[0, 'gold'],
[0.5, 'mediumturquoise'],
[1, 'magenta']],
intensity = f(x, y, z),
i = i1,
j = i2,
k = i3,
name='y',
showscale=True
),
go.Cone(
x=x2, y=y2, z=z2, u=uvw[:, 0], v=uvw[:, 1], w=uvw[:, 2], sizemode='absolute', sizeref=2, anchor='tail',
lighting_ambient=0, lighting_diffuse=0, opacity=.2
)
])
fig.show()
And yields this plot:
Hope this helps. There are a lot of tweaks to the display, and certainly better ways to construct a sphere mesh (e.g. see this article), so there should be a lot of freedom there (albeit at the cost of some work).
Good luck!
The requirement is to define this function f(x) = x^3 - 15x^2 - 18x + 1 and then plot it in such a way that the plotting region shows all of the turning points.
I think that my calculations below are wrong because if you look at the graph there are at least 2 turning points.
By that I mean that the function is first increasing and then is decreasing, but in my calculation I found a single solution for x, where x = 0.5.
How can I solve this problem?
%matplotlib notebook
import matplotlib.pyplot as plt
plt.rcParams.update({'font.size': 14})
#1. define the function
def f(x):
return x**3 - 15*x**2 - 18*x + 1
#find the turning points of a polynomial of 3rd degree
#2. find the derivative of x**3 - 15*x**2 - 18*x + 1
# the derivative is -15(2x + 1)
#3. by the null factor law, get the value of x:
# 2x + 1 = 0
# 2x = 1
# x = 0.5
#4. check what is f(x) for x = 0.5
0.5**3 - 15*0.5**2 - 18*0.5 + 1 = -39/4 = -9.75
#5. Therefore, we can conclude that there is a single turning point at A(0.5, -9.75)
x = np.linspace(-5,5,100)
plt.plot(x, f(x))
plt.plot(0.5,f(0.5),'.r', ms=20,label='x_1') #plot A(0.5, -9.75)
plt.xlabel('x')
plt.ylabel('f(x)')
plt.grid()
plt.show()
EDIT_1
Based on Picarus suggestion, I have recalculated the derivative, but the solution of my equation don't look as correct on the graph.
def f(x):
return x**3 - 15*x**2 - 18*x + 1
#find the turning points of a polynomial of 3rd degree
#2. find the derivative of x**3 - 15*x**2 - 18*x + 1
# Workout the derivative
# (x**3)' = 3*x**2
# (-15*x**2)' = -30*x
# (-18x)' = -18
# (1)' = 0
# Therefore our derviative equation is 3*x**2 -30*x -18
#Bring it to a more concise form
# 3(x**2 - 10*x -6) = 0
#Find the x solution for x**2 - 10*x -6
#Delta = b**2 - 4*a*c
#x1 = (10 + sqr(76))/2 = 9.358
#x2 = (10 - sqr(76))/2 = -3.84
x1 = (10 + np.sqrt(76))/2
x2 = (10 - np.sqrt(76))/2
print(x1)
print(x2)
#4. check what is f(x) for: x1 = 9.358, x2 = 0.641
x = np.linspace(-5,5,100)
plt.plot(x, f(x))
plt.plot(x1,f(x1),'.r', ms=20,label='x_1')
plt.plot(x2,f(x2),'.b', ms=20,label='x_2')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.grid()
plt.show()
import numpy as np
from matplotlib import pyplot as plt
# generate some toy data
n = 600
t = np.linspace(0, 600, n)
y = (300 * np.exp(-0.1 * t) + 1) + 20 * (np.random.random(n))
# get the gradient
dy = np.gradient(y)
# search gradient for first occurrence of thresh value:
thresh = 0.01
idx_thresh = np.argmax(dy > thresh)
# y[idx_thresh] would be the "turning point"
# visualization
plt.plot(t, y, 'b', label='y')
plt.plot(t, dy, 'g', label='dy')
plt.plot(t[idx_thresh:], y[idx_thresh:], 'r', label=f'y[dy > {thresh}]')
plt.legend()
I am trying to find areas or points where there is convergence in a vector field.
I have used the code below to generate the following plot:
import matplotlib.pyplot as plt
import numpy as np
def generate_fake_data():
return -(np.sin(X) * np.cos(Y) + np.cos(X)), -(-np.cos(X) * np.sin(Y) + np.sin(Y))
x = np.arange(0, 2 * np.pi + 2 * np.pi / 20, 2 * np.pi / 20)
y = np.arange(0, 2 * np.pi + 2 * np.pi / 20, 2 * np.pi / 20)
X, Y = np.meshgrid(x, y)
u, v = generate_fake_data()
fig, ax = plt.subplots(figsize=(7, 7))
# quiveropts = dict(headlength=0, headaxislength=0, pivot='middle', units='xy')
# ax.quiver(X, Y, u, v, **quiveropts)
ax.quiver(X, Y, u, v)
ax.xaxis.set_ticks([])
ax.yaxis.set_ticks([])
ax.axis([0, 2 * np.pi, 0, 2 * np.pi])
ax.set_aspect('equal')
ax.axis("off")
plt.gca().set_axis_off()
plt.subplots_adjust(top=1, bottom=0, right=1, left=0,
hspace=0, wspace=0)
plt.margins(0, 0)
plt.gca().xaxis.set_major_locator(plt.NullLocator())
plt.gca().yaxis.set_major_locator(plt.NullLocator())
plt.savefig("mock_data.png", bbox_inches='tight', pad_inches=0)
Ideally, what I am after is to find where there is convergence in this vector field in the top right and bottom right of the image.
I hope that the curl values could be used to achieve this, but any method is good to use.
Also, this is just a proof of concept, generate_fake_data will be replaced with a function reading in data from elsewhere that's changeable.
For converging points the divergence of the vector field is < 0.
from functools import reduce
conv = reduce(np.add,np.gradient(u)) + reduce(np.add,np.gradient(v))
(see Compute divergence of vector field using python)
We only need negativ divergence:
conv[conv>=0] = np.nan
plt.imshow(conv): the darker the more converging is the field:
Finding the absolute minimum (in the top right) is easy:
absmin = np.unravel_index(np.nanargmin(conv), conv.shape)
print(absmin, conv[absmin])
#(0, 15) -0.6669774724547413
Finding relative minima is harder, it should be possible with argrelmin, but frankly speaking I couldn't get it work correctly to return the second local minimum at (19,15).
Using this answer I get
lm = detect_local_minima(conv)
list(zip(lm))
[(0, 15), (1, 0), (19, 15), (20, 0)]
which also includes both minima in the left corners (which is mathematically correct, but not desired in our case, so maybe we could just exclude corners).
I need to make an offset parallel enclosure of an airfoil profile curve, but I cant figure out how to make all the points be equidistant to the points on the primary profile curve at desired distance.
this is my example airfoil profile
this is my best and not good approach
EDIT #Patrick Solution for distance 0.2
You'll have to special-case slopes of infinity/zero, but the basic approach is to use interpolation to calculate the slope at a point, and then find the perpendicular slope, and then calculate the point at that distance.
I have modified the example from here to add a second graph. It works with the data file you provided, but you might need to change the sign calculation for a different envelope.
EDIT As per your comments about wanting the envelope to be continuous, I have added a cheesy semicircle at the end that gets really close to doing this for you. Essentially, when creating the envelope, the rounder and more convex you can make it, the better it will work. Also, you need to overlap the beginning and the end, or you'll have a gap.
Also, it could almost certainly be made more efficient -- I am not a numpy expert by any means, so this is just pure Python.
def offset(coordinates, distance):
coordinates = iter(coordinates)
x1, y1 = coordinates.next()
z = distance
points = []
for x2, y2 in coordinates:
# tangential slope approximation
try:
slope = (y2 - y1) / (x2 - x1)
# perpendicular slope
pslope = -1/slope # (might be 1/slope depending on direction of travel)
except ZeroDivisionError:
continue
mid_x = (x1 + x2) / 2
mid_y = (y1 + y2) / 2
sign = ((pslope > 0) == (x1 > x2)) * 2 - 1
# if z is the distance to your parallel curve,
# then your delta-x and delta-y calculations are:
# z**2 = x**2 + y**2
# y = pslope * x
# z**2 = x**2 + (pslope * x)**2
# z**2 = x**2 + pslope**2 * x**2
# z**2 = (1 + pslope**2) * x**2
# z**2 / (1 + pslope**2) = x**2
# z / (1 + pslope**2)**0.5 = x
delta_x = sign * z / ((1 + pslope**2)**0.5)
delta_y = pslope * delta_x
points.append((mid_x + delta_x, mid_y + delta_y))
x1, y1 = x2, y2
return points
def add_semicircle(x_origin, y_origin, radius, num_x = 50):
points = []
for index in range(num_x):
x = radius * index / num_x
y = (radius ** 2 - x ** 2) ** 0.5
points.append((x, -y))
points += [(x, -y) for x, y in reversed(points)]
return [(x + x_origin, y + y_origin) for x, y in points]
def round_data(data):
# Add infinitesimal rounding of the envelope
assert data[-1] == data[0]
x0, y0 = data[0]
x1, y1 = data[1]
xe, ye = data[-2]
x = x0 - (x0 - x1) * .01
y = y0 - (y0 - y1) * .01
yn = (x - xe) / (x0 - xe) * (y0 - ye) + ye
data[0] = x, y
data[-1] = x, yn
data.extend(add_semicircle(x, (y + yn) / 2, abs((y - yn) / 2)))
del data[-18:]
from pylab import *
with open('ah79100c.dat', 'rb') as f:
f.next()
data = [[float(x) for x in line.split()] for line in f if line.strip()]
t = [x[0] for x in data]
s = [x[1] for x in data]
round_data(data)
parallel = offset(data, 0.1)
t2 = [x[0] for x in parallel]
s2 = [x[1] for x in parallel]
plot(t, s, 'g', t2, s2, 'b', lw=1)
title('Wing with envelope')
grid(True)
axes().set_aspect('equal', 'datalim')
savefig("test.png")
show()
If you are willing (and able) to install a third-party tool, I'd highly recommend the Shapely module. Here's a small sample that offsets both inward and outward:
from StringIO import StringIO
import matplotlib.pyplot as plt
import numpy as np
import requests
import shapely.geometry as shp
# Read the points
AFURL = 'http://m-selig.ae.illinois.edu/ads/coord_seligFmt/ah79100c.dat'
afpts = np.loadtxt(StringIO(requests.get(AFURL).content), skiprows=1)
# Create a Polygon from the nx2 array in `afpts`
afpoly = shp.Polygon(afpts)
# Create offset airfoils, both inward and outward
poffafpoly = afpoly.buffer(0.03) # Outward offset
noffafpoly = afpoly.buffer(-0.03) # Inward offset
# Turn polygon points into numpy arrays for plotting
afpolypts = np.array(afpoly.exterior)
poffafpolypts = np.array(poffafpoly.exterior)
noffafpolypts = np.array(noffafpoly.exterior)
# Plot points
plt.plot(*afpolypts.T, color='black')
plt.plot(*poffafpolypts.T, color='red')
plt.plot(*noffafpolypts.T, color='green')
plt.axis('equal')
plt.show()
And here's the output; notice how the 'bowties' (self-intersections) on the inward offset are automatically removed: