I have a 2D numpy matrix of size 512x256. I can easily convert it to an image using PIL, or scipy, etc. but that gives me the shape of a rectangle of size 512x256, obviously. I am wondering if I can do something to make this matrix take shape of a cone like the figure attached?
How I am thinking about it is that the first column of the matrix would be the left most line of the cone and the next column of the matrix would be a little right to that line, and so on. Since the angle between the two extremes is 45 degrees and I have 256 columns, that would be mean that each line gets an increment of (45/256) degree angle? These are just some rough thoughts but I wanted to learn from the community if they have any ideas about how should I proceed with this? I am envisioning a black square main image and this cone in the middle of it. Any ideas/thoughts?
Here is a quick & dirty solution that maps polar coordinates in the result image to rectangular coordinates in the original image and uses interp2d on each channel of the original image:
import numpy as np
from scipy import misc
from scipy.interpolate import interp2d
from math import pi, atan2, hypot
inputImagePath = 'wherever/whateverYouWantToInterpolate.jpg'
resultWidth = 800
resultHeight = 600
centerX = resultWidth / 2
centerY = - 50.0
maxAngle = 45.0 / 2 / 180 * pi
minAngle = -maxAngle
minRadius = 100.0
maxRadius = 600.0
inputImage = misc.imread(inputImagePath)
h,w,chn = inputImage.shape
print(f"h = {h} w = {w} chn = {chn}")
channels = [inputImage[:,:,i] for i in range(3)]
interpolated = [interp2d(range(w), range(h), c) for c in channels]
resultImage = np.zeros([resultHeight, resultWidth, 3], dtype = np.uint8)
for c in range(resultWidth):
for r in range(resultHeight):
dx = c - centerX
dy = r - centerY
angle = atan2(dx, dy) # yes, dx, dy in this case!
if angle < maxAngle and angle > minAngle:
origCol = (angle - minAngle) / (maxAngle - minAngle) * w
radius = hypot(dx, dy)
if radius > minRadius and radius < maxRadius:
origRow = (radius - minRadius) / (maxRadius - minRadius) * h
for chn in range(3):
resultImage[r, c, chn] = interpolated[chn](origCol, origRow)
import matplotlib.pyplot as plt
plt.imshow(resultImage)
plt.show()
Produces:
The performance is terrible, didn't bother to "vectorize". Will update when find out how.
Related
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:
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()
I would like to represent the elliptical orbit of a binary system of two stars. What I aim to, is something like this:
Where I have a grid of the sizes along the axes, an in-scale star at the focus, and the orbit of the secondary star. The decimal numbers along the orbit are the orbital phases. The arrow at the bottom is the Earth direction, and the thick part at the orbit is related to the observation for that specific case - I don't need it. What I want to change from this plot is:
Orbital phase: instead of numbers along the orbit, I would like "dashed rays" from the focus to the orbit, and the orbital phase above them:
I don't want the cross along (0, 0);
I would like to re-orient the orbit, in order that the 0.0 phase is around the top left part of the plot, and the Earth direction is an upward pointing straight arrow (the parameters of my system are different from the one plotted here).
I tried to look for python examples, but the only thing I came out with (from here), is a polar plot:
which is not really representative of what I want, but still is a beginning:
import numpy as np
import matplotlib.pyplot as plt
cos = np.cos
pi = np.pi
a = 10
e = 0.1
theta = np.linspace(0,2*pi, 360)
r = (a*(1-e**2))/(1+e*cos(theta))
fig = plt.figure()
ax = fig.add_subplot(111, polar=True)
ax.set_yticklabels([])
ax.plot(theta,r)
print(np.c_[r,theta])
plt.show()
Here's something that gets you very close. You do not need polar coordinates to plot a decent ellipse. There is a so-called artist you can readily utilize.
You probably have to customize the axis labels and maybe insert an arrow or two if you want:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
# initializing the figure:
fig = plt.figure()
# the (carthesian) axis:
ax = fig.add_subplot(111,aspect='equal')
ax.grid(True)
# parameters of the ellipse:
a = 5.0
e = 4.0
b = np.sqrt(a**2.0 - e**2.0)
# the center of the ellipse:
x = 6.0
y = 6.0
# the angle by which the ellipse is rotated:
angle = -45.0
#angle = 0.0
# plotting the ellipse, using an artist:
ax.add_artist(Ellipse(xy=[x,y], width=2.0*a, height=2.0*b, \
angle=angle, facecolor='none'))
ax.set_xlim(0,2.0*x)
ax.set_ylim(0,2.0*y)
# marking the focus (actually, both)
# and accounting for the rotation of the ellipse by angle
xf = [x - e*np.cos(angle * np.pi/180.0),
x + e*np.cos(angle * np.pi/180.0)]
yf = [y - e*np.sin(angle * np.pi/180.0),
y + e*np.sin(angle * np.pi/180.0)]
ax.plot(xf,yf,'xr')
# plotting lines from the focus to the ellipse:
# these should be your "rays"
t = np.arange(np.pi,3.0*np.pi,np.pi/5.0)
p = b**2.0 / a
E = e / a
r = [p/(1-E*np.cos(ti)) for ti in t]
# converting the radius based on the focus
# into x,y coordinates on the ellipse:
xr = [ri*np.cos(ti) for ri,ti in zip(r,t)]
yr = [ri*np.sin(ti) for ri,ti in zip(r,t)]
# accounting for the rotation by anlge:
xrp = [xi*np.cos(angle * np.pi/180.0) - \
yi*np.sin(angle * np.pi/180.0) for xi,yi in zip(xr,yr)]
yrp = [xi*np.sin(angle * np.pi/180.0) + \
yi*np.cos(angle * np.pi/180.0) for xi,yi in zip(xr,yr)]
for q in range(0,len(t)):
ax.plot([xf[0], xf[0]+xrp[q]],[yf[0], yf[0]+yrp[q]],'--b')
# put labels outside the "rays"
offset = 0.75
rLabel = [ri+offset for ri in r]
xrl = [ri*np.cos(ti) for ri,ti in zip(rLabel,t)]
yrl = [ri*np.sin(ti) for ri,ti in zip(rLabel,t)]
xrpl = [xi*np.cos(angle * np.pi/180.0) - \
yi*np.sin(angle * np.pi/180.0) for xi,yi in zip(xrl,yrl)]
yrpl = [xi*np.sin(angle * np.pi/180.0) + \
yi*np.cos(angle * np.pi/180.0) for xi,yi in zip(xrl,yrl)]
# for fancy label rotation reduce the range of the angle t:
tlabel = [(ti -np.pi)*180.0/np.pi for ti in t]
for q in range(0,len(tlabel)):
if tlabel[q] >= 180.0:
tlabel[q] -= 180.0
# convert the angle t from radians into degrees:
tl = [(ti-np.pi)*180.0/np.pi for ti in t]
for q in range(0,len(t)):
rotate_label = angle + tlabel[q]
label_text = '%.1f' % tl[q]
ax.text(xf[0]+xrpl[q],yf[0]+yrpl[q],label_text,\
va='center', ha='center',rotation=rotate_label)
plt.show()
The example above will result in this figure:
Explanations:
You can use an artist to plot the ellipse, instead of using polar coordinates
The nomenclature is based on the definitions available on Wikipedia
The angle in the artist setup rotates the ellipse. This angle is later used to rotate coordinates for the rays and labels (this is just math)
The rays are derived from the polar form of the ellipse relative to a focus.
The angle t runs from pi to 3.0*pi because I assumed that this would correspond to your idea of where the rays should start. You get the same rays for 0 to 2.0*pi. I used np.arange instead of linspace because I wanted a defined increment (pi/5.0, or 36 degrees) in this example.
The labels at the end of the rays are placed as text, the variable offset controls the distance between the ellipse and the labels. Adjust this as needed.
For the alignment of the label text orientation with the rays, I reduced the angle, t, to the range 0 to 180 degrees. This makes for better readability compared to the full range of 0 to 360 degrees.
For the label text I just used the angle, t, for simplicity. Replace this with whatever information better suits your purpose.
The angle, t, was converted from radians to degrees before the loop that places the label. Inside the loop, each element of tl is converted to a string. This allows for more formatting control (e.g. %.3f if you needed 3 decimals)
I have a code that slices a numpy array into a circle. I wish to recover only the values included in a certain range of angles from the circle and mask the array. For example: mask the original array with the (x,y) positions comprised between 0 and 45 degrees of the circle.
Is there a pythonic way for doing so?
Here's my (simplified) original code:
import numpy as np
matrix = np.zeros((500,500))
x = 240
y = 280
radius = 10
mask=np.ogrid[x-radius:x+radius+1,y-radius:y+radius+1]
matrix[mask]
Thanks in advance
Edit: I omitted that radius can vary.
I would do this by converting from cartesian to polar coordinates and constructing boolean masks for the circle and for the range of angles you want:
import numpy as np
def sector_mask(shape,centre,radius,angle_range):
"""
Return a boolean mask for a circular sector. The start/stop angles in
`angle_range` should be given in clockwise order.
"""
x,y = np.ogrid[:shape[0],:shape[1]]
cx,cy = centre
tmin,tmax = np.deg2rad(angle_range)
# ensure stop angle > start angle
if tmax < tmin:
tmax += 2*np.pi
# convert cartesian --> polar coordinates
r2 = (x-cx)*(x-cx) + (y-cy)*(y-cy)
theta = np.arctan2(x-cx,y-cy) - tmin
# wrap angles between 0 and 2*pi
theta %= (2*np.pi)
# circular mask
circmask = r2 <= radius*radius
# angular mask
anglemask = theta <= (tmax-tmin)
return circmask*anglemask
For example:
from matplotlib import pyplot as pp
from scipy.misc import lena
matrix = lena()
mask = sector_mask(matrix.shape,(200,100),300,(0,50))
matrix[~mask] = 0
pp.imshow(matrix)
pp.show()
Same approach for centered circles in square matrices:
def circleMask(mat, r=0):
if mat.shape[0] != mat.shape[1]:
raise TypeError('Matrix has to be square')
if not isinstance(r, int):
raise TypeError('Radius has to be of type int')
s = mat.shape[0]
d = num.abs(num.arange(-s/2 + s%2, s/2 + s%2))
dm = num.sqrt(d[:, num.newaxis]**2 + d[num.newaxis, :]**2)
return num.logical_and(dm >= r-.5, dm < r+.5)
looping over this implicit function is costly!
I have a 2d map of a coordinate transform. The data at each point is the aximuthal angle in the original coordinate system, which goes from 0 to 360. I'm trying to use pyplot.contour to plot lines of constant angle, e.g. 45 degrees. The contour appears along the 45 degree line between the two poles, but there's an additional part to the contour that connects the two poles along the 0/360 discontinuity. This makes a very jagged ugly line as it basically just traces the pixels with a number close to 0 on one side and another close to 360 on the other.
Examples:
Here is an image using full colour map:
You can see the discontinuity along the blue/red curve on the left side. One side is 360 degrees, the other is 0 degrees. When plotting contours, I get:
Note that all contours connect the two poles, but even though I have NOT plotted the 0 degree contour, all the other contours follow along the 0 degree discontinuity (because pyplot thinks if it's 0 on one side and 360 on the other, there must be all other angles in between).
Code to produce this data:
import numpy as np
import matplotlib.pyplot as plt
jgal = np.array(
[
[-0.054875539726, -0.873437108010, -0.483834985808],
[0.494109453312, -0.444829589425, 0.746982251810],
[-0.867666135858, -0.198076386122, 0.455983795705],
]
)
def s2v3(rra, rdec, r):
pos0 = r * np.cos(rra) * np.cos(rdec)
pos1 = r * np.sin(rra) * np.cos(rdec)
pos2 = r * np.sin(rdec)
return np.array([pos0, pos1, pos2])
def v2s3(pos):
x = pos[0]
y = pos[1]
z = pos[2]
if np.isscalar(x):
x, y, z = np.array([x]), np.array([y]), np.array([z])
rra = np.arctan2(y, x)
low = np.where(rra < 0.0)
high = np.where(rra > 2.0 * np.pi)
if len(low[0]):
rra[low] = rra[low] + (2.0 * np.pi)
if len(high[0]):
rra[high] = rra[high] - (2.0 * np.pi)
rxy = np.sqrt(x ** 2 + y ** 2)
rdec = np.arctan2(z, rxy)
r = np.sqrt(x ** 2 + y ** 2 + z ** 2)
if x.size == 1:
rra = rra[0]
rdec = rdec[0]
r = r[0]
return rra, rdec, r
def gal2fk5(gl, gb):
rgl = np.deg2rad(gl)
rgb = np.deg2rad(gb)
r = 1.0
pos = s2v3(rgl, rgb, r)
pos1 = np.dot(pos.transpose(), jgal).transpose()
rra, rdec, r = v2s3(pos1)
dra = np.rad2deg(rra)
ddec = np.rad2deg(rdec)
return dra, ddec
def make_coords(resolution=50):
width = 9
height = 6
px = width * resolution
py = height * resolution
coords = np.zeros((px, py, 4))
for ix in range(0, px):
for iy in range(0, py):
l = 360.0 / px * ix - 180.0
b = 180.0 / py * iy - 90.0
dra, ddec = gal2fk5(l, b)
coords[ix, iy, 0] = dra
coords[ix, iy, 1] = ddec
coords[ix, iy, 2] = l
coords[ix, iy, 3] = b
return coords
coords = make_coords()
# now do one of these
# plt.imshow(coords[:,:,0],origin='lower') # color plot
plt.contour(
coords[:, :, 0], levels=[45, 90, 135, 180, 225, 270, 315]
) # contour plot with jagged ugliness
plt.show()
How can I either:
stop pyplot.contour from drawing a contour along the discontinuity
make pyplot.contour recognize that the 0/360 discontinuity in angle is not a real discontinuity at all.
I can just increase the resolution of the underlying data, but before I get a nice smooth line it starts to take a very long time and a lot of memory to plot.
I will also want to plot a contour along 0 degrees, but if I can figure out how to hide the discontinuity I can just shift it to somewhere else not near a contour. Or, if I can make #2 happen, it won't be an issue.
This is definitely still a hack, but you can get nice smooth contours with a two-fold approach:
Plot contours of the absolute value of the phase (going from -180˚ to 180˚) so that there is no discontinuity.
Plot two sets of contours in a finite region so that numerical defects close to the tops and bottoms of the extrema do not creep in.
Here is the complete code to append to your example:
Z = np.exp(1j*np.pi*coords[:,:,0]/180.0)
Z *= np.exp(0.25j*np.pi/2.0) # Shift to get same contours as in your example
X = np.arange(300)
Y = np.arange(450)
N = 2
levels = 90*(0.5 + (np.arange(N) + 0.5)/N)
c1 = plt.contour(X, Y, abs(np.angle(Z)*180/np.pi), levels=levels)
c2 = plt.contour(X, Y, abs(np.angle(Z*np.exp(0.5j*np.pi))*180/np.pi), levels=levels)
One can generalize this code to get smooth contours for any "periodic" function. What is left to be done is to generate a new set of contours with the correct values so that colormaps apply correctly, labels will be applied correctly etc. However, there does not seem to be a simple way of doing this with matplotlib: the relevant QuadContourSet class does everything and I do not see a simple way of constructing an appropriate contour object from the contours c1 and c2.
I was interested in the exact same problem. One solution is to NaN out the contours along the branch cut; see here; another is to use the max_jump argument in matplotx's contour().
I molded the solution into a Python package, cplot.
import cplot
import numpy as np
def f(z):
return np.exp(1 / z)
cplot.show(f, (-1.0, +1.0, 400), (-1.0, +1.0, 400))