Simulation with RK4, update ODE variable as the simulation goes - python

Problem :
I’m currently making a python app that simulates a set of coupled ordinary differentials equations that depends on a variable, let’s call it « X ».
As for now, I’m basically simulating this set of ODE with RK4 for given time then I’m plotting the graph on an animated plot with « matplotlib animation » embedded in tkinter.
The fact is that I would like to be able to modify « X » as the equations are resolved so that the simulation can change as we modify this variable.
Context :
The solution changes from that time on. To put things in context, these are the equations for the xenon and iodine abundance as well as neutrons flow in a nuclear reactor. The variable that change is "big sigma_b" that governs the control bars.
ODE :
xenon and iodine abundance ODE :
neutrons flow ODE :
Summary :
To summarize, « X » belongs to the range [1.0, 2.0], let’s say we want to run 400 hours of simulations :
We launch the simulation of ODE
We update the animated plot every second (which is equal to 1 hour of simulation on the graph)
We modify « X » with the help of a slider for instance (in fact "X" is not directly modified, there is a dynamic process that progressively move the initial "X" value toward its chosen value).
The RK4 algorithm take that into account
We can see that change on the updated graph
etc…
When the simulation is finished, we stop the plot updating
Hopefully, it’s clear enough.
How could I do that ?

Translating the ideas of the comments into a mockup gives some code to experiment with
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider
import matplotlib.animation as anim
import numpy as np
from scipy.integrate import odeint
# model is dampened oscillation with control variable u
# representing the equilibrium position
# x'' + 0.4*x + x == u
def model(x,u): return np.array([x[1], u - 0.4*x[1] - x[0]])
# integrate over about 10 periods
N=250
t = np.linspace(0, 12*np.pi, N+1) # time
# arrays to hold the computed values
X = np.zeros([N+1,2])
U = np.zeros([N+1])
# initial values
X[0] = [3, 2]
U[0] = 2;
# initialize plot
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.25, bottom=0.25)
# initialize graphs
l1, = plt.plot(t[0], X[0,0], '-+b', ms=2 )
l2, = plt.plot(t[0], U[0], '-or', ms=2, lw=1 )
plt.xlim(t[0], t[-1]); plt.ylim(-1,3)
# construct slider
axcolor = 'black'
ax_U = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
sU = Slider(ax_U, 'U', 1.0, 2.0, valinit=U[0])
def animate(i):
# read the slider value
U[i] = sU.val
# integrate over the sub-interval
X[i] = odeint(lambda x,t: model(x,U[i]), X[i-1], [t[i-1],t[i]], atol=1e-4, rtol=1e-8)[-1];
# set the data to plot
l1.set_data(t[0:i+1],X[0:i+1,0]);
l2.set_data(t[0:i+1],U[0:i+1]);
return l1,l2
# start the animation, one should set the parameter to keep it from repeating
anim = anim.FuncAnimation(fig, animate, frames = range(1,N+1), blit=True, interval = 100 )
plt.show()

Related

Runge Kutta program outputting incorrect graphs

