Trouble plotting a right triangle at an angle in matplotlib - python

I'm trying to generate a right triangle with hypotenuse = 1, interior angle 25, with its base rotated 30 degrees. I've entered the trig formulas correctly to my understanding, and I suspect some kind of rounding error. Because the triangle produced in matplot is slightly off from a right triangle.
import math
import matplotlib.pyplot as plt
from annotation import label
angle_b = math.radians(25) # the interior 25 degree angle of our right triangle
angle_a = math.radians(30) # 30 degree angle between the plotted angle_b triangle base and the x axis.
point_A = (0,0)
point_B = (math.cos(angle_a + angle_b), math.sin(angle_a + angle_b))
point_C = (math.cos(angle_a) * math.cos(angle_b), math.sin(angle_a) * math.cos(angle_b))
# Label our points
label(plt, 'A', point_A)
label(plt, 'B', point_B)
label(plt, 'C', point_C)
# Draw the right triangle between our points.
plt.plot(*zip(point_A, point_B, point_C, point_A), marker='o', color='black')
As you can see, the angle ACB is not a right angle as trigonometry would predict.
#Mad Physicist pointed out a the comment that ABC does appear to be a right triangle. I would like to get ACB to be a right angle, like this example.
Here is with the right triangle formed by angle_a below the previous one:
point_D = (math.cos(angle_a) * math.cos(angle_b), 0)
plt.plot(*zip(point_A, point_C, point_D, point_A), marker='o', color='black')
label(plt, 'D', point_D)
Now solved. I finally got it working thanks to the line ax.set_aspect('equal')

The problem is that by default, matplotlib doesn't use the same distances in the x and in the y direction. Instead, matplotlib tries to fit everything nicely into the given bounds.
These uneven distances distort angles, and also deforms circles.
You can force an equal aspect ratio via ax.set_aspect('equal').
To calculate the positions via the angles, and have the right corner at point B, you need to take into account that the length AC is cos(b) times the length of AC. AC can be chosen to have 1 as length. Alternatively, you could divide both B and C by cos(b) to have a larger triangle, where the length of AB would be 1.
import matplotlib.pyplot as plt
import math
angle_b = math.radians(25) # the interior 25 degree angle of our right triangle
angle_a = math.radians(30) # 30 degree angle between the plotted angle_b triangle and the x axis.
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(14, 5), sharey=True)
for ax in (ax1, ax2):
point_A = (0, 0)
if ax == ax1:
point_B = (math.cos(angle_a + angle_b) * math.cos(angle_b), math.sin(angle_a + angle_b) * math.cos(angle_b))
point_C = (math.cos(angle_a), math.sin(angle_a))
ax.set_title('length AC is 1')
else:
point_B = (math.cos(angle_a + angle_b), math.sin(angle_a + angle_b))
point_C = (math.cos(angle_a) / math.cos(angle_b), math.sin(angle_a) / math.cos(angle_b))
ax.set_title('length AB is 1')
point_M = ((point_A[0] + point_C[0]) / 2, (point_A[1] + point_C[1]) / 2)
# Draw the right triangle between our points.
ax.plot(*zip(point_A, point_B, point_C, point_A), marker='o', color='black')
# draw a circle around the 3 points
ax.add_patch(plt.Circle(point_M, math.sqrt((point_M[0] - point_A[0]) ** 2 + (point_M[1] - point_A[1]) ** 2),
ec='r', ls='--', fc='none'))
ax.set_aspect('equal', 'datalim')
plt.show()
The same calculation works for any angle. Here is how 12 rotations in steps of 30 degrees look together:
The following code shows the effect of ax.set_aspect('equal') for the original points.
import matplotlib.pyplot as plt
import math
angle_b = math.radians(25) # the interior 25 degree angle of our right triangle
angle_a = math.radians(30) # 30 degree angle between the plotted angle_b triangle and the x axis.
point_A = (0, 0)
point_B = (math.cos(angle_a + angle_b), math.sin(angle_a + angle_b))
point_C = (math.cos(angle_a) * math.cos(angle_b), math.sin(angle_a) * math.cos(angle_b))
point_M = ((point_A[0] + point_B[0]) / 2, (point_A[1] + point_B[1]) / 2)
fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(16, 4), gridspec_kw={'width_ratios': [2, 1]})
for ax in (ax1, ax2):
# Draw the right triangle between our points.
ax.plot(*zip(point_A, point_B, point_C, point_A), marker='o', color='black')
# draw a circle around the 3 points
ax.add_patch(plt.Circle(point_M, math.sqrt((point_M[0] - point_A[0]) ** 2 + (point_M[1] - point_A[1]) ** 2),
ec='r', ls='--', fc='none'))
ax1.set_title('Default aspect ratio, deforms the angles')
ax2.set_aspect('equal') # or plt.axis('equal')
ax2.set_title('Equal aspect ratio')
plt.tight_layout()
plt.show()

