I am using the Zelle graphics package in Python to draw the iterations of a cantor ternary like IFS (iterated function system) graph.
The iterated function system I want to draw is:
{R;x/9+1/6,2/3+x/9,x/9+1/3,x/9+5/6}
The following code is an imitation of what I found on the Internet. Is the code correct? How do I get each line segment to have a different color, and how do I label the points in each stage?
from graphics import *
def cantor_set(win,x,y,h,Len):
if Len < 2: return
line = Line(Point(x, y), Point(x+Len, y))
line.setWidth(20)
line.draw(win)
cantor_set(win,2*x+3,y+h,h,Len//18)
cantor_set(win,2*x+12,y+h,h,Len//18)
cantor_set(win,2*x+6,y+h,h,Len//18)
cantor_set(win,2*x+15,y+h,h,Len//18)
def cantor_set_starter():
Len = 790
win = GraphWin("Cantor Set", 800, 200)
c = cantor_set(win,5,20,50,790)
win.postscript(file = "can1.eps")
#win.getMouse()
#win.close()
cantor_set_starter()
There are two major types of errors in programming, syntax and algorithm. Your program suffers from both. We can fix your syntax errors fairly easily:
from graphics import *
def cantor_set(win, x, y, h, length):
if length < 2:
return
line = Line(Point(x, y), Point(x + length, y))
line.setWidth(20)
line.draw(win)
cantor_set(win, 2 * x + 3, y + h, h, length // 18)
cantor_set(win, 2 * x + 12, y + h, h, length // 18)
cantor_set(win, 2 * x + 6, y + h, h, length // 18)
cantor_set(win, 2 * x + 15, y + h, h, length // 18)
def cantor_set_starter(length):
cantor_set(win, 5, 20, 50, length)
win = GraphWin("Cantor Set", 800, 200)
cantor_set_starter(790)
win.getMouse()
win.close()
But since there are algorithm errors, it doesn't draw a cantor ternary-like graph. To help you fix these, we need to know more about where you got the the iterated function system equation and other details. If the goal is simply to draw a Cantor Set, we can modify your program to do so, mostly by throwing away code and adjusting the math on what's left:
from graphics import *
def cantor_set(win, x, y, height, length):
if length < 2:
return
line = Line(Point(x, y), Point(x + length, y))
line.setWidth(height / 2)
line.draw(win)
length /= 3
cantor_set(win, x, y + height, height, length)
cantor_set(win, x + 2 * length, y + height, height, length)
win = GraphWin("Cantor Set", 800, 300)
cantor_set(win, 5, 20, 50, 790)
win.getMouse()
win.close()
Related
Heyy, i have to create a function but I’m not sure how exactly to do it so any help would be appreciated. This is what I have to do:
Create a function that takes a string, integer x, integer of width, integer of height and integer of depth as inputs. This same function should return the x’th part of the string. If x isn’t valid it should return an empty string.
So for example:
>>>function(‘v..xXXv..vVX.XX..v’, 0, 3, 3, 2)
‘ v..xXXv..’
>>>function(‘v..xXXv..vVX.XX..v’, 1, 3, 3, 2)
‘vVX.XX..v’
I tried doing the following:
def function(s, w, h, d):
print(s[n*w: (n+1)*w*h])
This worked for the first example but not for the second, and also did not use the input of depth. How would I go about fixing this and creating this function?
Try:
def string_splitter(string, x, width, height, depth):
if x < 1 or x > width * height * depth:
return ''
else:
return string[(x - 1) * (len(string) // (width * height * depth)) : x * (len(string) // (width * height * depth))]
I made a Fourier Series/Transform Tkinter app, and so far everything works as I want it to, except that I am having issues with the circles misaligning.
Here is an image explaining my issue (the green and pink were added after the fact to better explain the issue):
I have narrowed down the problem to the start of the lines, as it seems that they end in the correct place, and the circles are in their correct places.
The distance between the correct positions and the position where the lines start seems to grow, but is actually proportional to the speed of the circle rotating, as the circle rotates by larger amounts, thus going faster.
Here is the code:
from tkinter import *
import time
import math
import random
root = Tk()
myCanvas = Canvas(root, width=1300, height=750)
myCanvas.pack()
myCanvas.configure(bg="#0A2239")
global x,y, lines, xList, yList
NumOfCircles = 4
rList = [200]
n=3
for i in range(0, NumOfCircles):
rList.append(rList[0]/n)
n=n+2
print(rList)
num = 250/sum(rList)
for i in range(0, NumOfCircles):
rList[i] = rList[i]*num
x=0
y=0
lines = []
circles = []
centerXList = [300]
for i in range(0,NumOfCircles):
centerXList.append(0)
centerYList = [300]
for i in range(0,NumOfCircles):
centerYList.append(0)
xList = [0]*NumOfCircles
yList = [0]*NumOfCircles
waveLines = []
wavePoints = []
con=0
endCoord = []
for i in range(0, NumOfCircles):
endCoord.append([0,0])
lastX = 0
lastY = 0
count = 0
randlist = []
n=1
for i in range(0, NumOfCircles):
randlist.append(200/n)
n=n+2
def createCircle(x, y, r, canvasName):
x0 = x - r
y0 = y - r
x1 = x + r
y1 = y + r
return canvasName.create_oval(x0, y0, x1, y1, width=r/50, outline="#094F9A")
def updateCircle(i):
newX = endCoord[i-1][0]
newY = endCoord[i-1][1]
centerXList[i] = newX
centerYList[i] = newY
x0 = newX - rList[i]
y0 = newY - rList[i]
x1 = newX + rList[i]
y1 = newY + rList[i]
myCanvas.coords(circles[i], x0, y0, x1, y1)
def circleWithLine(i):
global line, lines
circle = createCircle(centerXList[i], centerYList[i], rList[i], myCanvas)
circles.append(circle)
line = myCanvas.create_line(centerXList[i], centerYList[i], centerXList[i], centerYList[i], width=2, fill="#1581B7")
lines.append(line)
def update(i, x, y):
endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
myCanvas.coords(lines[i], x, y, endCoord[i][0], endCoord[i][1])
xList[i] += (math.pi/randlist[i])
yList[i] += (math.pi/randlist[i])
def lineBetweenTwoPoints(x, y, x2, y2):
line = myCanvas.create_line(x, y, x2, y2, fill="white")
return line
def lineForWave(y1, y2, y3, y4, con):
l = myCanvas.create_line(700+con, y1, 702+con, y2, 704+con, y3, 706+con, y4, smooth=1, fill="white")
waveLines.append(l)
for i in range(0,NumOfCircles):
circleWithLine(i)
myCanvas.create_line(700, 20, 700, 620, fill="black", width = 3)
myCanvas.create_line(700, 300, 1250, 300, fill="red")
myCanvas.create_line(0, 300, 600, 300, fill="red", width = 0.5)
myCanvas.create_line(300, 0, 300, 600, fill="red", width = 0.5)
while True:
for i in range(0, len(lines)):
update(i, centerXList[i], centerYList[i])
for i in range(1, len(lines)):
updateCircle(i)
if count >= 8:
lineBetweenTwoPoints(lastX, lastY, endCoord[i][0], endCoord[i][1])
if count % 6 == 0 and con<550:
lineForWave(wavePoints[-7],wavePoints[-5],wavePoints[-3],wavePoints[-1], con)
con += 6
wavePoints.append(endCoord[i][1])
myCanvas.update()
lastX = endCoord[i][0]
lastY = endCoord[i][1]
if count != 108:
count += 1
else:
count = 8
time.sleep(0.01)
root.mainloop()
I am aware that this is not the best way to achieve what I am trying to achieve, as using classes would be much better. I plan to do that in case nobody can find a solution, and hope that when it is re-written, this issue does not persist.
The main problem that you are facing is that you receive floating point numbers from your calculations but you can only use integers for pixels. In the following I will show you where you fail and the quickest way to solve the issue.
First your goal is to have connected lines and you calculate the points here:
def update(i, x, y):
endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
myCanvas.coords(lines[i], x, y, endCoord[i][0], endCoord[i][1])
xList[i] += (math.pi/randlist[i])
yList[i] += (math.pi/randlist[i])
when you add the following code into this function you see that it fails there.
if i != 0:
print(i,x,y)
print(i,endCoord[i-1][0], endCoord[i-1][1])
Because x and y should always match with the last point (end of the previous line) that will be endCoord[i-1][0] and endCoord[i-1][1].
to solve your problem I simply skipt the match for the sarting point of the follow up lines and took the coordinates of the previous line with the following alternated function:
def update(i, x, y):
endCoord[i][0] = x+(rList[i]*math.cos(xList[i]))
endCoord[i][1] = y+(rList[i]*math.sin(yList[i]))
if i == 0:
points = x, y, endCoord[i][0], endCoord[i][1]
else:
points = endCoord[i-1][0], endCoord[i-1][1], endCoord[i][0], endCoord[i][1]
myCanvas.coords(lines[i], *points)
xList[i] += (math.pi/randlist[i])
yList[i] += (math.pi/randlist[i])
Additional proposals are:
don't use wildcard imports
import just what you really use in the code random isnt used in your example
the use of global in the global namespace is useless
create functions to avoid repetitive code
def listinpt_times_circles(inpt):
return [inpt]*CIRCLES
x_list = listinpt_times_circles(0)
y_list = listinpt_times_circles(0)
center_x_list = listinpt_times_circles(0)
center_x_list.insert(0,300)
center_y_list = listinpt_times_circles(0)
center_y_list.insert(0,300)
use .after(ms,func,*args) instead of a interrupting while loop and blocking call time.sleep
def animate():
global count,con,lastX,lastY
for i in range(0, len(lines)):
update(i, centerXList[i], centerYList[i])
for i in range(1, len(lines)):
updateCircle(i)
if count >= 8:
lineBetweenTwoPoints(lastX, lastY, endCoord[i][0], endCoord[i][1])
if count % 6 == 0 and con<550:
lineForWave(wavePoints[-7],wavePoints[-5],wavePoints[-3],wavePoints[-1], con)
con += 6
wavePoints.append(endCoord[i][1])
myCanvas.update_idletasks()
lastX = endCoord[i][0]
lastY = endCoord[i][1]
if count != 108:
count += 1
else:
count = 8
root.after(10,animate)
animate()
root.mainloop()
read the PEP 8 -- Style Guide for Python
use intuitive variable names to make your code easier to read for others and yourself in the future
list_of_radii = [200] #instead of rList
as said pixels will be expressed with integers not with floating point numbers
myCanvas.create_line(0, 300, 600, 300, fill="red", width = 1) #0.5 has no effect compare 0.1 to 1
using classes and a canvas for each animation will become handy if you want to show more cycles
dont use tkinters update method
As #Thingamabobs said, the main reason for the misalignment is that pixel coordinates work with integer values. I got excited about your project and decided to make an example using matplotlib, this way I do not have to work with integer values for the coordinates. The example was made to work with any function, I implemented samples with sine, square and sawtooth functions.
I also tried to follow some good practices for naming, type annotations and so on, I hope this helps you
from numbers import Complex
from typing import Callable, Iterable, List
import matplotlib.pyplot as plt
import numpy as np
def fourier_series_coeff_numpy(f: Callable, T: float, N: int) -> List[Complex]:
"""Get the coefficients of the Fourier series of a function.
Args:
f (Callable): function to get the Fourier series coefficients of.
T (float): period of the function.
N (int): number of coefficients to get.
Returns:
List[Complex]: list of coefficients of the Fourier series.
"""
f_sample = 2 * N
t, dt = np.linspace(0, T, f_sample + 2, endpoint=False, retstep=True)
y = np.fft.fft(f(t)) / t.size
return y
def evaluate_fourier_series(coeffs: List[Complex], ang: float, period: float) -> List[Complex]:
"""Evaluate a Fourier series at a given angle.
Args:
coeffs (List[Complex]): list of coefficients of the Fourier series.
ang (float): angle to evaluate the Fourier series at.
period (float): period of the Fourier series.
Returns:
List[Complex]: list of complex numbers representing the Fourier series.
"""
N = np.fft.fftfreq(len(coeffs), d=1/len(coeffs))
N = filter(lambda x: x >= 0, N)
y = 0
radius = []
for n, c in zip(N, coeffs):
r = 2 * c * np.exp(1j * n * ang / period)
y += r
radius.append(r)
return radius
def square_function_factory(period: float):
"""Builds a square function with given period.
Args:
period (float): period of the square function.
"""
def f(t):
if isinstance(t, Iterable):
return [1.0 if x % period < period / 2 else -1.0 for x in t]
elif isinstance(t, float):
return 1.0 if t % period < period / 2 else -1.0
return f
def saw_tooth_function_factory(period: float):
"""Builds a saw-tooth function with given period.
Args:
period (float): period of the saw-tooth function.
"""
def f(t):
if isinstance(t, Iterable):
return [1.0 - 2 * (x % period / period) for x in t]
elif isinstance(t, float):
return 1.0 - 2 * (t % period / period)
return f
def main():
PERIOD = 1
GRAPH_RANGE = 3.0
N_COEFFS = 30
f = square_function_factory(PERIOD)
# f = lambda t: np.sin(2 * np.pi * t / PERIOD)
# f = saw_tooth_function_factory(PERIOD)
coeffs = fourier_series_coeff_numpy(f, 1, N_COEFFS)
radius = evaluate_fourier_series(coeffs, 0, 1)
fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True, figsize=(10, 5))
ang_cum = []
amp_cum = []
for ang in np.linspace(0, 2*np.pi * PERIOD * 3, 200):
radius = evaluate_fourier_series(coeffs, ang, 1)
x = np.cumsum([x.imag for x in radius])
y = np.cumsum([x.real for x in radius])
x = np.insert(x, 0, 0)
y = np.insert(y, 0, 0)
axs[0].plot(x, y)
axs[0].set_ylim(-GRAPH_RANGE, GRAPH_RANGE)
axs[0].set_xlim(-GRAPH_RANGE, GRAPH_RANGE)
ang_cum.append(ang)
amp_cum.append(y[-1])
axs[1].plot(ang_cum, amp_cum)
axs[0].axhline(y=y[-1],
xmin=x[-1] / (2 * GRAPH_RANGE) + 0.5,
xmax=1.2,
c="black",
linewidth=1,
zorder=0,
clip_on=False)
min_x, max_x = axs[1].get_xlim()
line_end_x = (ang - min_x) / (max_x - min_x)
axs[1].axhline(y=y[-1],
xmin=-0.2,
xmax=line_end_x,
c="black",
linewidth=1,
zorder=0,
clip_on=False)
plt.pause(0.01)
axs[0].clear()
axs[1].clear()
if __name__ == '__main__':
main()
I am having trouble writing to an image with my Mandelbrot program; it is to do with rounding errors with "(maxx-minx)/width" compared to "(maxy-miny)/width" line 31, which leads to a 501 by 500 picture rather than a 500 square picture.
((width+1) * length) not (width * length).
How can I fix this?
from PIL import Image
from cmath import *
from math import sqrt
from numpy import arange
width = 500
height = 500
minx = -0.752 #float(input("Please enter the minimum x value:"))
maxx = -0.748 #float(input("Please enter the maximum x value:"))
miny = 0.098 #float(input("Please enter the minimum y value:"))
maxy = 0.102 #float(input("Please enter the maximum y value:"))
gradient = Image.open("mandelbrot.png")
gradlist = list(gradient.getdata())
def testMandelbrot(x, y):
z = 0 + 0j
c = x + (y*1j)
iter = 0
while iter <= 69 and sqrt(z.real**2 + z.imag**2) < 4:
z = (z*z) + c
iter += 1
if iter == 70:
return (0, 0, 0, 255)
else:
return gradlist[int((iter - 1) * 140 / 70)]
img = Image.new('RGBA', (width, height), color=(255, 255, 255, 255))
image = [testMandelbrot(x, y) for y in arange(miny, maxy, (maxy-miny)/height) for x in arange(minx, maxx, (maxx-minx)/width)] #this line creates the error ((maxx-minx)/width) / (maxx - min) gives (width+1) not width
print(len(image), img.size)
img.putdata(image)
img.save("picture111.png", "PNG")
I'd suggest using numpy's linspace rather than arange. It will return an array of exactly the given number of evenly spaced samples.
See that linspace(0.098, 0.102, 500, endpoint=False) is exactly 500 points long. If you want to include the endpoint, you can leave out endpoint=False or pass endpoint=True.
With endpoint=False if you generate another image with the same size height and width but offset by difference between max_ - min_ the result would be the adjacent tile depending on which it would result in one of the eight.
Your code would then be:
Y = linspace(miny, maxy, height, endpoint=False)
X = linspace(minx, maxx, width, endpoint=False)
image = [testMandelbrot(x, y) for y in Y for x in X]
I name the arrays because Y is reused len(X) times and because height and width are small (500), it doesn't cost much and aids readability.
I have attempted creating a diamond square algorithm based on this javascript because it is readable and makes sense too me. I am having a few issues that I cannot seem to solve however.
When running the code, the desired output is some random value populating each position of the 2D array that is different from the initial array initialization. The issue that I am having is that the result 2D array isn't being completely populated and when increasing the grid size above 3, I get a IndexError: list index out of range error.
Here is the code:
class DiamondSquare:
def __init__(self, size, roughness):
self.size = (size ** 2) + 1
self.max = self.size - 1
self.roughness = roughness
self.grid = self.make_grid(self.size)
self.divide(self.max)
print(self.grid)
def divide(self, size):
x = size / 2
y = size / 2
half = size / 2
scale = self.roughness * size
if (half < 1):
return
# Squares
for y in range(half, self.max, size):
for x in range(half, self.max, size):
s_scale = random.uniform(0, 1) * scale * 2 - scale
self.square(x, y, half, s_scale)
# Diamonds
for y in range(0, self.max, half):
for x in range((y + half) % size, self.max, size):
d_scale = random.uniform(0, 1) * scale * 2 - scale
self.diamond(x, y, half, d_scale)
self.divide(size / 2)
def square(self, x, y, size, scale):
"""
TL TR
X
BL BR
"""
tl = self.grid[x - size][y - size]
tr = self.grid[x + size][y - size]
bl = self.grid[x + size][y + size]
br = self.grid[x - size][y + size]
average = ((tl + tr + bl + br) / 4) + scale
self.grid[x][y] = average
def diamond(self, x, y, size, scale):
"""
T
L X R
B
"""
t = self.grid[x][y - size]
r = self.grid[x + size][y]
b = self.grid[x][y + size]
l = self.grid[x - size][y + size]
average = ((t + r + b + l) / 4) + scale
self.grid[x][y] = average
def make_grid(self, size):
grid = []
for y in range(size):
temp = []
for x in range(size):
temp.append(-1)
grid.append(temp)
grid[0][0] = self.max
grid[self.max][0] = self.max / 2
grid[self.max][self.max] = 0
grid[0][self.max] = self.max / 2
return grid
def get_grid(self):
return self.grid
When trying to increase the size variable (in init) to a value above 2, I get the following traceback:
Traceback (most recent call last):
File "C:\Users\user\Documents\Development\Python\Fractal\diamond_square.py", line 150, in <module>
a = DiamondSquare(5, 0.7)
File "C:\Users\user\Documents\Development\Python\Fractal\diamond_square.py", line 14, in __init__
self.divide(self.max)
File "C:\Users\user\Documents\Development\Python\Fractal\diamond_square.py", line 35, in divide
self.diamond(x, y, half, d_scale)
File "C:\Users\user\Documents\Development\Python\Fractal\diamond_square.py", line 68, in diamond
r = self.grid[x + size][y]
IndexError: list index out of range
I am honestly not sure why this is happening and I can't figure it out at all. I am using the following code to produce this error:
a = DiamondSquare(x, 0.7)
Where x is any integer above 2 and the second parameter is the roughness.
Also regarding the grid error, trying to create a grid from DiamondSquare.divide(), produces the following:
[[4, 1.0649105908291359, 1.234026481506731, 0.07818244918327344, 2],
[0.43855022217756057, 0.4659935454877355, 1.283183468707215, 0.28019876872734906, -1],
[-0.4946413746345607, -1.1327574166276582, 0.45804405178511276, -1.4905717022572778, -1],
[-1.4175095415414622, -0.660055583070249, -0.8017056243549873, -0.18216161649389495, -1],
[2, -1, -1, -1, 0]]
Where the -1's are, there should be other random numbers as with the rest of the grid. The -1's make up the mid point of the bottom and the right hand side. I believe it is something to do with my diamonds for loop but I am not sure where I am going wrong.
I achieve this grid, I use the following code:
a = DiamondSquare(2, 0.7)
where the first parameter is the size and the second is the roughness.
Could I please have some help resolving the issues stated above? Thank you in advance!
You changed the place of the base and the power when you create the size parameter. You wrote (size ** 2) + 1 but it should be (2 ** size) + 1. This will hopefully solve your problems.
I managed to solve it by removing the 2D list and simply using a 1D list instead which is what I should have done in the first place but I didn't because I misread the original code.
class DiamondSquare:
def __init__(self, size, roughness):
self.size = (2 ** size) + 1
self.max = self.size - 1
self.roughness = roughness
self.make_grid(self.size)
self.divide(self.max)
# Sets x,y position in self.grid
def set(self, x, y, val):
self.grid[x + self.size * y] = val;
# Get's value of x, y in self.grid
def get(self, x, y):
if (x < 0 or x > self.max or y < 0 or y > self.max):
return -1
return self.grid[x + self.size * y]
def divide(self, size):
x = size / 2
y = size / 2
half = size / 2
scale = self.roughness * size
if (half < 1):
return
# Square
for y in range(half, self.max, size):
for x in range(half, self.max, size):
s_scale = random.uniform(0, 1) * scale * 2 - scale
self.square(x, y, half, s_scale)
# Diamond
for y in range(0, self.max + 1, half):
for x in range((y + half) % size, self.max + 1, size):
d_scale = random.uniform(0, 1) * scale * 2 - scale
self.diamond(x, y, half, d_scale)
self.divide(size / 2)
def square(self, x, y, size, scale):
top_left = self.get(x - size, y - size)
top_right = self.get(x + size, y - size)
bottom_left = self.get(x + size, y + size)
bottom_right = self.get(x - size, y + size)
average = ((top_left + top_right + bottom_left + bottom_right) / 4)
self.set(x, y, average + scale)
def diamond(self, x, y, size, scale):
"""
T
L X R
B
"""
top = self.get(x, y - size)
right = self.get(x + size, y)
bottom = self.get(x, y + size)
left = self.get(x - size, y)
average = ((top + right + bottom + left) / 4)
self.set(x, y, average + scale)
def make_grid(self, size):
self.grid = []
for x in range(size * size):
self.grid.append(-1)
self.set(0, 0, self.max)
self.set(self.max, 0, self.max /2 )
self.set(self.max, self.max, 0)
self.set(0, self.max, self.max / 2)
def get_grid(self):
return self.grid
a = DiamondSquare(3, 0.5)
print(a.get_grid())
Is there a way to draw direction fields in python?
My attempt is to modify http://www.compdigitec.com/labs/files/slopefields.py giving
#!/usr/bin/python
import math
from subprocess import CalledProcessError, call, check_call
def dy_dx(x, y):
try:
# declare your dy/dx here:
return x**2-x-2
except ZeroDivisionError:
return 1000.0
# Adjust window parameters
XMIN = -5.0
XMAX = 5.0
YMIN = -10.0
YMAX = 10.0
XSCL = 0.5
YSCL = 0.5
DISTANCE = 0.1
def main():
fileobj = open("data.txt", "w")
for x1 in xrange(int(XMIN / XSCL), int(XMAX / XSCL)):
for y1 in xrange(int(YMIN / YSCL), int(YMAX / YSCL)):
x= float(x1 * XSCL)
y= float(y1 * YSCL)
slope = dy_dx(x,y)
dx = math.sqrt( DISTANCE/( 1+math.pow(slope,2) ) )
dy = slope*dx
fileobj.write(str(x) + " " + str(y) + " " + str(dx) + " " + str(dy) + "\n")
fileobj.close()
try:
check_call(["gnuplot","-e","set terminal png size 800,600 enhanced font \"Arial,12\"; set xrange [" + str(XMIN) + ":" + str(XMAX) + "]; set yrange [" + str(YMIN) + ":" + str(YMAX) + "]; set output 'output.png'; plot 'data.txt' using 1:2:3:4 with vectors"])
except (CalledProcessError, OSError):
print "Error: gnuplot not found on system!"
exit(1)
print "Saved image to output.png"
call(["xdg-open","output.png"])
return 0
if __name__ == '__main__':
main()
However the best image I get from this is.
How can I get an output that looks more like the first image? Also, how can I add the three solid lines?
You can use this matplotlib code as a base. Modify it for your needs.
I have updated the code to show same length arrows. The important option is to set the angles option of the quiver function, so that the arrows are correctly printed from (x,y) to (x+u,y+v) (instead of the default, which just takes into account of (u,v) when computing the angles).
It is also possible to change the axis form "boxes" to "arrows". Let me know if you need that change and I could add it.
import matplotlib.pyplot as plt
from scipy.integrate import odeint
import numpy as np
fig = plt.figure()
def vf(x, t):
dx = np.zeros(2)
dx[0] = 1.0
dx[1] = x[0] ** 2 - x[0] - 2.0
return dx
# Solution curves
t0 = 0.0
tEnd = 10.0
# Vector field
X, Y = np.meshgrid(np.linspace(-5, 5, 20), np.linspace(-10, 10, 20))
U = 1.0
V = X ** 2 - X - 2
# Normalize arrows
N = np.sqrt(U ** 2 + V ** 2)
U = U / N
V = V / N
plt.quiver(X, Y, U, V, angles="xy")
t = np.linspace(t0, tEnd, 100)
for y0 in np.linspace(-5.0, 0.0, 10):
y_initial = [y0, -10.0]
y = odeint(vf, y_initial, t)
plt.plot(y[:, 0], y[:, 1], "-")
plt.xlim([-5, 5])
plt.ylim([-10, 10])
plt.xlabel(r"$x$")
plt.ylabel(r"$y$")
I had a lot of fun making one of these as a hobby project using pygame. I plotted the slope at each pixel, using shades of blue for positive and shades of red for negative. Black is for undefined. This is dy/dx = log(sin(x/y)+cos(y/x)):
You can zoom in & out - here is zoomed in on the middle upper part here:
and also click on a point to graph the line going through that point:
It's just 440 lines of code, so here is the .zip of all the files. I guess I'll excerpt relevant bits here.
The equation itself is input as a valid Python expression in a string, e.g. "log(sin(x/y)+cos(y/x))". This is then compiled. This function here graphs the color field, where self.func.eval() gives the dy/dx at the given point. The code is a bit complicated here because I made it render in stages - first 32x32 blocks, then 16x16, etc. - to make it snappier for the user.
def graphcolorfield(self, sqsizes=[32,16,8,4,2,1]):
su = ScreenUpdater(50)
lastskip = self.xscreensize
quitit = False
for squaresize in sqsizes:
xsquaresize = squaresize
ysquaresize = squaresize
if squaresize == 1:
self.screen.lock()
y = 0
while y <= self.yscreensize:
x = 0
skiprow = y%lastskip == 0
while x <= self.xscreensize:
if skiprow and x%lastskip==0:
x += squaresize
continue
color = (255,255,255)
try:
m = self.func.eval(*self.ct.untranscoord(x, y))
if m >= 0:
if m < 1:
c = 255 * m
color = (0, 0, c)
else:
#c = 255 - 255 * (1.0/m)
#color = (c, c, 255)
c = 255 - 255 * (1.0/m)
color = (c/2.0, c/2.0, 255)
else:
pm = -m
if pm < 1:
c = 255 * pm
color = (c, 0, 0)
else:
c = 255 - 255 * (1.0/pm)
color = (255, c/2.0, c/2.0)
except:
color = (0, 0, 0)
if squaresize > 1:
self.screen.fill(color, (x, y, squaresize, squaresize))
else:
self.screen.set_at((x, y), color)
if su.update():
quitit = True
break
x += xsquaresize
if quitit:
break
y += ysquaresize
if squaresize == 1:
self.screen.unlock()
lastskip = squaresize
if quitit:
break
This is the code which graphs a line through a point:
def _grapheqhelp(self, sx, sy, stepsize, numsteps, color):
x = sx
y = sy
i = 0
pygame.draw.line(self.screen, color, (x, y), (x, y), 2)
while i < numsteps:
lastx = x
lasty = y
try:
m = self.func.eval(x, y)
except:
return
x += stepsize
y = y + m * stepsize
screenx1, screeny1 = self.ct.transcoord(lastx, lasty)
screenx2, screeny2 = self.ct.transcoord(x, y)
#print "(%f, %f)-(%f, %f)" % (screenx1, screeny1, screenx2, screeny2)
try:
pygame.draw.line(self.screen, color,
(screenx1, screeny1),
(screenx2, screeny2), 2)
except:
return
i += 1
stx, sty = self.ct.transcoord(sx, sy)
pygame.draw.circle(self.screen, color, (int(stx), int(sty)), 3, 0)
And it runs backwards & forwards starting from that point:
def graphequation(self, sx, sy, stepsize=.01, color=(255, 255, 127)):
"""Graph the differential equation, given the starting point sx and sy, for length
length using stepsize stepsize."""
numstepsf = (self.xrange[1] - sx) / stepsize
numstepsb = (sx - self.xrange[0]) / stepsize
self._grapheqhelp(sx, sy, stepsize, numstepsf, color)
self._grapheqhelp(sx, sy, -stepsize, numstepsb, color)
I never got around to drawing actual lines because the pixel approach looked too cool.
Try changing your values for the parameters to this:
XSCL = .2
YSCL = .2
These parameters determine how many points are sampled on the axes.
As per your comment, you'll need to also plot the functions for which the derivation dy_dx(x, y) applies.
Currently, you're only calculating and plotting the slope lines as calculated by your function dy_dx(x,y). You'll need to find (in this case 3) functions to plot in addition to the slope.
Start by defining a function:
def f1_x(x):
return x**3-x**2-2x;
and then, in your loop, you'll have to also write the desired values for the functions into the fileobj file.