I'm creating a program to compare two graphs, using the Runge Kutta method to solve an ODE. Ideally the resulting plots would be directly on top of each other, but the Runge Kutta function is outputting an incorrect plot. Am I incorrectly populating the array, or is it a problem with calling the new values?
Here is the program I have:
#Import correct libraries and extensions
import numpy as np
from scipy.integrate import odeint
from matplotlib import pyplot as plt
import warnings
def fxn():
warnings.warn("deprecated", DeprecationWarning)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
fxn()
#Define conditions and store values
h=0.02 #Given values for ODE
x0=1
y0=5
xpoints=[x0] #array for storing X values
ypoints=[y0] #Array for storing Y values
#State equations and define functions
def dy_dx(y,x):
return x / (np.exp(x) - 1) #Provided Equation
def RungeKuttaFehlberg(x,y):
return x / (np.exp(x) - 1)
#Calculates k1-k4, x and y solutions
def RKFAlg(x0,y0,h):
k1 = RungeKuttaFehlberg(x0,y0)
k2 = RungeKuttaFehlberg(x0+(h/2),y0+((h/2)*k1))
k3 = RungeKuttaFehlberg(x0+(h/2),y0+((h/2)*k2))
k4 = RungeKuttaFehlberg(x0+h,y0+(h*k3))
y1 = y0+(h/6)*(k1+(2*k2)+(2*k3)+k4)
x1 = x0+h
x1 = round(x1,2)
print("Point (x(n+1),y(n+1)) =",(x1,y1))
return((x1,y1)) #Returns as ordered pair
#Define range for number of calculations
for i in range(2000):
print(f"Y{i+1}".format(i)) #Solution value format
x0,y0 = RKFAlg(x0,y0,h) #Calls RKF Function
xpoints.append(x0) #Saves values into array
ypoints.append(y0)
y0 = 1
ODEy1 = odeint(dy_dx,y0,xpoints)
#Runge-Kutta Graph
plt.plot(xpoints,ypoints,'b:',linewidth = 1) #command to plot lines using various colors and widths
plt.suptitle("RKF Graph")
plt.xlabel("x Points")
plt.ylabel("y Points")
plt.show()
#ODE graph
plt.plot(xpoints,ODEy1,'g-',linewidth=1)
plt.suptitle("ODE Graph")
plt.xlabel("x Points")
plt.ylabel("y Points")
plt.show()
#Function for plotting RKF and ODE graph
plt.plot(xpoints,ODEy1,'g-',linewidth=2,label="ODE")
plt.plot(xpoints,ypoints,'b:',linewidth=3,label="Runge-Kutta")
plt.suptitle("ODE and RKF Comparison")
plt.legend(bbox_to_anchor=(.8,1),loc=0,borderaxespad=0)
plt.xlabel("X")
plt.ylabel("Y")
plt.show()
#Function for plotting the difference graph
diff = [] #array to store difference
for i in range(len(xpoints)):
diff.append(ypoints[i]-ODEy1[i])
plt.plot(xpoints,diff)
plt.suptitle("Difference")
plt.xlabel("x Points")
plt.ylabel("RKF and ODE diff.")
plt.show()
In python indentation defines the structure of a program, what the command block is inside a loop and what comes after the loop.
Your aim is to iterate through the time interval with steps h building the arrays for x and y. After this loop you want to call odeint once to get reference values over the x array.
Thus change this part to
#Define range for number of calculations
x,y = x0,y0 # for systems use y0.copy()
for i in range(2000):
print(f"Y{i+1}".format(i)) #Solution value format
x,y = RKFAlg(x,y,h) #Calls RKF Function
xpoints.append(x) #Saves values into array
ypoints.append(y)
ODEy1 = odeint(dy_dx,ypoints[0],xpoints)
For consistency it would be better to keep the interfaces of the integrators close,
def RK4Alg(func,x0,y0,h):
k1 = func(x0,y0)
...
Then you can pass the same rhs function dy_dx to both methods, and can remove the mis-named RungeKuttaFehlberg.
For a sensible reference solution one should set the error tolerances to values that are decisively lower than the expected errors of the method to test.
opts = {"atol":1e-8, "rtol":1e-11}
ODEy1 = odeint(dy_dx,y0,xpoints,**opts)

Plotting subplots from a nested for loop in Python