You appear to have jumbled your trigonometry. I'm going to suggest moving slowly step-by-step, and using variable names that help you remember what is going on.
Let's start with the original triangle. If interior_angle = np.deg2rad(25) and the hypotenuse has length 1, then the right angle on the x-axis is at (np.cos(interior_angle), 0) and the other interior angle is at (np.cos(interior_angle), np.sin(interior_angle)). Neither of the points in your diagram corresponds.
Now let's express the triangle as a matrix whose columns are the vertices:
interior_angle = np.deg2rad(25)
vertices = np.array([
[0, np.cos(interior_angle), np.cos(interior_angle), 0],
[0, 0, np.sin(interior_angle), 0],
])
The last vertex is a repeat of the origin to make plotting easier.
Now let's look at the rotation. For rotation_angle = np.deg2rad(30), point (x, y) rotates to (np.cos(rotation_angle) * x - np.sin(rotation_angle) * y, np.sin(rotation_angle) * x + np.cos(rotation_angle) * y). This can be expressed as a matrix equation:
rotation_matrix = np.array([
[np.cos(rotation_angle), -np.sin(rotation_angle)],
[np.sin(rotation_angle), np.cos(rotation_angle)]])
p_out = rotation_matrix # p_in
The array vertices is so constructed that it can be directly multiplied by a rotation matrix. You can therefore write
rotation_angle = np.deg2rad(30)
rotation_matrix = np.array([
[np.cos(rotation_angle), -np.sin(rotation_angle)],
[np.sin(rotation_angle), np.cos(rotation_angle)]])
rotated_vertices = rotation_matrix # vertices
The plotted image should make more sense now:
plt.plot(*vertices)
plt.plot(*rotated_vertices)
plt.axis('equal')
plt.show()

Related

How to draw tangential circles to the inside of a curve in Python using numpy and matplotlib in Python?

