Mapping a disc to a square using FG method - python

Context:
I am working with meshes in computational fluid dynamics. I would like to generate a structured mesh around a circle. My plan is to generate a polar mesh (Left mesh in Figure below) then use FG formulas to get the final mesh (Right mesh on Figure below).
I am using FG method from Page 4 of this article to map a disc to square . Unfortunately the article doesn't mention how to handle singularities in the formulas. Here are the expressions:
x = sgn(uv)/(v*sqrt 2)*sqrt(u**2+v**2-sqrt((u**2+v**2)(u**2+v**2-4u**2v**2)))
y = sgn(uv)/(u*sqrt 2)*sqrt(u**2+v**2-sqrt((u**2+v**2)(u**2+v**2-4u**2v**2)))
Before programming this, I am struggling with some problems with these formaulas
Questions
Why do these formulae map the following points: (1,0), (0,1), (-1,0), (-1,-1), and (0,0) to the point (0,0) ?
How I am supposed to get the intermediate shape between a circle and a square as shown in the figure below.
Is it possible to provide an algorithm to get the right mesh from the left one?
Here is my attempt:
"""Map a circular computational domain with structured mesh around a circle (circular cylinder in 3D) to
Rectangular domain"""
import numpy as np
from numpy import sqrt, sign, pi, cos, sin
import matplotlib.pyplot as plt
def FGsquircle(u, v):
SMALL = 1e-15
t0 = u**2+v**2
t1 = (u**2+v**2)*(u**2+v**2-4*u**2*v**2)
t2 = u**2+v**2
t3 = (u**2+v**2)*(u**2+v**2-4*u**2*v**2)
x = sign(u*v)/(v*sqrt(2.0)+SMALL)*sqrt(t0-sqrt(t1))
y = sign(u*v)/(u*sqrt(2.0)+SMALL)*sqrt(t2-sqrt(t3))
return x, y
R0 = 1.0 # radius of the disc
RMAX = 5.0 # the radius of the outer circle in the domain
NT = 360 # num of division in the theta direction
NR = 10 # num of radial divisions
r = [R0+(RMAX-R0)/NR*k for k in range(NR)] # the radii of circles
theta = np.array([2*pi/NT*k for k in range(NT+1)])
u = [r[k]*cos(theta) for k in range(NR)]
v = [r[k]*sin(theta) for k in range(NR)]
u = np.array(u)
v = np.array(v)
x, y = FGsquircle(u, v)
I got the following error:
utils.py:21: RuntimeWarning: invalid value encountered in sqrt
x = sign(u*v)/(v*sqrt(2.0)+SMALL)*sqrt(t0-sqrt(t1))
utils.py:22: RuntimeWarning: invalid value encountered in sqrt
y = sign(u*v)/(u*sqrt(2.0)+SMALL)*sqrt(t2-sqrt(t3))
I appreciate any help.

Why do these formulae map the following points: (1,0), (0,1), (-1,0), (-1,-1), and (0,0) to the point (0,0) ?
In both expressions, you divide by u or v, so when one of the two is 0, the expression becomes undefined (not zero).
How I am supposed to get the intermediate shape between a circle and a square as shown in the figure below.
Just map a circle of radius smaller than 1.
Is it possible to provide an algorithm to get the right mesh from the left one?
You'll just need to transform the mesh points. The cells can remain the same.
Example code:
from numpy import sqrt, sign
import numpy
import matplotlib.pyplot as plt
def f(x):
u, v = x
alpha = sqrt(
u ** 2
+ v ** 2
- sqrt((u ** 2 + v ** 2) * (u ** 2 + v ** 2 - 4 * u ** 2 * v ** 2))
)
return numpy.array(
[sign(u * v) / (v * sqrt(2)) * alpha, sign(u * v) / (u * sqrt(2)) * alpha]
)
for r in numpy.linspace(0.1, 1.0, 10):
theta = numpy.linspace(0.0, 2 * numpy.pi, 1000, endpoint=True)
uv = r * numpy.array([numpy.cos(theta), numpy.sin(theta)])
xy = f(uv)
plt.plot(xy[0], xy[1], "-")
plt.gca().set_aspect("equal")
plt.show()