I am trying to plot line plots(Drifted brownian motion) for different values of mu and sigma, I have a function that iterates a list of possible mu values and possible sigma values and it's supposed to then return the resulting plots. The problem is I am unsure how to make the subplots return the required number of rows. I have given it the correct nrows and ncols but the problem comes in with the indexing. Does anyone have a trick to solve this?
I have provided the code and the error message below,
# Drifted BM for varying values mu and sigma respectively
def DriftedBMTest2(nTraj=50,T=5.0,dt=0.01,n=5, sigma = [0.1,1.0,2], mulist=[0,0.5,1,1.5], ValFSize=(18,14)):
nMu = len(mulist)
nSigma = len(mulist)
# Discretize, dt = time step = $t_{j+1}- t_{j}$
dt = T/(n-1)
# Loop on different value sigma
for z in range(nSigma):
# Loop on different value Mu
for k in range(nMu):
n=int(T/dt)
x=np.zeros(n+1,float)
# Create plot space
temp = nSigma*nMu/2
plt.subplot(temp,2,k+1)
plt.title("Drifted BM $\sigma$={}, $\mu$={}".format(sigma[z],mulist[k]))
plt.xlabel(r'$t$')
plt.ylabel(r'$W_t$');
# Container for colours for each trajectory
colors = plt.cm.jet(np.linspace(0,1,nTraj))
# Generate many trajectories
for j in range(nTraj):
# Time simulation
# Add the time * constant(mu)
for i in range(n):
x[i+1]=x[i]+np.sqrt(dt)*np.random.randn() + i*mulist[k]
# Scale Each Tradjectory
x = x * sigma[z]
# Plot trajectory just computed
plt.plot(np.linspace(0,T,n+1),x,'b-',alpha=0.3, color=colors[j], lw=3.0)
DriftedBMTest2( sigma = [1,2], mulist=[-2,1] )
I then get the first two plots but not all of them and the error below.
MatplotlibDeprecationWarning: Adding an axes using the same arguments as a previous axes currently reuses the earlier instance. In a future version, a new instance will always be created and returned. Meanwhile, this warning can be suppressed, and the future behavior ensured, by passing a unique label to each axes instance.
Sorry if this is a bad question, I am new to Python but any help would be appreciated.
Try adding fig = plt.figure() between the two for loops
for z in range(nSigma):
# Loop on different value Mu
fig = plt.figure() # <---- Line added here
for k in range(nMu):
If that doesn't give the desired layout, you can try moving it to the inner for loop as
for z in range(nSigma):
# Loop on different value Mu
for k in range(nMu):
fig = plt.figure() # <---- Line added here

Plotting in matplotlib