I would like to plot circles which are tangents to the interior of a curve in Python as shown below:
I tried the following approach.
I created an inverted exponential curve to get the curve as shown below.
x = np.arange(0, 2, 0.1)
y = [1/np.exp(i) for i in x]
plt.plot(x, y, marker = "o")
To add a circle as tangent to the curve, I am adding a circle manually as follows:
fig, ax = plt.subplots()
#Plot exponential curve
ax.plot(x, y)
#Add tangential circle manually
pi = np.pi
c1 = plt.Circle((x[1]+ np.sin(pi/4) * 0.1, y[1] + np.sin(pi/4) * 0.1),
radius = 0.1, facecolor = "black", ec = "white")
ax.add_patch(c1)
Here my assumption is that the angle denoted by the shaded part in the figure below (between the normal radius and horizontal x-axis is 45 degrees).
However, I think this is an incorrect assumption and incorrect way to do it.
When I add more circles, they are not exactly tangential to the curve as shown below.
What would be the correct approach to draw the circles as tangents to the curve? Is it possible to do it in Python?
Using trigonometry (there is likely a way to simplify but I'm rusty+tired :p).
fig, ax = plt.subplots()
X = np.arange(0, 5, 0.1)
Y = np.sin(X)
#Plot exponential curve
ax.plot(X, Y)
ax.set_aspect('equal')
#Add tangential circles
for i in np.arange(1,len(X),5):
ax.plot(X[i], Y[i], marker='o', color='r')
pi = np.pi
radius = 0.3
# slope of tangent
dydx = np.gradient(Y)[i]/np.gradient(X)[i]
# slope of perpendicular
slope = 1/abs(dydx)
# angle to horizontal
angle = np.arctan(slope)
c1 = plt.Circle((X[i] - np.sign(dydx)*np.cos(angle) * radius,
Y[i] + np.sin(angle) * radius),
radius = radius, facecolor = "black", ec = "white")
ax.add_patch(c1)
output:
Your function is exp(-x)
It's derivative is -exp(-x), so tangent vector at point x, exp(-x) is (1, -exp(-x)) and normal vector is (exp(-x), 1) (note sign change).
Normalize it
n_len = np.hypot((y)[i], 1)
nx = y[i] / n_len
ny = 1 / n_len
center for radius R is
c1 = plt.Circle((x[i] + R * nx, y[i] + R * ny, ...
For the circle to be tangent, its center must be located along the normal to the curve (this is a straight-line with slope -δx/δy).
You did not specify how the radius varies, so there are two options:
the radius is a function of x or y or s (curvilinear abscissa) that you supply; then place the center at the desired distance from the contact point, on the normal;
the radius is such that it matches the local curvature; then what you want is the osculating circle; there is a formula to compute the radius of curvature.
https://en.wikipedia.org/wiki/Osculating_circle

How to tell is a plotted point is within a circle matplotlib

I have a circle plotted on a graph using matplotlib
angle = np.linspace(0, 4 * np.pi, 150)
radius = 0.2
x = radius * np.cos(angle) + 0.5
y = radius * np.sin(angle) + 0.5
fig, ax = plt.subplots()
ax.set_aspect(1)
ax.plot(x, y)
I also have some code that randomly plots scatter points on the graph:
q = [random.uniform(0.3, 0.7) for n in range(900)]
b = [random.uniform(0.3, 0.7) for n in range(900)]
ax.scatter(q, b, color='black', marker='.', s=1)
I want to count how many of the randomly plotted points fall within the circle. Is there a way I can do this?
To do this you have to find the points that have a distance between them and the center of the circle inferior to the radius. The center of the circle is (0.5, 0.5).
To do this you can do the following:
import numpy as np
q, b = np.array(q), np.array(b)
# The mask is a boolean array that tells if a point is inside
# of the circle or outside of the circle.
mask = np.sqrt((q-0.5)**2 + (b-0.5)**2) <= radius
#np.count_nonzero counts the number of True elements in an array.
number_inside = np.count_nonzero(mask)
number_inside is the number of points inside the circle.

Using a linear equation to create a radially interpolated circle

Given a linear equation, I want to use the slope to create a circle of values around a given point, defined by the slope of the linear equation if possible
Im currently a bit far away - can only make the radial plot but do not know how to connect this with an input equation. My first thought would be to change the opacity using import matplotlib.animation as animation and looping matplotlib's alpha argument to become gradually more and more opaque. However the alpha doesnt seem to change opacity.
Code:
# lenth of radius
distance = 200
# create radius
radialVals = np.linspace(0,distance)
# 2 pi radians = full circle
azm = np.linspace(0, 2 * np.pi)
r, th = np.meshgrid(radialVals, azm)
z = (r ** 2.0) / 4.0
# creates circle
plt.subplot(projection="polar")
# add color gradient
plt.pcolormesh(th, r, z)
plt.plot(azm, r,alpha=1, ls='', drawstyle = 'steps')
#gridlines
# plt.grid()
plt.show()
Here is one way to solve it, the idea is to create a mesh, calculate the colors with a function then use imshow to visualize the mesh.
from matplotlib import pyplot as plt
import numpy as np
def create_mesh(slope,center,radius,t_x,t_y,ax,xlim,ylim):
"""
slope: the slope of the linear function
center: the center of the circle
raadius: the radius of the circle
t_x: the number of grids in x direction
t_y: the number of grids in y direction
ax: the canvas
xlim,ylim: the lims of the ax
"""
def cart2pol(x,y):
rho = np.sqrt(x**2 + y**2)
phi = np.arctan2(y,x)
return rho,phi
def linear_func(slope):
# initialize a patch and grids
patch = np.empty((t_x,t_y))
patch[:,:] = np.nan
x = np.linspace(xlim[0],xlim[1],t_x)
y = np.linspace(ylim[0],ylim[1],t_y)
x_grid,y_grid = np.meshgrid(x, y)
# centered grid
xc = np.linspace(xlim[0]-center[0],xlim[1]-center[0],t_x)
yc = np.linspace(ylim[0]-center[1],ylim[1]-center[1],t_y)
xc_grid,yc_grid = np.meshgrid(xc, yc)
rho,phi = cart2pol(xc_grid,yc_grid)
linear_values = slope * rho
# threshold controls the size of the gaussian
circle_mask = (x_grid-center[0])**2 + (y_grid-center[1])**2 < radius
patch[circle_mask] = linear_values[circle_mask]
return patch
# modify the patch
patch = linear_func(slope)
extent = xlim[0],xlim[1],ylim[0],ylim[1]
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
fig,ax = plt.subplots(nrows=1,ncols=2,figsize=(12,6))
slopes = [40,30]
centroids = [[2,2],[4,3]]
radii = [1,4]
for item in ax:item.set_xlim(0,8);item.set_ylim(0,8)
v_max,v_min = max(slopes),0
create_mesh(slopes[0],centroids[0],radii[0],t_x=300,t_y=300,ax=ax[0],xlim=(0,8),ylim=(0,8))
create_mesh(slopes[1],centroids[1],radii[1],t_x=300,t_y=300,ax=ax[1],xlim=(0,8),ylim=(0,8))
plt.show()
The output of this code is
As you can see, the color gradient of the figure on the left is not as sharp as the figure on the right because of the different slopes ([40,30]).
Also note that, these two lines of code
v_max,v_min = max(slopes),0
ax.imshow(patch,alpha=.6,interpolation='bilinear',extent=extent,
cmap=plt.cm.YlGn,vmin=v_min,vmax=v_max)
are added in order to let the two subplots share the same colormap.

Drawing elliptical lines between two circles without intersecting matplotlib

I'm trying to draw elliptical lines using matplotlib to connect two circles but would like to make it so the elliptical lines do not intersect either circle.
Currently my design has resulted in this:
which as you can see has lines going through both circle A and B.
I decided to use matplotlib.patches.Arc since I didn't want it filled and it allowed me to draw a left and right part. Here is what I have:
from matplotlib import pyplot
from matplotlib.patches import Arc
import math
def calculate_perimeter(a, b):
perimeter = math.pi * (3*(a+b) - math.sqrt( (3*a + b) * (a + 3*b) ))
return perimeter
def draw_circle(xy, radius, text):
circle = pyplot.Circle(xy, radius=radius, fill=False)
pyplot.gca().add_patch(circle)
pyplot.gca().annotate(text, xy=xy, fontsize=10, ha='center', va='center')
def draw_arc(xy1, xy2, a, b, theta1, theta2):
# Calculate center of the elliptical arc
center = (xy1[0], (xy1[1] + xy2[1])/2.0)
arc = Arc(center, a, b, theta1=theta1, theta2=theta2)
pyplot.gca().add_patch(arc)
if __name__ == '__main__':
pyplot.figure()
center_circle1 = (5, 5)
center_circle2 = (5, 20)
dist_y = center_circle2[1] - center_circle1[1]
adjustment = 5.3 # #TODO: How do I calculate what this needs to be?
# Circles
draw_circle(center_circle1, 1, 'A')
draw_circle(center_circle2, 1, 'B')
# Draw right side of arc
theta1 = 270.0 + adjustment
theta2 = 90.0 - adjustment
draw_arc(center_circle1, center_circle2, 3, dist_y, theta1, theta2)
# Draw left side of arc
theta1 = 90.0 + adjustment
theta2 = 270.0 - adjustment
draw_arc(center_circle1, center_circle2, 3, dist_y, theta1, theta2)
pyplot.axis('scaled')
pyplot.axis('off')
pyplot.show()
For instance when I put adjustment = 5.3 I get:
If I zoom in on this area though it's easy to see it does not line up:
My question then becomes, how do I calculate what adjustment should be?
I thought I would be able to calculate the perimeter if I consider it a complete ellipse and subtract the amount that overlaps in one of the circles and use that to get the adjustment, but I'm not sure if that would work or how to calculate how much overlaps inside. Any help on this would be appreciated.
Rather than adjusting the figure manually, consider using the zorder in the Patch constructor.
The various artists on a plot are stacked upon each other vertically, with those with the highest zorder on top. By setting the zorder, therefore, you will cause the circles to be drawn over the ellipse, obscuring it.
Example code:
from matplotlib import pyplot as plt
from matplotlib.patches import Circle, Arc
fig, ax = plt.subplots(figsize=(6, 6))
ax.add_patch(Circle((0.5, 0.75), 0.05, edgecolor='black', facecolor='white', zorder=3))
ax.add_patch(Circle((0.5, 0.25), 0.05, edgecolor='black', facecolor='white', zorder=3))
ax.add_patch(Arc((0.5, 0.5), 0.1, 0.5))
That generates
.

Off-centered weighted numpy histogram2d?

I'm attempting to generate a model PSF from a set of observed stars. I'm following the great example provided by ali_m in this answer (MCVE below)
The 5 stars I'm using look like this:
where the center (peak intensity) is at bins [9, 9]. The results of their combination via numpy's hitsogram2d is this:
showing a peak density at bins [8, 8]. To center it at [9, 9], I have to obtain the centroids (see below) as:
cx, cy = np.array([1.] * len(stars)), np.array([1.] * len(stars))
instead. Why is this?
import numpy as np
from matplotlib import pyplot as plt
stars = # Uploaded here: http://pastebin.com/tjLqM9gQ
fig, ax = plt.subplots(2, 3, figsize=(5, 5))
for i in range(5):
ax.flat[i].imshow(
stars[i], cmap=plt.cm.viridis, interpolation='nearest',
origin='lower', vmin=0.)
ax.flat[i].axhline(9., ls='--', lw=2, c='w')
ax.flat[i].axvline(9., ls='--', lw=2, c='w')
fig.tight_layout()
# (nstars, ny, nx) pixel coordinates relative to each centroid
# pixel coordinates (integer)
x, y = np.mgrid[:20, :20]
# centroids (float)
cx, cy = np.array([0.] * len(stars)), np.array([0.] * len(stars))
dx = cx[:, None, None] + x[None, ...]
dy = cy[:, None, None] + y[None, ...]
# 2D weighted histogram
bins = np.linspace(0., 20., 20)
h, xe, ye = np.histogram2d(dx.ravel(), dy.ravel(), bins=bins,
weights=stars.ravel())
fig, ax = plt.subplots(1, 1, subplot_kw={'aspect': 'equal'})
ax.hold(True)
ax.imshow(h, cmap=plt.cm.viridis, interpolation='nearest',
origin='lower', vmin=0.)
ax.axhline(8., ls='--', lw=2, c='w')
ax.axvline(8., ls='--', lw=2, c='w')
plt.show()
The reason, the histogram is not centered at the point (9,9) where the single star intensity distribution is centered, is that the code to generate it shifts around the bins of the histogram.
As I already suggested in the comments, keep things simple. E.g. we do not need plots to see the problem. Also, I do not understand what those dx dy are, so let's avoid them.
We can then calculate the histogram by
import numpy as np
stars = # Uploaded here: http://pastebin.com/tjLqM9gQ
# The argmax of a single star results in (9,9)
single_star_argmax = np.unravel_index(np.argmax(stars[0]), stars[0].shape)
# Create a meshgrid of coordinates (0,1,...,19) times (0,1,...,19)
y,x = np.mgrid[:len(stars[0,:,0]), :len(stars[0,0,:])]
# duplicating the grids
xcoord, ycoord = np.array([x]*len(stars)), np.array([y]*len(stars))
# compute histogram with coordinates as x,y
# and [20,20] bins
h, xe, ye = np.histogram2d(xcoord.ravel(), ycoord.ravel(),
bins=[len(stars[0,0,:]), len(stars[0,:,0])],
weights=stars.ravel())
# The argmax of the combined stars results in (9,9)
combined_star_argmax = np.unravel_index(np.argmax(h), h.shape)
print single_star_argmax
print combined_star_argmax
print single_star_argmax == combined_star_argmax
# prints:
# (9, 9)
# (9, 9)
# True
The only problem in the original code really was the line bins = np.linspace(0., 20., 20) which creates 20 points between 0 and 20,
0. 1.05263158 2.10526316 ... 18.94736842 20.
This scales the bin size to ~1.05 and lets your argmax occur already "earlier" then expected.
What you really want are 20 points between 0 and 19, np.linspace(0,19,20) or
np.arange(0,20)
To avoid such mistakes, one can simply give the length of the original array as argument, bins=20.

Categories