Related

Change in coordinate density for np.meshgrid() in matplotlib

I am plotting a vector field using the numpy function quiver() and it works. But I would like to emphasize the cowlick in the following plot:
I am not sure how to go about it, but increasing the density of arrows in the center could possibly do the trick. To do so, I would like to resort to some option within np.meshgrid() that would allow me to get more tightly packed x,y coordinate points in the center. A linear, quadratic or other specification does not seem to be built in. I am not sure if sparse can be modified to this end.
The code:
lim = 10
int = 0.22 *lim
x,y = np.meshgrid(np.arange(-lim, lim, int), np.arange(-lim, lim, int))
u = 3 * np.cos(np.arctan2(y,x)) - np.sqrt(x**2+y**2) * np.sin(np.arctan2(y,x))
v = 3 * np.sin(np.arctan2(y,x)) + np.sqrt(x**2+y**2) * np.cos(np.arctan2(y,x))
color = x**2 + y**2
plt.rcParams["image.cmap"] = "Greys_r"
mult = 1
plt.figure(figsize=(mult*lim, mult*lim))
plt.quiver(x,y,u,v,color, linewidths=.006, lw=.1)
plt.show()
Closing the loop on this, thanks to the accepted answer I was able to finally strike a balance between the density of the mesh as I learned from to do from #flwr and keeping the "cowlick" structure of the vector field conspicuous (avoiding the radial structure around the origin as much as possible):
You can construct the points whereever you want to calculate your field on and quivers will be happy about it. The code below uses polar coordinates and stretches the radial coordinate non-linearly.
import numpy as np
import matplotlib.pyplot as plt
lim = 10
N = 10
theta = np.linspace(0.1, 2*np.pi, N*2)
stretcher_factor = 2
r = np.linspace(0.3, lim**(1/stretcher_factor), N)**stretcher_factor
R, THETA = np.meshgrid(r, theta)
x = R * np.cos(THETA)
y = R * np.sin(THETA)
# x,y = np.meshgrid(x, y)
r = x**2 + y**2
u = 3 * np.cos(THETA) - np.sqrt(r) * np.sin(THETA)
v = 3 * np.sin(THETA) + np.sqrt(r) * np.cos(THETA)
plt.rcParams["image.cmap"] = "Greys_r"
mult = 1
plt.figure(figsize=(mult*lim, mult*lim))
plt.quiver(x,y,u,v,r, linewidths=.006, lw=.1)
Edit: Bug taking meshgrid twice
np.meshgrid just makes a grid of the vectors you provide.
What you could do is contract this regular grid in the center to have more points in the center (best visible with more points), e.g. like so:
# contract in the center
a = 0.5 # how far to contract
b = 0.8 # how strongly to contract
c = 1 - b*np.exp(-((x/lim)**2 + (y/lim)**2)/a**2)
x, y = c*x, c*y
plt.plot(x,y,'.k')
plt.show()
Alternatively you can x,y cooridnates that are not dependent on a grid at all:
x = np.random.randn(500)
y = np.random.randn(500)
plt.plot(x,y,'.k')
plt.show()
But I think you'd prefer a slightly more regular patterns you could look into poisson disk sampling with adaptive distances or something like that, but the key point here is that for using quiver, you can use ANY set of coordinates, they do not have to be in a regular grid.

How to use mgrid to interpolate between a rectangle and a circle