I wrote the following code,
import numpy as np
from random import gauss
from random import seed
from pandas import Series
import matplotlib.pyplot as plt
import math
###variable declaration
R=0.000001 #radiaus of particle
beta=0.23 # shape factor
Ad=9.2#characteristic nanoscale defect area
gamma=4*10**-2 #surface tension
tau=.0001 #line tension
phi=-(math.pi/2)#spatial perturbation
lamda=((Ad)/(2*3.14*R)) #averge contcat line position
mu=0.001#viscosity of liquid
lamda_m=10**-9# characteristic size of adsorption site
KbT=(1.38**-24)*293 # boltzman constant with tempartaure
nu=0.001#moleculer volume in liquid phase 1
khi=3 #scaling factor
#deltaF=(beta*gamma*Ad)#surface energy perturbation
deltaF=19*KbT
# seed random number generator
seed(0)
# create white noise series
series = [gauss(0.0, 1.0) for i in range(1)]
series = Series(series)
#########################################
Z=0.0000001 #particle position
time=1
dt=1
for time in np.arange(1, 100, dt):
#####simulation loop#######
theta=np.arccos(-Z/R) #contact angle
theta_e=((math.pi*110)/180) #equilibrium contact angle
Z_e=-R*np.cos(theta_e)#equilibrium position of particle
C=3.14*gamma*(R-Z_e) #additive constant
Fsz= (gamma*math.pi*(Z-Z_e)**2)+(tau*2*math.pi*math.sqrt(R**2-Z**2))+C
Fz=Fsz+(0.5*deltaF*np.sin((2*math.pi/lamda)*(Z-Z_e)-phi))#surface force
#dFz=(((gamma*Ad)/2)*np.sin(2*math.pi/lamda))+((Z-Z_e)*(2*gamma*math.pi))-((tau*2*math.pi*Z)/(math.sqrt(R**2-Z**2)))
dFz=(deltaF*np.sin(2*math.pi/lamda))+((Z-Z_e)*(2*gamma*math.pi))-((tau*2*math.pi*Z)/(math.sqrt(R**2-Z**2)))
w_a=gamma*lamda_m**2*(1-np.cos(theta_e)) #work of adhesion
epsilon_z=2*math.pi*R*np.sin(theta)*mu*(nu/(lamda_m**3))*np.exp(w_a/KbT)#transitional drag
epsilon_s=khi*mu*((4*math.pi**2*R**2)/math.sqrt(Ad))*(1-(Z/R)**2)
epsilon=epsilon_z+epsilon_s
Ft=math.sqrt(2*KbT*epsilon)*series #thermal force
v=(dFz+Ft)/epsilon ##new velocity
Z=Z+v*dt #new position
print('z=',Z)
print('v=',v)
print('Fz=',Fz)
print('dFz',dFz)
print('time',time)
plt.plot(Z,time)
plt.show()
According to my code I suppose to have 99 values for everything (Fz, Z, v , time). While I print , I can see all the values but while I was trying to plot them with different parameters with respect to each other for analyzing, I never get any graph. Can anyone tell me, what is missing from my code with explanation?
#AnttiA's answer is basically correct, but can be easily misunderstood, as can be seen from the OP's comment. Therefore here the complete code altered such that a plot is actually produced. Instead of making Z a list, define another variable as list, say Z_all = [], and then append the updated Z-values to that list. The same can be done for the time variable, i.e. time_all = np.arange(1,100,dt). Finally, take the plot command out of the loop and plot the entire data series at once.
Note that in your example you don't really have a series of random numbers, you pull one fixed number for one fixed seed, thus the plot is not really meaningful (it appears to be producing a straight line). Trying to interpret your intentions correctly, you probably want a series of random numbers that is as long as your time series. This is most easily done using np.random.normal
There are a lot of other ways that your code could be optimised. For instance, all mathematical functions from the math module are also found in numpy, so you could just not import math at all. The same goes for pandas. Also, you define some constant values inside the for-loop, which could be computed once before the loop. Lastly, #AnttiA is probably right, that you want time on the x axis and Z on the y axis. I therefore generate two plots -- on the left time over Z and on the right Z over time. Now finally the altered code:
import numpy as np
#from random import gauss
#from random import seed
#from pandas import Series
import matplotlib.pyplot as plt
#import math
###variable declaration
R=0.000001 #radiaus of particle
beta=0.23 # shape factor
Ad=9.2#characteristic nanoscale defect area
gamma=4*10**-2 #surface tension
tau=.0001 #line tension
phi=-(np.pi/2)#spatial perturbation
lamda=((Ad)/(2*3.14*R)) #averge contcat line position
mu=0.001#viscosity of liquid
lamda_m=10**-9# characteristic size of adsorption site
KbT=(1.38**-24)*293 # boltzman constant with tempartaure
nu=0.001#moleculer volume in liquid phase 1
khi=3 #scaling factor
#deltaF=(beta*gamma*Ad)#surface energy perturbation
deltaF=19*KbT
##quantities moved out of the for-loop:
theta_e=((np.pi*110)/180) #equilibrium contact angle
Z_e=-R*np.cos(theta_e)#equilibrium position of particle
C=3.14*gamma*(R-Z_e) #additive constant
w_a=gamma*lamda_m**2*(1-np.cos(theta_e)) #work of adhesion
#########################################
Z=0.0000001 #particle position
##time=1
dt=1
Z_all = []
time_all = np.arange(1, 100, dt)
# seed random number generator
# seed(0)
np.random.seed(0)
# create white noise series
##series = [gauss(0.0, 1.0) for i in range(1)]
##series = Series(series)
series = np.random.normal(0.0, 1.0, len(time_all))
for time, S in zip(time_all,series):
#####simulation loop#######
Z_all.append(Z)
theta=np.arccos(-Z/R) #contact angle
Fsz= (gamma*np.pi*(Z-Z_e)**2)+(tau*2*np.pi*np.sqrt(R**2-Z**2))+C
Fz=Fsz+(0.5*deltaF*np.sin((2*np.pi/lamda)*(Z-Z_e)-phi))#surface force
#dFz=(((gamma*Ad)/2)*np.sin(2*np.pi/lamda))+((Z-Z_e)*(2*gamma*np.pi))-((tau*2*np.pi*Z)/(np.sqrt(R**2-Z**2)))
dFz=(deltaF*np.sin(2*np.pi/lamda))+((Z-Z_e)*(2*gamma*np.pi))-((tau*2*np.pi*Z)/(np.sqrt(R**2-Z**2)))
epsilon_z=2*np.pi*R*np.sin(theta)*mu*(nu/(lamda_m**3))*np.exp(w_a/KbT)#transitional drag
epsilon_s=khi*mu*((4*np.pi**2*R**2)/np.sqrt(Ad))*(1-(Z/R)**2)
epsilon=epsilon_z+epsilon_s
Ft=np.sqrt(2*KbT*epsilon)*S #series #thermal force
v=(dFz+Ft)/epsilon ##new velocity
Z=Z+v*dt #new position
print('z=',Z)
print('v=',v)
print('Fz=',Fz)
print('dFz',dFz)
print('time',time)
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(8,4))
axes[0].plot(Z_all,time_all)
axes[0].set_xlabel('Z')
axes[0].set_ylabel('t')
axes[1].plot(time_all, Z_all)
axes[1].set_xlabel('t')
axes[1].set_ylabel('Z')
fig.tight_layout()
plt.show()
The result looks like this:
I suppose, you will get plot anyway, y-values are probably from 94 to 104.
Now you are plotting line with one point. Its length is zero, that's why you cannot see it, try: plt.plot(Z,time,'*').
Now you should get graph with an asterix in the middle.
As Thomas suggested, you should use arrays instead of using last calculated value. If you prefer loops (sometimes they are easier to modify), modify the lines...
Before loop:
Z = [0.0000001] # Initialize Z for time 0
time_vec = np.arange(1, 100, dt)
Inside loop:
Z.append(Z[-1] + v*dt) # new position
After loop:
plt.plot(Z[1:], time_vec)
Have no time to test it, hopefully works...
Note that first argument in plot command is x-axis values and second y-axis, I'd prefer time in x-axis.

