Efficient function mapping with arguments in numpy - python

I am trying to create a heightmap by interpolating between a bunch of heights at certain points in an area. To process the whole image, I have the following code snippet:
map_ = np.zeros((img_width, img_height))
for x in range(img_width):
for y in range(img_height):
map_[x, y] = calculate_height(set(points.items()), x, y)
This is calculate_height:
def distance(x1, y1, x2, y2) -> float:
return np.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
def calculate_height(points: set, x, y) -> float:
total = 0
dists = {}
for pos, h in points:
d = distance(pos[0], pos[1], x, y)
if x == pos[0] and y == pos[1]:
return h
d = 1 / (d ** 2)
dists[pos] = d
total += d
r = 0
for pos, h in points:
ratio = dists[pos] / total
r += ratio * h
r = r
return r
This snippet works perfectly, but if the image is too big, it takes a long time to process, because this is O(n^2). The problem with this, is that a "too big" image is 800 600, and it takes almost a minute to process, which to me seems a bit excessive.
My goal is not to reduce the time complexity from O(n^2), but to reduce the time it takes to process images, so that I can get decently sized images in a reasonable amount of time.
I found
this post, but I couldn't really try it out because it's all for a 1D array, and I have a 2D array, and I also need to pass the coordinates of each point and the set of existing points to the calculate_height function. What can I try to optimize this code snippet?
Edit: Moving set(points.items) out of the loop as #thethiny suggested was a HUGE improvement. I had no idea it was such a heavy thing to do. This makes it fast enough for me, but feel free to add more suggestions for the next people to come by!
Edit 2: I have further optimized this processing by including the following changes:
# The first for loop inside the calculate_distance function
for pos, h in points:
d2 = distance2(pos[0], pos[1], x, y)
if x == pos[0] and y == pos[1]:
return h
d2 = d2 ** -1 # 1 / (d ** 2) == d ** -2 == d2 ** -1
dists[pos] = d2 # Having the square of d on these two lines makes no difference
total += d2
This reduced execution time for a 200x200 image from 1.57 seconds to 0.76 seconds. The 800x600 image mentioned earlier now takes 6.13 seconds to process :D
This is what points looks like (as requested by #norok12):
# Hints added for easier understanding, I know this doesn't run
points: dict[tuple[int, int], float] = {
(x: int, y: int): height: float,
(x: int, y: int): height: float,
(x: int, y: int): height: float
}
# The amount of points varies between datasets, so I can't provide a useful range other than [3, inf)

There's a few problems with your implementation.
Essentially what you're implementing is approximation using radial basis functions.
The usual algorithm for that looks like:
sum_w = 0
sum_wv = 0
for p,v in points.items():
d = distance(p,x)
w = 1.0 / (d*d)
sum_w += w
sum_wv += w*v
return sum_wv / sum_w
Your code has some extra logic for bailing out if p==x - which is good.
But it also allocates an array of distances - which this single loop form does not need.
This brings execution of an example in a workbook from 13s to 12s.
The next thing to note is that collapsing the points dict into an numpy array gives us the chance to use numpy functions.
points_array = np.array([(p[0][0],p[0][1],p[1]) for p in points.items()]).astype(np.float32)
Then we can write the function as
def calculate_height_fast(points, x, y) -> float:
dx = points[:,0] - x
dy = points[:,1] - y
r = np.hypot(dx,dy)
w = 1.0 / (r*r)
sum_w = np.sum(w)
return np.sum(points[:,2] * w) / np.sum(w)
This brings our time down to 658ms. But we can do better yet...
Since we're now using numpy functions we can apply numba.njit to JIT compile our function.
#numba.njit
def calculate_height_fast(points, x, y) -> float:
dx = points[:,0] - x
dy = points[:,1] - y
r = np.hypot(dx,dy)
w = 1.0 / (r*r)
sum_w = np.sum(w)
return np.sum(points[:,2] * w) / np.sum(w)
This was giving me 105ms (after the function had been run once to ensure it got compiled).
This is a speed up of 130x over the original implementation (for my data)
You can see the full implementations here

This really is a small addition to #MichaelAnderson's detailed answer.
Probably calculate_height_fast() can get faster by optimizing a bit more with explicit looping:
#numba.njit
def calculate_height_faster(points, x, y) -> float:
dx = points[:, 0] - x
dy = points[:, 1] - y
r = np.hypot(dx, dy)
# compute weighted average
n = r.size
sum_w = sum_wp = 0
for i in range(n):
w = 1.0 / (r[i] * r[i])
sum_w += w
sum_wp += points[i, 2] * w
return sum_wp / sum_w

Related

Python - Function return for a 2D array

I am converting an old Matlab code into a Python script before I lose access to my Matlab license.
The purpose of the Python code is to write a script that calculates an optimal I-beam geometry given a specified force requirement and distance from the force loading point.
When the function does not return anything and just prints 'vals' (i.e. print(vals)), the output shows me all possible combinations which is desirable. However when I return the vals array (i.e. return vals), the output is just 0.0. I am not too sure why I am unable to return vals.
Ideally I want to achieve a 2D array of all possible combinations of vals (it should be a 980x980 array), then extract the maximal value of vals using a different function call.
This is my code:
## Defining the system inputs
sigma = 100 # Max stress not allowed to exceed (N/mm^2)
F = 100 * 10**3 # Force applied ( N)
dist = [100, 1000, 2000, 3000, 4000] # Distance from force applied (mm)
t = 10 # Thickness of I-beam (mm)
y = np.zeros(1000 - 2*t) # Pre-allocating vector with zeroes
h = np.arange(1, 1001 - 2*t, 1) # Height of I-beam
w = np.arange(1, 1001 - 2*t, 1) # width of I-beam
tol = 2.00 # tolerance for the system (how close we want our desired shear modulus to be to the limit)
# Writing the IBeam function
def IBeam(sigma, F, t, h, w, dist, tol):
M = F * dist # calculating moment
zShear = M / sigma # calculating shear modulus based on given conditions
# iterating through all combinations of height and width
for i in range(0, len(h)): # iterating through the height vector
for j in range(0, len(w)): # iterating through the width vector
XSA = 2*t*w[j] + t*h[i] # calculating XSA for each h/w combination
Ix = ((t*h[i]**3)/12) + (w[j]/12)*((2*t+h[i])**3 - h[i]**3) # calculating Ix for each h/w combination
y = h[i]/2 + t # calculating y for each h combination
zInt = Ix / y # calculating zInt for each Ix/y combination
# Check to see if our zInt < zShear since this is not ideal
if (zInt <= zShear):
zInt = 0
# Check to see if our zInt >= tol * zShear since this is not ideal
if (zInt >= tol * zShear):
zInt = 0
# Creating the relationship between zInt and XSA
vals = zInt / XSA
# return vals
return vals
# Calling the IBeam and max function
iBeamVals = IBeam(sigma, F, t, h, w, dist[0], tol)
print(iBeamVals)
Apologies if this questions has been asked before or to a similar capacity beforehand.
Thanks
AJ

Access current time step in scipy.integrate.odeint within the function

Is there a way to access what the current time step is in scipy.integrate.odeint?
I am trying to solve a system of ODEs where the form of the ode depends on whether or not a population will be depleted. Basically I take from population x provided x doesn't go below a threshold. If the amount I need to take this timestep is greater than that threshold I will take all of x to that point and the rest from z.
I am trying to do this by checking how much I will take this time step, and then allocating between populations x and z in the DEs.
To do this I need to be able to access the step size within the ODE solver to calculate what will be taken this time step. I am using scipy.integrate.odeint - is there a way to access the time step within the function defining the odes?
Alternatively, can you access what the last time was in the solver? I know it won't necessarily be the next time step, but it's likely a good enough approximation for me if that is the best I can do. Or is there another option I've not thought of to do this?
The below MWE is not my system of equations but what I could come up with to try to illustrate what I'm doing. The problem is that on the first time step, if the time step were 1 then the population will go too low, but since the timestep will be small, initially you can take all from x.
import numpy as np
from scipy.integrate import odeint
import matplotlib.pyplot as plt
plt.interactive(False)
tend = 5
tspan = np.linspace(0.0, tend, 1000)
A = 3
B = 4.09
C = 1.96
D = 2.29
def odefunc(P,t):
x = P[0]
y = P[1]
z = P[2]
if A * x - B * x * y < 0.6:
dxdt = A/5 * x
dydt = -C * y + D * x * y
dzdt = - B * z * y
else:
dxdt = A * x - B * x * y
dydt = -C * y + D * x * y
dzdt = 0
dPdt = np.ravel([dxdt, dydt, dzdt])
return dPdt
init = ([0.75,0.95,100])
sol = odeint(odefunc, init, tspan, hmax = 0.01)
x = sol[:, 0]
y = sol[:, 1]
z = sol[:, 2]
plt.figure(1)
plt.plot(tspan,x)
plt.plot(tspan,y)
plt.plot(tspan,z)
Of course you can hack something together that might work.
You could log t but you have to be aware that the values
might not be constantly increasing. This depends on the ODE algorithm and how it works (forward, backward, and central finite differences).
But it will give you an idea where about you are.
logger = [] # visible in odefunc
def odefunc(P,t):
x = P[0]
y = P[1]
z = P[2]
print(t)
logger.append(t)
if logger: # if the list is not empty
if logger[-1] > 2.5: # then read the last value
print('hua!')
if A * x - B * x * y < 0.6:
dxdt = A/5 * x
dydt = -C * y + D * x * y
dzdt = - B * z * y
else:
dxdt = A * x - B * x * y
dydt = -C * y + D * x * y
dzdt = 0
dPdt = np.ravel([dxdt, dydt, dzdt])
return dPdt
print(logger)
As pointed out in the another answer, time may not be strictly increasing at each call to the ODE function in odeint, especially for stiff problems.
The most robust way to handle this kind of discontinuity in the ode function is to use an event to find the location of the zero of (A * x - B * x * y) - 0.6 in your example. For a discontinuous solution, use a terminal event to stop the computation precisely at the zero, and then change the ode function. In solve_ivp you can do this with the events parameter. See the solve ivp documentation and specifically the examples related to the cannonball trajectories. odeint does not support events, and solve_ivp has an LSODA method available that calls the same Fortran library as odeint.
Here is a short example, but you may want to additionally check that sol1 reached the terminal event before solving for sol2.
from scipy.integrate import solve_ivp
tend = 10
def discontinuity_zero(t, y):
return y[0] - 10
discontinuity_zero.terminal = True
def ode_func1(t, y):
return y
def ode_func2 (t, y):
return -y**2
sol1 = solve_ivp(ode_func1, t_span=[0, tend], y0=[1], events=discontinuity_zero, rtol=1e-8)
t1 = sol1.t[-1]
y1 = [sol1.y[0, -1]]
print(f'time={t1} y={y1} discontinuity_zero={discontinuity_zero(t1, y1)}')
sol2 = solve_ivp(ode_func2, t_span=[t1, tend], y0=y1, rtol=1e-8)
plt.plot(sol1.t, sol1.y[0,:])
plt.plot(sol2.t, sol2.y[0,:])
plt.show()
This prints the following, where the time of the discontinuity is accurate to 7 digits.
time=2.302584885712467 y=[10.000000000000002] discontinuity_zero=1.7763568394002505e-15

How to make a gif of mandelbrot fractal zoom (Python)?

I have made a Mandelbrot fractal using PIL module in Python.
Now, I want to make a GIF of zooming into one point. I have watched other code on-line, but needless to say, I didn't understand it, since the pattern I'm using is a bit different (I'm using classes).
I know that to zoom in I need to change scale... But I plainly don't know how to implement it.
from PIL import Image
import random
class Fractal:
"""Fractal class."""
def __init__(self, size, scale, computation):
"""Constructor.
Arguments:
size -- the size of the image as a tuple (x, y)
scale -- the scale of x and y as a list of 2-tuple
[(minimum_x, minimum_y), (maximum_x, maximum_y)]
computation -- the function used for computing pixel values as a function
"""
self.size = size
self.scale = scale
self.computation = computation
self.img = Image.new("RGB", (size[0], size[1]))
def compute(self):
"""
Create the fractal by computing every pixel value.
"""
for y in range(self.size[1]):
for x in range(self.size[0]):
i = self.pixel_value((x, y))
r = i % 8 * 32
g = i % 16 * 16
b = i % 32 * 8
self.img.putpixel((x, y), (r, g, b))
def pixel_value(self, pixel):
"""
Return the number of iterations it took for the pixel to go out of bounds.
Arguments:
pixel -- the pixel coordinate (x, y)
Returns:
the number of iterations of computation it took to go out of bounds as integer.
"""
# x = pixel[0] * (self.scale[1][0] - self.scale[0][0]) / self.size[0] + self.scale[0][0]
# y = pixel[1] * (self.scale[1][1] - self.scale[0][1]) / self.size[1] + self.scale[0][1]
x = (pixel[0] / self.size[0]) * (self.scale[1][0] - self.scale[0][0]) + self.scale[0][0]
y = (pixel[1] / self.size[1]) * (self.scale[1][1] - self.scale[0][1]) + self.scale[0][1]
return self.computation((x, y))
def save_image(self, filename):
"""
Save the image to hard drive.
Arguments:
filename -- the file name to save the file to as a string.
"""
self.img.save(filename, "PNG")
if __name__ == "__main__":
def mandelbrot_computation(pixel):
"""Return integer -> how many iterations it takes for the pixel to escape the mandelbrot set."""
c = complex(pixel[0], pixel[1]) # Complex number: A + Bi (A is real number, B is imaginary number).
z = 0 # We are assuming the starting z value for each square is 0.
iterations = 0 # Will count how many iterations it takes for a pixel to escape the mandelbrot set.
for i in range(255): # The more iterations, the more detailed the mandelbrot set will be.
if abs(z) >= 2.0: # Checks, if pixel escapes the mandelbrot set. Same as square root of pix[0] and pix[1].
break
z = z**2 + c
iterations += 1
return iterations
mandelbrot = Fractal((1000, 1000), [(-2, -2), (2, 2)], mandelbrot_computation())
mandelbrot.compute()
mandelbrot.save_image("mandelbrot.png")
This is a "simple" linear transformation, both scale (zoom) and translate (shift), as you learned in linear algebra. Do you recall a formula something like
s(y-k) = r(x-h) + c
The translation is (h, k); the scale in each direction is (r, s).
To implement this, you need to alter your loop increments. To zoom in a factor of k in each direction, you need to reduce the range of your coordinates, likewise reducing the increment between pixel positions.
The most important thing here is to partially decouple your display coordinates from your mathematical values: you no longer display the value for 0.2 - 0.5i at the location labeled (0.2, -0.5); the new position computes from your new frame bounds.
Your old code isn't quite right for this:
for y in range(self.size[1]):
for x in range(self.size[0]):
i = self.pixel_value((x, y))
...
self.img.putpixel((x, y), (r, g, b))
Instead, you'll need something like:
# Assume that the limits x_min, x_max, y_min, y_max
# are assigned by the zoom operation.
x_inc = (x_max - x_min) / self.size[0]
y_inc = (y_max - y_min) / self.size[1]
for y in range(self.size[1]):
for x in range(self.size[0]):
a = x*x_inc + x_min
b = y*y_inc + y_min
i = self.pixel_value((a, b))
...
self.img.putpixel((x, y), (r, g, b))

Optimizing by translation to map one x,y set of points onto another

I have a list of x,y ideal points, and a second list of x,y measured points. The latter has some offset and some noise.
I am trying to "fit" the latter to the former. So, extract the x,y offset of the latter relative to the former.
I'm following some examples of scipy.optimize.leastsq, but having trouble getting it working. Here is my code:
import random
import numpy as np
from scipy import optimize
# Generate fake data. Goal: Get back dx=0.1, dy=0.2 at the end of this exercise
dx = 0.1
dy = 0.2
# "Actual" (ideal) data.
xa = np.array([0,0,0,1,1,1])
ya = np.array([0,1,2,0,1,2])
# "Measured" (non-ideal) data. Add the offset and some randomness.
xm = map(lambda x: x + dx + random.uniform(0,0.01), xa)
ym = map(lambda y: y + dy + random.uniform(0,0.01), ya)
# Plot each
plt.figure()
plt.plot(xa, ya, 'b.', xm, ym, 'r.')
# The error function.
#
# Args:
# translations: A list of xy tuples, each xy tuple holding the xy offset
# between 'coords' and the ideal positions.
# coords: A list of xy tuples, each xy tuple holding the measured (non-ideal)
# coordinates.
def errfunc(translations, coords):
sum = 0
for t, xy in zip(translations, coords):
dx = t[0] + xy[0]
dy = t[1] + xy[1]
sum += np.sqrt(dx**2 + dy**2)
return sum
translations, coords = [], []
for xxa, yya, xxm, yym in zip(xa, ya, xm, ym):
t = (xxm-xxa, yym-yya)
c = (xxm, yym)
translations.append(t)
coords.append(c)
translation_guess = [0.05, 0.1]
out = optimize.leastsq(errfunc, translation_guess, args=(translations, coords), full_output=1)
print out
I get the error:
errfunc() takes exactly 2 arguments (3 given)"
I'm not sure why it says 3 arguments as I only gave it two. Can anyone help?
====
ANSWER:
I was thinking about this wrong. All I have to do is to take the average of the dx and dy's -- that gives the correct result.
n = xa.shape[0]
dx = -np.sum(xa - xm) / n
dy = -np.sum(ya - ym) / n
print dx, dy
The scipy.optimize.leastsq assumes that the function you are using already has one input, x0, the initial guess. Any other additional inputs are then listed in args.
So you are sending three arguments: translation_guess, transactions, and coords.
Note that here it specifies that args are "extra arguments."
Okay, I think I understand now. You have the actual locations and the measured locations and you want to figure out the constant offset, but there is noise on each pair. Correct me if I'm wrong:
xy = tuple with coordinates of measured point
t = tuple with measured offset (constant + noise)
The actual coordinates of a point are (xy - t) then?
If so, then we think it should be measured at (xy - t + guess).
If so, then our error is (xy - t + guess - xy) = (guess - t)
Where it is measured doesn't even matter! We just want to find the guess that is closest to all of the measured translations:
def errfunc(guess, translations):
errx = 0
erry = 0
for t in translations:
errx += guess[0] - t[0]
erry += guess[1] - t[1]
return errx,erry
What do you think? Does that make sense or did I miss something?

Ising Model in Python

I'm currently working on writing code for the Ising Model using Python3. I'm still pretty new to coding. I have working code, but the output result is not as expected and I can't seem to find the error. Here is my code:
import numpy as np
import random
def init_spin_array(rows, cols):
return np.random.choice((-1, 1), size=(rows, cols))
def find_neighbors(spin_array, lattice, x, y):
left = (x , y - 1)
right = (x, y + 1 if y + 1 < (lattice - 1) else 0)
top = (x - 1, y)
bottom = (x + 1 if x + 1 < (lattice - 1) else 0, y)
return [spin_array[left[0], left[1]],
spin_array[right[0], right[1]],
spin_array[top[0], top[1]],
spin_array[bottom[0], bottom[1]]]
def energy(spin_array, lattice, x ,y):
return -1 * spin_array[x, y] * sum(find_neighbors(spin_array, lattice, x, y))
def main():
lattice = eval(input("Enter lattice size: "))
temperature = eval(input("Enter the temperature: "))
sweeps = eval(input("Enter the number of Monte Carlo Sweeps: "))
spin_array = init_spin_array(lattice, lattice)
print("Original System: \n", spin_array)
# the Monte Carlo follows below
for sweep in range(sweeps):
for i in range(lattice):
for j in range(lattice):
e = energy(spin_array, lattice, i, j)
if e <= 0:
spin_array[i, j] *= -1
elif np.exp(-1 * e/temperature) > random.randint(0, 1):
spin_array[i, j] *= -1
else:
continue
print("Modified System: \n", spin_array)
main()
I think the error is in the Monte Carlo Loop, but I am not sure. The system should be highly ordered at low temperatures and become disordered past the critical temperature of 2.27. In other words, the randomness of the system should increase as T approaches 2.27. For example, at T=.1, we should see large patches of spins that are aligned, i.e. patches of -1s and 1s. Past 2.27 the system should be disordered and we should not see these patches.
Your question would make much more sense if you were to include the system size, the number of sweeps, and the average manetisation. How many of the intermediate configurations are ordered and how many disordered? MC is a sampling technique - individual configurations mean nothing and there might (and will) be disordered states at low temperature and ordered states at high T. It is the assembly properties (the average magnetisation) that is meaningful.
Anyway, there are three errors in your code: a small one, a medium one, and a really severe one.
The small one is that you are ignoring an entire row and an entire column while searching for neighbours in find_neighbors:
right = (x, y + 1 if y + 1 < (lattice - 1) else 0)
should be:
right = (x, y + 1 if y + 1 < lattice else 0)
or even better:
right = (x, (y + 1) % lattice)
Same applies to bottom.
The medium one is that your computation of the energy difference is off by a factor of two:
def energy(spin_array, lattice, x ,y):
return -1 * spin_array[x, y] * sum(find_neighbors(spin_array, lattice, x, y))
^^
The factor is actually 2*J, where J is the coupling constant, therefore having -1 there means:
your critical temperature is halved, and more importantly...
you have antiferromagnetic spin interaction (J < 0), so no ordered states for you even at very low temperatures.
The worst mistake however is the use of random.randint() for the rejection sampling:
elif np.exp(-1 * e/temperature) > random.randint(0, 1):
spin_array[i, j] *= -1
You should be using random.random() instead, otherwise the transition probability will always be 50%.
Here is a modification of your program that automatically sweeps over the temperature region from 0.1 to 5.0:
import numpy as np
import random
def init_spin_array(rows, cols):
return np.ones((rows, cols))
def find_neighbors(spin_array, lattice, x, y):
left = (x, y - 1)
right = (x, (y + 1) % lattice)
top = (x - 1, y)
bottom = ((x + 1) % lattice, y)
return [spin_array[left[0], left[1]],
spin_array[right[0], right[1]],
spin_array[top[0], top[1]],
spin_array[bottom[0], bottom[1]]]
def energy(spin_array, lattice, x ,y):
return 2 * spin_array[x, y] * sum(find_neighbors(spin_array, lattice, x, y))
def main():
RELAX_SWEEPS = 50
lattice = eval(input("Enter lattice size: "))
sweeps = eval(input("Enter the number of Monte Carlo Sweeps: "))
for temperature in np.arange(0.1, 5.0, 0.1):
spin_array = init_spin_array(lattice, lattice)
# the Monte Carlo follows below
mag = np.zeros(sweeps + RELAX_SWEEPS)
for sweep in range(sweeps + RELAX_SWEEPS):
for i in range(lattice):
for j in range(lattice):
e = energy(spin_array, lattice, i, j)
if e <= 0:
spin_array[i, j] *= -1
elif np.exp((-1.0 * e)/temperature) > random.random():
spin_array[i, j] *= -1
mag[sweep] = abs(sum(sum(spin_array))) / (lattice ** 2)
print(temperature, sum(mag[RELAX_SWEEPS:]) / sweeps)
main()
And the result for 20x20 and 100x100 lattices and 100 sweeps:
The starting configuration is a completely ordered one to prevent the development of domain walls that are very stable at low temperatures. Also, 30 additional sweeps are performed initially in order to thermalise the system (not nearly enough when close to the critical temperature, but the Metropolis-Hastings algorithm cannot properly handle the critical slowdown there anyway).

Categories

Resources