I am trying to create a 3D surface that has a 1/4 rectangle for the exterior and 1/4 circle for the interior. I had help before to create the 3D surface with an ellipse as an exterior but I cannot do this for a rectangle for some reason. I have done the math by hand which makes sense, but my code does not. I would greatly appreciate any help with this.
import numpy as np
import pyvista as pv
# parameters for the waveguide
# diameter of the inner circle
waveguide_throat = 30
# axes of the outer ellipse
ellipse_x = 250
ellipse_y = 170
# shape parameters for the z profile
depth_factor = 4
angle_factor = 40
# number of grid points in radial and angular direction
array_length = 100
phase_plug = 0
phase_plug_dia = 20
plug_offset = 5
dome_dia = 28
# theta is angle where x and y intersect
theta = np.arctan(ellipse_x / ellipse_y)
# chi is for x direction and lhi is for y direction
chi = np.linspace(0, theta, 100)
lhi = np.linspace(theta, np.pi/2, 100)
# mgrid to create structured grid
r, phi = np.mgrid[0:1:array_length*1j, 0:np.pi/2:array_length*1j]
# Rectangle exterior, circle interior
x = (ellipse_y * np.tan(chi)) * r + ((waveguide_throat / 2 * (1 - r)) * np.cos(phi))
y = (ellipse_x / np.tan(lhi)) * r + ((waveguide_throat / 2 * (1 - r)) * np.sin(phi))
# compute z profile
angle_factor = angle_factor / 10000
z = (ellipse_x / 2 * r / angle_factor) ** (1 / depth_factor)
plotter = pv.Plotter()
waveguide_mesh = pv.StructuredGrid(x, y, z)
plotter.add_mesh(waveguide_mesh)
plotter.show()
The linear interpolation you're trying to use is a general tool that should work (with one small caveat). So the issue is first with your rectangular edge.
Here's a sanity check which plots your interior and exterior lines:
# debugging: plot interior and exterior
exterior_points = np.array([
ellipse_y * np.tan(chi),
ellipse_x / np.tan(lhi),
np.zeros_like(chi)
]).T
phi_aux = np.linspace(0, np.pi/2, array_length)
interior_points = np.array([
waveguide_throat / 2 * np.cos(phi_aux),
waveguide_throat / 2 * np.sin(phi_aux),
np.zeros_like(phi_aux)
]).T
plotter = pv.Plotter()
plotter.add_mesh(pv.wrap(exterior_points))
plotter.add_mesh(pv.wrap(interior_points))
plotter.show()
The bottom left is your interior circle, looks good. The top right is what's supposed to be a rectangle, but isn't.
To see why your original surface looks the way it does, we have to note one more thing (this is the small caveat I mentioned): the orientation of your curves is also the opposite. This implies that you interpolate the "top" (in the screenshot) point of your interior curve with the "bottom" point of the exterior curve. This explains the weird fan shape.
So you need to fix the exterior curve, and make sure the orientation of the two edges is the same. Note that you can just create the two 1d arrays for the two edges, and then interpolate them. You don't have to come up with a symbolic formula that you plug into the interpolation step. If you have 1d arrays of the same shape x_interior, y_interior, x_exterior, y_exterior then you can then do x_exterior * r + x_interior * (1 - r) and the same for y. This means removing the mgrid call, only using an array r of shape (n, 1), and making use of array broadcasting to do the interpolation. This means doing r = np.linspace(0, 1, array_length)[:, None].
So the question is how to define your rectangle. You need to have the same number of points on the rectangular curve than what you have on the circle (I would strongly recommend using the array_length parameter everywhere to ensure this!). Since you want to span the whole rectangle, I believe you have to choose an array index (i.e. a certain angle in the circular arc) which will map to the corner of the rectangle. Then it's a simple matter of varying only y for the points until that index, and x for the rest (or vice versa).
Here's what I mean: you know that the rectangle's corner is at angle theta in your code (although I think you have x and y mixed up if we assume the conventional relationship between "x", "y" and the tangent of the angle). Since theta goes from 0 to pi/2, and your phi values also go from 0 to pi/2, you should choose index (array_length * (2*theta/np.pi)).round().astype(int) - 1 (or something similar) that will map to the rectangle's corner. If you have a square, this gives you theta = pi/4, and consequently (array_length / 2).round().astype(int) - 1. For array_length = 3 this is index (2 - 1) == 1, which is the middle index for 3-length arrays. (The more points you have along the edge, the less it will matter if you commit an off-by-one error here.)
The only remaining complication then is that we have to explicitly broadcast the 1d z array to the common shape. And we can use the same math you used to get a rectangular edge that is equidistant in angles.
Your code fixed with this suggestion (note that I've added 1 to the corner index because I'm using it as a right-exclusive range index):
import numpy as np
import pyvista as pv
# parameters for the waveguide
# diameter of the inner circle
waveguide_throat = 30
# axes of the outer ellipse
ellipse_x = 250
ellipse_y = 170
# shape parameters for the z profile
depth_factor = 4
angle_factor = 40
# number of grid points in radial and angular direction
array_length = 100
# quarter circle interior line
phi = np.linspace(0, np.pi/2, array_length)
x_interior = waveguide_throat / 2 * np.cos(phi)
y_interior = waveguide_throat / 2 * np.sin(phi)
# theta is angle where x and y intersect
theta = np.arctan2(ellipse_y, ellipse_x)
# find array index which maps to the corner of the rectangle
corner_index = (array_length * (2*theta/np.pi)).round().astype(int)
# construct rectangular coordinates manually
x_exterior = np.zeros_like(x_interior)
y_exterior = x_exterior.copy()
phi_aux = np.linspace(0, theta, corner_index)
x_exterior[:corner_index] = ellipse_x
y_exterior[:corner_index] = ellipse_x * np.tan(phi_aux)
phi_aux = np.linspace(np.pi/2, theta, array_length - corner_index, endpoint=False)[::-1] # mind the reverse!
x_exterior[corner_index:] = ellipse_y / np.tan(phi_aux)
y_exterior[corner_index:] = ellipse_y
# interpolate between two curves
r = np.linspace(0, 1, array_length)[:, None] # shape (array_length, 1) for broadcasting
x = x_exterior * r + x_interior * (1 - r)
y = y_exterior * r + y_interior * (1 - r)
# debugging: plot interior and exterior
exterior_points = np.array([
x_exterior,
y_exterior,
np.zeros_like(x_exterior),
]).T
interior_points = np.array([
x_interior,
y_interior,
np.zeros_like(x_interior),
]).T
plotter = pv.Plotter()
plotter.add_mesh(pv.wrap(exterior_points))
plotter.add_mesh(pv.wrap(interior_points))
plotter.show()
# compute z profile
angle_factor = angle_factor / 10000
z = (ellipse_x / 2 * r / angle_factor) ** (1 / depth_factor)
# explicitly broadcast to the shape of x and y
z = np.broadcast_to(z, x.shape)
plotter = pv.Plotter()
waveguide_mesh = pv.StructuredGrid(x, y, z)
plotter.add_mesh(waveguide_mesh, style='wireframe')
plotter.show()
The curves look reasonable:
As does the interpolated surface:

How to rotate a set of 2D points to minimize vertical span in numpy?

I have a collection of 2D points and I want to rotate them so as to display them spanning the least vertical height. Is there a way to find the angle without using optimization?
As a brief illustration:
import numpy as np
# some initial points spread out vertically
xy = np.random.randn(2, 100)
xy[1, :] *= 2
def rotate(xy, theta):
"""Return a rotated set of points.
"""
s = np.sin(theta)
c = np.cos(theta)
xyr = np.empty_like(xy)
xyr[0, :] = c * xy[0, :] - s * xy[1, :]
xyr[1, :] = s * xy[0, :] + c * xy[1, :]
return xyr
def span(xy):
"""Return the vertical span of the points.
"""
return xy[1, :].max() - xy[1, :].min()
def plot(xy):
"""2D plot with fixed aspect ratio.
"""
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3, 3))
ax.scatter(*xy, alpha=0.3)
ax.set_aspect(1)
plt.show()
plt.show()
Have a look at the original points:
>>> span(xy)
11.503342270923472
>>> plot(xy)
Have a look at some rotated points:
>>> xyr = rotate(xy, np.pi / 2)
>>> span(xyr)
4.594620173868735
>>> plot(xyr)
The optimal answer (which should here be around pi / 2) is easy to find by invoking scipy:
>>> from scipy.optimize import minimize_scalar
>>> minimize_scalar(lambda theta: span(rotate(xy, theta)))
fun: 4.523188831276214
nfev: 38
nit: 34
success: True
x: 1.590391370976612
But surely there is a simpler way that doesn't require scipy - and 38 function evaluations!
This is probably not necessarily optimal, but a good enough simple solution may be to take the main components of the data and rotate the data to align largest main component with the horizontal axis.
import numpy as np
# Posted code...
# Find eigenvector with largest eigenvalue
c = (xy # xy.T) / (xy.shape[1] - 1)
evals, evecs = np.linalg.eig(c)
imax = np.abs(evals).argmax()
v = evecs[:, imax]
# Correction angle is opposite of vector angle
theta = -np.arctan2(v[1], v[0])
# Rotate
xy2 = rotate(xy, theta)
# Plot
plot(xy2)
Output:

Why is this randomly generated spherical point cloud not uniformly distributed?

I'm trying to simulate radiation emitting from a point source. To do this, given the coordinates of a source and the desired length of emitted rays, I randomly generate a direction vector in spherical coordinates, convert it to cartesian, and return the correct end point. However, when I run this, and visualize the resulting point cloud (consisting of all the randomly generated end points) in Blender, I see that it's more densely populated at the "poles" of the sphere. I'd like the points to be uniformly distributed along the sphere. How can I achieve this?
The random generation function:
def getRadiationEmissionLineSeg(p, t):
if(p.size == 4):
#polar angle spans [0, pi] from +Z axis to -Z axis
#azimuthal angle spans [0, 2*pi] orthogonal to the zenith (in the XY plane)
theta = math.pi * random.random()
phi = 2 * math.pi * random.random()
#use r = 1 to get a unit direction vector
v = sphericalToCartesian(1, theta, phi)
#parametric vector form: vec = p + tv
#p = point that lies on vector (origin point in case of a ray)
#t = parameter (-inf, inf) for lines, [0, inf) for rays
#v = direction vector (must be normalized)
return p + t * v
The spherical coordinates -> cartesian conversion function:
def sphericalToCartesian(r, theta, phi):
x = r * math.sin(theta) * math.cos(phi)
y = r * math.sin(theta) * math.sin(phi)
z = r * math.cos(theta)
return npy.array([x, y, z, 0])
When you transform points by spherical coordinates and angle theta approaches pi, the circle which is an image of [0,2pi]x{theta} gets smaller and smaller. Since theta is uniformly distributed, there will be more points near poles. It could be seen on image of grid.
If you want to generate uniformly distributed points on sphere, you can use the fact that if you cut a sphere with two parallel planes, the area of the strip of spherical surface between the planes depends only on the distance between the planes. Hence, you can get a uniform distribution on the sphere using two uniformly distributed random variables:
z coordinate between -r and r,
an angle theta between [0, 2pi) corresponding to a longitude.
Then you can easily calculate x and y coordiantes.
Example code:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
r = 1
n = 1000
z = np.random.random(n)*2*r - r
phi = np.random.random(n)*2*np.pi
x = np.sqrt(1 - z**2)*np.cos(phi)
y = np.sqrt(1 - z**2)*np.sin(phi)
fig = plt.figure(figsize=(8, 8))
ax = plt.axes(projection='3d')
ax.scatter(x, y, z)
plt.show()
Results for n=100,250,1000:

Color matplotlib quiver field according to magnitude and direction

I'm attempting to achieve the same behavior as this function in Matlab, whereby the color of each arrow corresponds to both its magnitude and direction, essentially drawing its color from a wheel. I saw this question, but it only seems to work for barbs. I also saw this answer, but quiver complains that the color array must be two-dimensional.
What is the best way to compute C for matplotlib.pyplot.quiver, taking into account both magnitude and direction?
Even though this is quite old now, I've come across the same problem. Based on matplotlibs quiver demo and my own answer to this post, I created the following example. The idea is to convert the angle of a vector to the color using HSV colors Hue value. The absolute value of the vector is used as the saturation and the value.
import numpy as np
import matplotlib.colors
import matplotlib.pyplot as plt
def vector_to_rgb(angle, absolute):
"""Get the rgb value for the given `angle` and the `absolute` value
Parameters
----------
angle : float
The angle in radians
absolute : float
The absolute value of the gradient
Returns
-------
array_like
The rgb value as a tuple with values [0..1]
"""
global max_abs
# normalize angle
angle = angle % (2 * np.pi)
if angle < 0:
angle += 2 * np.pi
return matplotlib.colors.hsv_to_rgb((angle / 2 / np.pi,
absolute / max_abs,
absolute / max_abs))
X = np.arange(-10, 10, 1)
Y = np.arange(-10, 10, 1)
U, V = np.meshgrid(X, Y)
angles = np.arctan2(V, U)
lengths = np.sqrt(np.square(U) + np.square(V))
max_abs = np.max(lengths)
c = np.array(list(map(vector_to_rgb, angles.flatten(), lengths.flatten())))
fig, ax = plt.subplots()
q = ax.quiver(X, Y, U, V, color=c)
plt.show()
The color wheel is the following. The code for generating it is mentioned in the Edit.
Edit
I just noticed, that the linked matlab function "renders a vector field as a grid of unit-length arrows. The arrow direction indicates vector field direction, and the color indicates the magnitude". So my above example is not really what is in the question. Here are some modifications.
The left graph is the same as above. The right one does, what the cited matlab function does: A unit-length arrow plot with the color indicating the magnitude. The center one does not use the magnitude but only the direction in the color which might be useful too. I hope other combinations are clear from this example.
import numpy as np
import matplotlib.colors
import matplotlib.pyplot as plt
def vector_to_rgb(angle, absolute):
"""Get the rgb value for the given `angle` and the `absolute` value
Parameters
----------
angle : float
The angle in radians
absolute : float
The absolute value of the gradient
Returns
-------
array_like
The rgb value as a tuple with values [0..1]
"""
global max_abs
# normalize angle
angle = angle % (2 * np.pi)
if angle < 0:
angle += 2 * np.pi
return matplotlib.colors.hsv_to_rgb((angle / 2 / np.pi,
absolute / max_abs,
absolute / max_abs))
X = np.arange(-10, 10, 1)
Y = np.arange(-10, 10, 1)
U, V = np.meshgrid(X, Y)
angles = np.arctan2(V, U)
lengths = np.sqrt(np.square(U) + np.square(V))
max_abs = np.max(lengths)
# color is direction, hue and value are magnitude
c1 = np.array(list(map(vector_to_rgb, angles.flatten(), lengths.flatten())))
ax = plt.subplot(131)
ax.set_title("Color is lenth,\nhue and value are magnitude")
q = ax.quiver(X, Y, U, V, color=c1)
# color is length only
c2 = np.array(list(map(vector_to_rgb, angles.flatten(),
np.ones_like(lengths.flatten()) * max_abs)))
ax = plt.subplot(132)
ax.set_title("Color is direction only")
q = ax.quiver(X, Y, U, V, color=c2)
# color is direction only
c3 = np.array(list(map(vector_to_rgb, 2 * np.pi * lengths.flatten() / max_abs,
max_abs * np.ones_like(lengths.flatten()))))
# create one-length vectors
U_ddash = np.ones_like(U)
V_ddash = np.zeros_like(V)
# now rotate them
U_dash = U_ddash * np.cos(angles) - V_ddash * np.sin(angles)
V_dash = U_ddash * np.sin(angles) + V_ddash * np.cos(angles)
ax = plt.subplot(133)
ax.set_title("Uniform length,\nColor is magnitude only")
q = ax.quiver(X, Y, U_dash, V_dash, color=c3)
plt.show()
To plot the color wheel use the following code. Note that this uses the max_abs value from above which is the maximum value that the color hue and value can reach. The vector_to_rgb() function is also re-used here.
ax = plt.subplot(236, projection='polar')
n = 200
t = np.linspace(0, 2 * np.pi, n)
r = np.linspace(0, max_abs, n)
rg, tg = np.meshgrid(r, t)
c = np.array(list(map(vector_to_rgb, tg.T.flatten(), rg.T.flatten())))
cv = c.reshape((n, n, 3))
m = ax.pcolormesh(t, r, cv[:,:,1], color=c, shading='auto')
m.set_array(None)
ax.set_yticklabels([])
I don't know if you've since found that quiver with matplotlib 1.4.x has 3d capability. This capability is limited when attempting to colour the arrows however.
A friend and I write the following script (in half an hour or so) to plot my experiment data using hex values from a spreadsheet, for my thesis. We're going to make this more automated once we're done with the semester but the issue with passing a colour map to quiver is that it can't accept a vector form for some reason.
This link is to my git repository where the code I used, slightly neatened up by another friend, is hosted.
I hope I can save someone the time it took me.

Categories