How to remove/omit smaller contour lines using matplotlib

I am trying to plot contour lines of pressure level. I am using a netCDF file which contain the higher resolution data (ranges from 3 km to 27 km). Due to higher resolution data set, I get lot of pressure values which are not required to be plotted (rather I don't mind omitting certain contour line of insignificant values). I have written some plotting script based on the examples given in this link http://matplotlib.org/basemap/users/examples.html.
After plotting the image looks like this
From the image I have encircled the contours which are small and not required to be plotted. Also, I would like to plot all the contour lines smoother as mentioned in the above image. Overall I would like to get the contour image like this:-
Possible solution I think of are
Find out the number of points required for plotting contour and mask/omit those lines if they are small in number.
or
Find the area of the contour (as I want to omit only circled contour) and omit/mask those are smaller.
or
Reduce the resolution (only contour) by increasing the distance to 50 km - 100 km.
I am able to successfully get the points using SO thread Python: find contour lines from matplotlib.pyplot.contour()
But I am not able to implement any of the suggested solution above using those points.
Any solution to implement the above suggested solution is really appreciated.
Edit:-
# Andras Deak
I used print 'diameter is ', diameter line just above del(level.get_paths()[kp]) line to check if the code filters out the required diameter. Here is the filterd messages when I set if diameter < 15000::
diameter is 9099.66295612
diameter is 13264.7838257
diameter is 445.574234531
diameter is 1618.74618114
diameter is 1512.58974168
However the resulting image does not have any effect. All look same as posed image above. I am pretty sure that I have saved the figure (after plotting the wind barbs).
Regarding the solution for reducing the resolution, plt.contour(x[::2,::2],y[::2,::2],mslp[::2,::2]) it works. I have to apply some filter to make the curve smooth.
Full working example code for removing lines:-
Here is the example code for your review
#!/usr/bin/env python
from netCDF4 import Dataset
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
import numpy as np
import scipy.ndimage
from mpl_toolkits.basemap import interp
from mpl_toolkits.basemap import Basemap
# Set default map
west_lon = 68
east_lon = 93
south_lat = 7
north_lat = 23
nc = Dataset('ncfile.nc')
# Get this variable for later calucation
temps = nc.variables['T2']
time = 0 # We will take only first interval for this example
# Draw basemap
m = Basemap(projection='merc', llcrnrlat=south_lat, urcrnrlat=north_lat,
llcrnrlon=west_lon, urcrnrlon=east_lon, resolution='l')
m.drawcoastlines()
m.drawcountries(linewidth=1.0)
# This sets the standard grid point structure at full resolution
x, y = m(nc.variables['XLONG'][0], nc.variables['XLAT'][0])
# Set figure margins
width = 10
height = 8
plt.figure(figsize=(width, height))
plt.rc("figure.subplot", left=.001)
plt.rc("figure.subplot", right=.999)
plt.rc("figure.subplot", bottom=.001)
plt.rc("figure.subplot", top=.999)
plt.figure(figsize=(width, height), frameon=False)
# Convert Surface Pressure to Mean Sea Level Pressure
stemps = temps[time] + 6.5 * nc.variables['HGT'][time] / 1000.
mslp = nc.variables['PSFC'][time] * np.exp(9.81 / (287.0 * stemps) * nc.variables['HGT'][time]) * 0.01 + (
6.7 * nc.variables['HGT'][time] / 1000)
# Contour only at 2 hpa interval
level = []
for i in range(mslp.min(), mslp.max(), 1):
if i % 2 == 0:
if i >= 1006 and i <= 1018:
level.append(i)
# Save mslp values to upload to SO thread
# np.savetxt('mslp.txt', mslp, fmt='%.14f', delimiter=',')
P = plt.contour(x, y, mslp, V=2, colors='b', linewidths=2, levels=level)
# Solution suggested by Andras Deak
for level in P.collections:
for kp,path in enumerate(level.get_paths()):
# include test for "smallness" of your choice here:
# I'm using a simple estimation for the diameter based on the
# x and y diameter...
verts = path.vertices # (N,2)-shape array of contour line coordinates
diameter = np.max(verts.max(axis=0) - verts.min(axis=0))
if diameter < 15000: # threshold to be refined for your actual dimensions!
#print 'diameter is ', diameter
del(level.get_paths()[kp]) # no remove() for Path objects:(
#level.remove() # This does not work. produces ValueError: list.remove(x): x not in list
plt.gcf().canvas.draw()
plt.savefig('dummy', bbox_inches='tight')
plt.close()
After the plot is saved I get the same image
You can see that the lines are not removed yet. Here is the link to mslp array which we are trying to play with http://www.mediafire.com/download/7vi0mxqoe0y6pm9/mslp.txt
If you want x and y data which are being used in the above code, I can upload for your review.
Smooth line
You code to remove the smaller circles working perfectly. However the other question I have asked in the original post (smooth line) does not seems to work. I have used your code to slice the array to get minimal values and contoured it. I have used the following code to reduce the array size:-
slice = 15
CS = plt.contour(x[::slice,::slice],y[::slice,::slice],mslp[::slice,::slice], colors='b', linewidths=1, levels=levels)
The result is below.
After searching for few hours I found this SO thread having simmilar issue:-
Regridding regular netcdf data
But none of the solution provided over there works.The questions similar to mine above does not have proper solutions. If this issue is solved then the code is perfect and complete.
General idea
Your question seems to have 2 very different halves: one about omitting small contours, and another one about smoothing the contour lines. The latter is simpler, since I can't really think of anything else other than decreasing the resolution of your contour() call, just like you said.
As for removing a few contour lines, here's a solution which is based on directly removing contour lines individually. You have to loop over the collections of the object returned by contour(), and for each element check each Path, and delete the ones you don't need. Redrawing the figure's canvas will get rid of the unnecessary lines:
# dummy example based on matplotlib.pyplot.clabel example:
import matplotlib
import numpy as np
import matplotlib.cm as cm
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
# difference of Gaussians
Z = 10.0 * (Z2 - Z1)
plt.figure()
CS = plt.contour(X, Y, Z)
for level in CS.collections:
for kp,path in reversed(list(enumerate(level.get_paths()))):
# go in reversed order due to deletions!
# include test for "smallness" of your choice here:
# I'm using a simple estimation for the diameter based on the
# x and y diameter...
verts = path.vertices # (N,2)-shape array of contour line coordinates
diameter = np.max(verts.max(axis=0) - verts.min(axis=0))
if diameter<1: # threshold to be refined for your actual dimensions!
del(level.get_paths()[kp]) # no remove() for Path objects:(
# this might be necessary on interactive sessions: redraw figure
plt.gcf().canvas.draw()
Here's the original(left) and the removed version(right) for a diameter threshold of 1 (note the little piece of the 0 level at the top):
Note that the top little line is removed while the huge cyan one in the middle doesn't, even though both correspond to the same collections element i.e. the same contour level. If we didn't want to allow this, we could've called CS.collections[k].remove(), which would probably be a much safer way of doing the same thing (but it wouldn't allow us to differentiate between multiple lines corresponding to the same contour level).
To show that fiddling around with the cut-off diameter works as expected, here's the result for a threshold of 2:
All in all it seems quite reasonable.
Your actual case
Since you've added your actual data, here's the application to your case. Note that you can directly generate the levels in a single line using np, which will almost give you the same result. The exact same can be achieved in 2 lines (generating an arange, then selecting those that fall between p1 and p2). Also, since you're setting levels in the call to contour, I believe the V=2 part of the function call has no effect.
import numpy as np
import matplotlib.pyplot as plt
# insert actual data here...
Z = np.loadtxt('mslp.txt',delimiter=',')
X,Y = np.meshgrid(np.linspace(0,300000,Z.shape[1]),np.linspace(0,200000,Z.shape[0]))
p1,p2 = 1006,1018
# this is almost the same as the original, although it will produce
# [p1, p1+2, ...] instead of `[Z.min()+n, Z.min()+n+2, ...]`
levels = np.arange(np.maximum(Z.min(),p1),np.minimum(Z.max(),p2),2)
#control
plt.figure()
CS = plt.contour(X, Y, Z, colors='b', linewidths=2, levels=levels)
#modified
plt.figure()
CS = plt.contour(X, Y, Z, colors='b', linewidths=2, levels=levels)
for level in CS.collections:
for kp,path in reversed(list(enumerate(level.get_paths()))):
# go in reversed order due to deletions!
# include test for "smallness" of your choice here:
# I'm using a simple estimation for the diameter based on the
# x and y diameter...
verts = path.vertices # (N,2)-shape array of contour line coordinates
diameter = np.max(verts.max(axis=0) - verts.min(axis=0))
if diameter<15000: # threshold to be refined for your actual dimensions!
del(level.get_paths()[kp]) # no remove() for Path objects:(
# this might be necessary on interactive sessions: redraw figure
plt.gcf().canvas.draw()
plt.show()
Results, original(left) vs new(right):
Smoothing by resampling
I've decided to tackle the smoothing problem as well. All I could come up with is downsampling your original data, then upsampling again using griddata (interpolation). The downsampling part could also be done with interpolation, although the small-scale variation in your input data might make this problem ill-posed. So here's the crude version:
import scipy.interpolate as interp #the new one
# assume you have X,Y,Z,levels defined as before
# start resampling stuff
dN = 10 # use every dN'th element of the gridded input data
my_slice = [slice(None,None,dN),slice(None,None,dN)]
# downsampled data
X2,Y2,Z2 = X[my_slice],Y[my_slice],Z[my_slice]
# same as X2 = X[::dN,::dN] etc.
# upsampling with griddata over original mesh
Zsmooth = interp.griddata(np.array([X2.ravel(),Y2.ravel()]).T,Z2.ravel(),(X,Y),method='cubic')
# plot
plt.figure()
CS = plt.contour(X, Y, Zsmooth, colors='b', linewidths=2, levels=levels)
You can freely play around with the grids used for interpolation, in this case I just used the original mesh, as it was at hand. You can also play around with different kinds of interpolation: the default 'linear' one will be faster, but less smooth.
Result after downsampling(left) and upsampling(right):
Of course you should still apply the small-line-removal algorithm after this resampling business, and keep in mind that this heavily distorts your input data (since if it wasn't distorted, then it wouldn't be smooth). Also, note that due to the crude method used in the downsampling step, we introduce some missing values near the top/right edges of the region under consideraton. If this is a problem, you should consider doing the downsampling based on griddata as I've noted earlier.
This is a pretty bad solution, but it's the only one that I've come up with. Use the get_contour_verts function in this solution you linked to, possibly with the matplotlib._cntr module so that nothing gets plotted initially. That gives you a list of contour lines, sections, vertices, etc. Then you have to go through that list and pop the contours you don't want. You could do this by calculating a minimum diameter, for example; if the max distance between points is less than some cutoff, throw it out.
That leaves you with a list of LineCollection objects. Now if you make a Figure and Axes instance, you can use Axes.add_collection to add all of the LineCollections in the list.
I checked this out really quick, but it seemed to work. I'll come back with a minimum working example if I get a chance. Hope it helps!
Edit: Here's an MWE of the basic idea. I wasn't familiar with plt._cntr.Cntr, so I ended up using plt.contour to get the initial contour object. As a result, you end up making two figures; you just have to close the first one. You can replace checkDiameter with whatever function works. I think you could turn the line segments into a Polygon and calculate areas, but you'd have to figure that out on your own. Let me know if you run into problems with this code, but it at least works for me.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
def checkDiameter(seg, tol=.3):
# Function for screening line segments. NB: Not actually a proper diameter.
diam = (seg[:,0].max() - seg[:,0].min(),
seg[:,1].max() - seg[:,1].min())
return not (diam[0] < tol or diam[1] < tol)
# Create testing data
x = np.linspace(-1,1, 21)
xx, yy = np.meshgrid(x,x)
z = np.exp(-(xx**2 + .5*yy**2))
# Original plot with plt.contour
fig0, ax0 = plt.subplots()
# Make sure this contour object actually has a tiny contour to remove
cntrObj = ax0.contour(xx,yy,z, levels=[.2,.4,.6,.8,.9,.95,.99,.999])
# Primary loop: Copy contours into a new LineCollection
lineNew = list()
for lineOriginal in cntrObj.collections:
# Get properties of the original LineCollection
segments = lineOriginal.get_segments()
propDict = lineOriginal.properties()
propDict = {key: value for (key,value) in propDict.items()
if key in ['linewidth','color','linestyle']} # Whatever parameters you want to carry over
# Filter out the lines with small diameters
segments = [seg for seg in segments if checkDiameter(seg)]
# Create new LineCollection out of the OK segments
if len(segments) > 0:
lineNew.append(mpl.collections.LineCollection(segments, **propDict))
# Make new plot with only these line collections; display results
fig1, ax1 = plt.subplots()
ax1.set_xlim(ax0.get_xlim())
ax1.set_ylim(ax0.get_ylim())
for line in lineNew:
ax1.add_collection(line)
plt.show()
FYI: The bit with propDict is just to automate bringing over some of the line properties from the original plot. You can't use the whole dictionary at once, though. First, it contains the old plot's line segments, but you can just swap those for the new ones. But second, it appears to contain a number of parameters that are in conflict with each other: multiple linewidths, facecolors, etc. The {key for key in propDict if I want key} workaround is my way to bypass that, but I'm sure someone else can do it more cleanly.

Python Control Package - Increasing resolution of Nyquist plotting

I'm using Python Control Module to plot Bode and Nyquist diagrams of a transfer function. The code is as simple as follows:
# Simple Nyquist plotting
import control
import matplotlib.pyplot as plt
num = 5
den = [1,6,11,6]
#Creating a transfer function G = num/den
G = control.tf(num,den)
control.nyquist(G)
plt.grid(True)
plt.title('Nyquist Diagram of G(s) = 5/(s+1)(s+2)(s+3)')
plt.xlabel('Re(s)')
plt.ylabel('Im(s)')
plt.show()
The Nyquist diagram is plotted:
I wonder if it is possible to increase the graph of the number of points, improving its resolution.
Note that in the plot, all the data points are present. You just have to enlarge the window and you'll see all the points.
You can do that manually (just enlarging the plot window), or you can set the plot window in Matplotlib before showing the result:
If you've already got the figure created you can quickly do this:
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(18.5, 10.5)
fig.savefig('test2png.png', dpi=100)
To propagate the size change to an existing gui window add forward=True
fig.set_size_inches(18.5, 10.5, forward=True)
python-control library follows a matlab-like syntax so it is best to check first if it is possible to do it as it is in matlab. This time it is indeed. You can actually look at the function signature for hints.
For example in an IPython terminal if we type
cnt.nyquist?
we get
Signature: cnt.nyquist(syslist, omega=None, Plot=True, color='b', labelFreq=0, *args, **kwargs)
Docstring:
Nyquist plot for a system
Plots a Nyquist plot for the system over a (optional) frequency range.
Parameters
----------
syslist : list of Lti
List of linear input/output systems (single system is OK)
omega : freq_range
Range of frequencies (list or bounds) in rad/sec
Plot : boolean
If True, plot magnitude
labelFreq : int
Label every nth frequency on the plot
*args, **kwargs:
Additional options to matplotlib (color, linestyle, etc)
Returns
-------
real : array
real part of the frequency response array
imag : array
imaginary part of the frequency response array
freq : array
frequencies
Examples
--------
>>> sys = ss("1. -2; 3. -4", "5.; 7", "6. 8", "9.")
>>> real, imag, freq = nyquist_plot(sys)
File: c:\python34\lib\site-packages\control\freqplot.py
Type: function
So for your case it is simple to fix
num = 5
den = [1,6,11,6]
#Creating a transfer function G = num/den
G = control.tf(num,den)
w = numpy.logspace(-3,3,5000)
control.nyquist(G,w);

Categories