I'm trying to make particles attract each other in Python. It works a bit but they always move to the top-left corner (0;0).
A year ago, CodeParade released a video about a game of life he made with particles. I thought it was cool and wanted to recreate it myself in Python. It wasn't that difficult but I have a problem. Every time some particles are near enough to attract each other, they get a bit closer but at the same time they "run" to the upper-left corner which happens to be (0;0). I first thought I wasn't applying the attraction effect correctly but after re-reading it multiple times I haven't found any errors. Does somebody have any idea why it doesn't work as expected ?
/ Here is the code /
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import pygame, random, time
import numpy as np
attraction = [ [-2.6,8.8,10.2,0.7],
[4.1,-3.3,-3.1,4.4],
[0.6,3.7,-0.4,5.1],
[-7.8,0.3,0.3,0.0]]
minR = [[100.0,100.0,100.0,100.0],
[100.0,100.0,100.0,100.0],
[100.0,100.0,100.0,100.0],
[100.0,100.0,100.0,100.0]]
maxR = [[41.7,16.4,22.1,15.0],
[16.4,41.7,32.0,75.1],
[22.1,32.0,55.7,69.9],
[15.0,75.1,69.9,39.5]]
colors = [ (200,50,50),
(200,100,200),
(100,255,100),
(50,100,100)]
#Rouge
#Violet
#Vert
#Cyan
particles = []
#Number of particles
numberParticles = 5
#Width
w = 500
#Height
h = 500
#Radius of particles
r = 4
#Rendering speed
speed = 0.05
#Attraction speed factor
speedFactor = 0.01
#Min distance factor
minRFactor = 0.1
#Max distance factor
maxRFactor = 2
#Attraction factor
attractionFactor = 0.01
def distance(ax, ay, bx, by):
return intg((ax - bx)**2 + (ay - by)**2)
def intg(x):
return int(round(x))
def display(plan):
#Fill with black
#Momentarily moved to main
#pygame.Surface.fill(plan,(0,0,0))
#For each particle, draw it
for particle in particles:
pygame.draw.circle(plan,colors[particle[0]],(particle[1],particle[2]),r)
#Update display
pygame.display.flip()
def update(particles):
newParticles = []
for particleIndex in xrange(len(particles)):
typeId, x, y = particles[particleIndex]
othersX = [[],[],[],[]]
othersY = [[],[],[],[]]
#For every other particles
for otherParticle in particles[0:particleIndex]+particles[particleIndex+1:]:
otherTypeId, otherX, otherY = otherParticle
"""
#Draw minR and maxR of attraction for each color
pygame.draw.circle(screen,colors[otherTypeId],(x,y),intg(minR[typeId][otherTypeId] * minRFactor),1)
pygame.draw.circle(screen,colors[otherTypeId],(x,y),intg(maxR[typeId][otherTypeId] * maxRFactor),1)
"""
#If otherParticle is between minR and maxR from (x;y)
if (minR[typeId][otherTypeId] * minRFactor)**2 <= distance(x,y,otherX,otherY) <= (maxR[typeId][otherTypeId] * maxRFactor)**2:
#Append otherParticle's coordinates to othersX and othersY respectively
othersX[otherTypeId].append(otherX)
othersY[otherTypeId].append(otherY)
#Take the average attractions for each color
othersX = [np.mean(othersX[i]) * attraction[typeId][i] * attractionFactor for i in xrange(len(othersX)) if othersX[i] != []]
othersY = [np.mean(othersY[i]) * attraction[typeId][i] * attractionFactor for i in xrange(len(othersY)) if othersY[i] != []]
#If not attracted, stay in place
if othersX == []:
newX = x
else:
#Take the average attraction
avgX = np.mean(othersX)
#Determine the new x position
newX = x - (x - avgX) * speedFactor
#If out of screen, warp
if newX > w:
newX -= w
elif newX < 0:
newX += w
#If not attracted, stay in place
if othersY == []:
newY = y
else:
#Take the average attraction
avgY = np.mean(othersY)
#Determine the new y position
newY = y - (y - avgY) * speedFactor
#If out of screen, warp
if newY > h:
newY -= h
elif newY < 0:
newY += h
#Append updated particle to newParticles
newParticles.append([typeId,intg(newX),intg(newY)])
return newParticles
if __name__ == "__main__":
#Initialize pygame screen
pygame.init()
screen = pygame.display.set_mode([w,h])
#Particle = [type,posX,posY]
#Create randomly placed particles of random type
for x in xrange(numberParticles):
particles.append([random.randint(0,3),random.randint(0,w),random.randint(0,h)])
display(screen)
#Wait a bit
time.sleep(1)
while True:
#raw_input()
#Fill the screen with black
pygame.Surface.fill(screen,(0,0,0))
#Update particles
particles = update(particles)
#Display particles
display(screen)
#Wait a bit
time.sleep(speed)
The issue is in the lines:
othersX = [np.mean(othersX[i]) * attraction[typeId][i] * attractionFactor for i in range(len(othersX)) if othersX[i] != []]
othersY = [np.mean(othersY[i]) * attraction[typeId][i] * attractionFactor for i in range(len(othersY)) if othersY[i] != []]
othersX and othersY should be positions, but since the coordinates are multiplied by attraction[typeId][i] * attractionFactor, the coordinates are shifted to top left.
This can be evaluated with ease, by omitting the factors:
othersX = [np.mean(othersX[i]) for i in range(len(othersX)) if othersX[i] != []]
othersY = [np.mean(othersY[i]) for i in range(len(othersY)) if othersY[i] != []]
An option is to use vectors form (x, y) to (otherX, otherY) rather than positions:
for otherParticle in particles[0:particleIndex]+particles[particleIndex+1:]:
otherTypeId, otherX, otherY = otherParticle
if (minR[typeId][otherTypeId] * minRFactor)**2 <= distance(x,y,otherX,otherY) <= (maxR[typeId][otherTypeId] * maxRFactor)**2:
# Append otherParticle's coordinates to othersX and othersY respectively
othersX[otherTypeId].append(otherX - x)
othersY[otherTypeId].append(otherY - y)
othersX = [np.mean(othersX[i]) * attraction[typeId][i] * attractionFactor for i in range(len(othersX)) if othersX[i] != []]
othersY = [np.mean(othersY[i]) * attraction[typeId][i] * attractionFactor for i in range(len(othersY)) if othersY[i] != []]
Of course you've to adapt the calculation of the new positions, too:
avgX = np.mean(othersX)
newX = x + avgX * speedFactor
avgY = np.mean(othersY)
newY = y + avgY * speedFactor
As mentioned in the other answer, you should use floating point numbers for the calculations:
def distance(ax, ay, bx, by):
# return intg((ax - bx)**2 + (ay - by)**2)
return (ax - bx)**2 + (ay - by)**2
# newParticles.append([typeId,intg(newX),intg(newY)])
newParticles.append([typeId, newX, newY])
But round to integral coordinates when you draw the circles:
for particle in particles:
# pygame.draw.circle(plan,colors[particle[0]],(particle[1],particle[2]),r)
pygame.draw.circle(plan,colors[particle[0]],(intg(particle[1]),intg(particle[2])),r)
It might be this line here:
newParticles.append([typeId,intg(newX),intg(newY)])
You calculated the position of you particles with high precision earlier, but then the intg() will round all of those numbers down towards 0 before you save it to newparticles. Over time this will skew things towards [0,0].
How I would fix this is by keeping the data in your particles and newparticles as floating point precision, only do the rounding when you have to put things on screen. This way the high accuracy you use will be kept from one timestep to the next.
Related
Is there a way to incorporate Perlin Noise into the Ursina game engine in Python to give it a Minecraft feeling? I think I may be onto something, but for some reason, the y value is not varying. Do you mind helping me out?
The code that I have so far:
from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
from perlin_noise import PerlinNoise;
app = Ursina()
noise = PerlinNoise(octaves=10, seed=1)
# Define a Voxel class.
# By setting the parent to scene and the model to 'cube' it becomes a 3d button.
class Voxel(Button):
def __init__(self, position=(0,0,0)):
super().__init__(
parent = scene,
position = position,
model = 'cube',
origin_y = .5,
texture = 'white_cube',
color = color.color(0, 0, random.uniform(.9, 1.0)),
highlight_color = color.lime,
)
def input(self, key):
if self.hovered:
if key == 'left mouse down':
voxel = Voxel(position=self.position + mouse.normal)
if key == 'right mouse down':
destroy(self)
for z in range(8):
for x in range(8):
y = .25 + noise([x, z])
voxel = Voxel(position=(x, y,z))
player = FirstPersonController()
app.run()
As shown in the usage examples, you'll have to scale the noise value with an additional division:
for z in range(8):
for x in range(8):
y = .25 + noise([x/8, z/8])
voxel = Voxel(position=(x, y,z))
You'll probably want to replace that magic number with a constant.
You can use this:
seed=random.randint(1,1000000)
noise = PerlinNoise(octaves=3,seed=seed)
for z in range(8):
for x in range(8):
y = noise([x * 0.02,z * 0.02])
y = math.floor(y * 7.5)
voxel = Voxel(position=(x,y,z))
First, you did not say "Perlin Noise" correctly, so remove the semi-colon, then you'll use this code:
noise = PerlinNoise(octaves=intgeer,seed=0)
for z in range(8):
for x in range(8):
y = 0. 25 noise([x * 0.02,z * 0.02])
voxel = Voxel(position=(x,y,z))
It sounds like Voxel(...) places a voxel at the specified position, right? If so, then you would need to loop over the whole column to place the blocks into.
And if your noise library doesn't handle frequency and amplitude scaling internally, then you need to do that externally. Multiply the input coordinates by fractions, and multiply the output value by larger values.
min_height = 4.0
max_height = 16.0
for z in range(8):
for x in range(8):
height = # some noise formula
# if noise() range is 0 to 1: min_height + noise([x * 0.05, z * 0.05]) * (max_height - min_height)
# if noise() range is -1 to 1: min_height + (noise([x * 0.05, z * 0.05]) * 0.5 + 0.5) * (max_height - min_height)
for y in range(height): # might need to convert height to an integer
voxel = Voxel(position=(x,y,z))
Also please keep in mind the problems of Perlin for noise in its unmitigated form. The algorithm's popularity by name far outweighs its canonical form's true merit in light of its square alignment issues and the broader space of options we have now, and in spite of the overwhelming amount of content that still teaches it in a vacuum. See the final paragraph from my last answer for more info: https://stackoverflow.com/a/73348943
I would suggest this: https://pypi.org/project/pyfastnoiselite/
from pyfastnoiselite.pyfastnoiselite import (
FastNoiseLite, NoiseType, FractalType,
)
min_height = 4.0
max_height = 16.0
noise = FastNoiseLite(0)
noise.noise_type = NoiseType.NoiseType_OpenSimplex2
noise.fractal_type = FractalType.FractalType_FBm
noise.fractal_octaves = 4
noise.frequency = 0.03
for z in range(8):
for x in range(8):
height = min_height + (noise.get_noise(x, z) * 0.5 + 0.5) * (max_height - min_height)
for y in range(height): # might need to convert height to an integer
Voxel(position=(x,y,z))
or for 3D noise to enable overhangs, you vary that heightmap within Y as well:
from pyfastnoiselite.pyfastnoiselite import (
FastNoiseLite, NoiseType, FractalType,
)
min_height = 4.0
max_height = 16.0
noise = FastNoiseLite(0)
noise.noise_type = NoiseType.NoiseType_OpenSimplex2
noise.fractal_type = FractalType.FractalType_FBm
noise.fractal_octaves = 4
noise.frequency = 0.03
for z in range(8):
for x in range(8):
for y in range(min_height): # might need to convert bounds to integers
Voxel(position=(x,y,z))
for y in range(min_height, max_height): # might need to convert bounds to integers
effective_height = min_height + (noise.get_noise(x, y, z) * 0.5 + 0.5) * (max_height - min_height)
if effective_height > y:
Voxel(position=(x,y,z))
Examples untested. There may be some changes to parameters or even syntax to get these working! Feel free to make the needed edits to this answer to get it working properly.
Also min_height + (noise.get_noise(x, y, z) * 0.5 + 0.5) * (max_height - min_height) can be simplified down to noise.get_noise(x, y, z) * factor + offset, where
factor = 0.5 * (max_height - min_height)
offset = factor + min_height
I have created a random walk scenario where it takes one step in a random direction for a specific number of times. The one thing that I have run in to is that sometimes it will go off of the graphics window that I have set up and I can no longer see where it is at.
Here is the code:
from random import *
from graphics import *
from math import *
def walker():
win = GraphWin('Random Walk', 800, 800)
win.setCoords(-50, -50, 50, 50)
center = Point(0, 0)
x = center.getX()
y = center.getY()
while True:
try:
steps = int(input('How many steps do you want to take? (Positive integer only) '))
if steps > 0:
break
else:
print('Please enter a positive number')
except ValueError:
print('ERROR... Try again')
for i in range(steps):
angle = random() * 2 * pi
newX = x + cos(angle)
newY = y + sin(angle)
newpoint = Point(newX, newY).draw(win)
Line(Point(x, y), newpoint).draw(win)
x = newX
y = newY
walker()
My question is, Is there a way that I can set parameters on the graphics window so that the walker can not go outside the window? And if it tries to, it would just turn around and try another direction?
Try defining upper and lower bounds for x and y. Then use a while loop that keeps trying random points until the next one is in bounds.
from random import *
from graphics import *
from math import *
def walker():
win = GraphWin('Random Walk', 800, 800)
win.setCoords(-50, -50, 50, 50)
center = Point(0, 0)
x = center.getX()
y = center.getY()
while True:
try:
steps = int(input('How many steps do you want to take? (Positive integer only) '))
if steps > 0:
break
else:
print('Please enter a positive number')
except ValueError:
print('ERROR... Try again')
# set upper and lower bounds for next point
upper_X_bound = 50.0
lower_X_bound = -50.0
upper_Y_bound = 50.0
lower_Y_bound = -50.0
for i in range(steps):
point_drawn = 0 # initialize point not drawn yet
while point_drawn == 0: # do until point is drawn
drawpoint = 1 # assume in bounds
angle = random() * 2 * pi
newX = x + cos(angle)
newY = y + sin(angle)
if newX > upper_X_bound or newX < lower_X_bound:
drawpoint = 0 # do not draw, x out of bounds
if newY > upper_Y_bound or newY < lower_Y_bound:
drawpoint = 0 # do not draw, y out of bounds
if drawpoint == 1: # only draw points that are in bounds
newpoint = Point(newX, newY).draw(win)
Line(Point(x, y), newpoint).draw(win)
x = newX
y = newY
point_drawn = 1 # set this to exit while loop
walker()
I was working on creating a python script that could model electric field lines, but the quiver plot comes out with arrows that are way too large. I've tried changing the units and the scale, but the documentation on matplotlib makes no sense too me... This seems to only be a major issue when there is only one charge in the system, but the arrows are still slightly oversized with any number of charges. The arrows tend to be oversized in all situations, but it is most evident with only one particle.
import matplotlib.pyplot as plt
import numpy as np
import sympy as sym
import astropy as astro
k = 9 * 10 ** 9
def get_inputs():
inputs_loop = False
while inputs_loop is False:
""""
get inputs
"""
inputs_loop = True
particles_loop = False
while particles_loop is False:
try:
particles_loop = True
"""
get n particles with n charges.
"""
num_particles = int(raw_input('How many particles are in the system? '))
parts = []
for i in range(num_particles):
parts.append([float(raw_input("What is the charge of particle %s in Coulombs? " % (str(i + 1)))),
[float(raw_input("What is the x position of particle %s? " % (str(i + 1)))),
float(raw_input('What is the y position of particle %s? ' % (str(i + 1))))]])
except ValueError:
print 'Could not convert input to proper data type. Please try again.'
particles_loop = False
return parts
def vec_addition(vectors):
x_sum = 0
y_sum = 0
for b in range(len(vectors)):
x_sum += vectors[b][0]
y_sum += vectors[b][1]
return [x_sum,y_sum]
def electric_field(particle, point):
if particle[0] > 0:
"""
Electric field exitation is outwards
If the x position of the particle is > the point, then a different calculation must be made than in not.
"""
field_vector_x = k * (
particle[0] / np.sqrt((particle[1][0] - point[0]) ** 2 + (particle[1][1] - point[1]) ** 2) ** 2) * \
(np.cos(np.arctan2((point[1] - particle[1][1]), (point[0] - particle[1][0]))))
field_vector_y = k * (
particle[0] / np.sqrt((particle[1][0] - point[0]) ** 2 + (particle[1][1] - point[1]) ** 2) ** 2) * \
(np.sin(np.arctan2((point[1] - particle[1][1]), (point[0] - particle[1][0]))))
"""
Defining the direction of the components
"""
if point[1] < particle[1][1] and field_vector_y > 0:
print field_vector_y
field_vector_y *= -1
elif point[1] > particle[1][1] and field_vector_y < 0:
print field_vector_y
field_vector_y *= -1
else:
pass
if point[0] < particle[1][0] and field_vector_x > 0:
print field_vector_x
field_vector_x *= -1
elif point[0] > particle[1][0] and field_vector_x < 0:
print field_vector_x
field_vector_x *= -1
else:
pass
"""
If the charge is negative
"""
elif particle[0] < 0:
field_vector_x = k * (
particle[0] / np.sqrt((particle[1][0] - point[0]) ** 2 + (particle[1][1] - point[1]) ** 2) ** 2) * (
np.cos(np.arctan2((point[1] - particle[1][1]), (point[0] - particle[1][0]))))
field_vector_y = k * (
particle[0] / np.sqrt((particle[1][0] - point[0]) ** 2 + (particle[1][1] - point[1]) ** 2) ** 2) * (
np.sin(np.arctan2((point[1] - particle[1][1]), (point[0] - particle[1][0]))))
"""
Defining the direction of the components
"""
if point[1] > particle[1][1] and field_vector_y > 0:
print field_vector_y
field_vector_y *= -1
elif point[1] < particle[1][1] and field_vector_y < 0:
print field_vector_y
field_vector_y *= -1
else:
pass
if point[0] > particle[1][0] and field_vector_x > 0:
print field_vector_x
field_vector_x *= -1
elif point[0] < particle[1][0] and field_vector_x < 0:
print field_vector_x
field_vector_x *= -1
else:
pass
return [field_vector_x, field_vector_y]
def main(particles):
"""
Graphs the electrical field lines.
:param particles:
:return:
"""
"""
plot particle positions
"""
particle_x = 0
particle_y = 0
for i in range(len(particles)):
if particles[i][0]<0:
particle_x = particles[i][1][0]
particle_y = particles[i][1][1]
plt.plot(particle_x,particle_y,'r+',linewidth=1.5)
else:
particle_x = particles[i][1][0]
particle_y = particles[i][1][1]
plt.plot(particle_x,particle_y,'r_',linewidth=1.5)
"""
Plotting out the quiver plot.
"""
parts_x = [particles[i][1][0] for i in range(len(particles))]
graph_x_min = min(parts_x)
graph_x_max = max(parts_x)
x,y = np.meshgrid(np.arange(graph_x_min-(graph_x_max-graph_x_min),graph_x_max+(graph_x_max-graph_x_min)),
np.arange(graph_x_min-(graph_x_max-graph_x_min),graph_x_max+(graph_x_max-graph_x_min)))
if len(particles)<2:
for x_pos in range(int(particles[0][1][0]-10),int(particles[0][1][0]+10)):
for y_pos in range(int(particles[0][1][0]-10),int(particles[0][1][0]+10)):
vecs = []
for particle_n in particles:
vecs.append(electric_field(particle_n, [x_pos, y_pos]))
final_vector = vec_addition(vecs)
distance = np.sqrt((final_vector[0] - x_pos) ** 2 + (final_vector[1] - y_pos) ** 2)
plt.quiver(x_pos, y_pos, final_vector[0], final_vector[1], distance, angles='xy', scale_units='xy',
scale=1, width=0.05)
plt.axis([particles[0][1][0]-10,particles[0][1][0]+10,
particles[0][1][0] - 10, particles[0][1][0] + 10])
else:
for x_pos in range(int(graph_x_min-(graph_x_max-graph_x_min)),int(graph_x_max+(graph_x_max-graph_x_min))):
for y_pos in range(int(graph_x_min-(graph_x_max-graph_x_min)),int(graph_x_max+(graph_x_max-graph_x_min))):
vecs = []
for particle_n in particles:
vecs.append(electric_field(particle_n,[x_pos,y_pos]))
final_vector = vec_addition(vecs)
distance = np.sqrt((final_vector[0]-x_pos)**2+(final_vector[1]-y_pos)**2)
plt.quiver(x_pos,y_pos,final_vector[0],final_vector[1],distance,angles='xy',units='xy')
plt.axis([graph_x_min-(graph_x_max-graph_x_min),graph_x_max+(graph_x_max-graph_x_min),graph_x_min-(graph_x_max-graph_x_min),graph_x_max+(graph_x_max-graph_x_min)])
plt.grid()
plt.show()
g = get_inputs()
main(g)}
You may set the scale such that it roughly corresponds to the u and v vectors.
plt.quiver(x_pos, y_pos, final_vector[0], final_vector[1], scale=1e9, units="xy")
This would result in something like this:
If I interprete it correctly, you want to draw the field vectors for point charges. Looking around at how other people have done that, one finds e.g. this blog entry by Christian Hill. He uses a streamplot instead of a quiver but we might take the code for calculating the field and replace the plot.
In any case, we do not want and do not need 100 different quiver plots, as in the code from the question, but only one single quiver plot that draws the entire field. We will of course run into a problem if we want to have the field vector's length denote the field strength, as the magnitude goes with the distance from the particles by the power of 3. A solution might be to scale the field logarithmically before plotting, such that the arrow lengths are still somehow visible, even at some distance from the particles. The quiver plot's scale parameter then can be used to adapt the lengths of the arrows such that they somehow fit to other plot parameters.
""" Original code by Christian Hill
http://scipython.com/blog/visualizing-a-vector-field-with-matplotlib/
Changes made to display the field as a quiver plot instead of streamlines
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
def E(q, r0, x, y):
"""Return the electric field vector E=(Ex,Ey) due to charge q at r0."""
den = ((x-r0[0])**2 + (y-r0[1])**2)**1.5
return q * (x - r0[0]) / den, q * (y - r0[1]) / den
# Grid of x, y points
nx, ny = 32, 32
x = np.linspace(-2, 2, nx)
y = np.linspace(-2, 2, ny)
X, Y = np.meshgrid(x, y)
charges = [[5.,[-1,0]],[-5.,[+1,0]]]
# Electric field vector, E=(Ex, Ey), as separate components
Ex, Ey = np.zeros((ny, nx)), np.zeros((ny, nx))
for charge in charges:
ex, ey = E(*charge, x=X, y=Y)
Ex += ex
Ey += ey
fig = plt.figure()
ax = fig.add_subplot(111)
f = lambda x:np.sign(x)*np.log10(1+np.abs(x))
ax.quiver(x, y, f(Ex), f(Ey), scale=33)
# Add filled circles for the charges themselves
charge_colors = {True: 'red', False: 'blue'}
for q, pos in charges:
ax.add_artist(Circle(pos, 0.05, color=charge_colors[q>0]))
ax.set_xlabel('$x$')
ax.set_ylabel('$y$')
ax.set_xlim(-2,2)
ax.set_ylim(-2,2)
ax.set_aspect('equal')
plt.show()
(Note that the field here is not normalized in any way, which should no matter for visualization at all.)
A different option is to look at e.g. this code which also draws field lines from point charges.
I am new to programming, so I hope my stupid questions do not bug you.
I am now trying to calculate the poisson sphere distribution(a 3D version of the poisson disk) using python and then plug in the result to POV-RAY so that I can generate some random distributed packing rocks.
I am following these two links:
[https://github.com/CodingTrain/Rainbow-Code/blob/master/CodingChallenges/CC_33_poisson_disc/sketch.js#L13]
[https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf]
tl;dr
0.Create an n-dimensional grid array and cell size = r/sqrt(n) where r is the minimum distance between each sphere. All arrays are set to be default -1 which stands for 'without point'
1.Create an initial sample. (it should be placed randomly but I choose to put it in the middle). Put it in the grid array. Also, intialize an active array. Put the initial sample in the active array.
2.While the active list is not empty, pick a random index. Generate points near it and make sure the points are not overlapping with nearby points(only test with the nearby arrays). If no sample can be created near the 'random index', kick the 'random index' out. Loop the process.
And here is my code:
import math
from random import uniform
import numpy
import random
radius = 1 #you can change the size of each sphere
mindis = 2 * radius
maxx = 10 #you can change the size of the container
maxy = 10
maxz = 10
k = 30
cellsize = mindis / math.sqrt(3)
nrofx = math.floor(maxx / cellsize)
nrofy = math.floor(maxy / cellsize)
nrofz = math.floor(maxz / cellsize)
grid = []
active = []
default = numpy.array((-1, -1, -1))
for fillindex in range(nrofx * nrofy * nrofz):
grid.append(default)
x = uniform(0, maxx)
y = uniform(0, maxy)
z = uniform(0, maxz)
firstpos = numpy.array((x, y, z))
firsti = maxx // 2
firstj = maxy // 2
firstk = maxz // 2
grid[firsti + nrofx * (firstj + nrofy * firstk)] = firstpos
active.append(firstpos)
while (len(active) > 0) :
randindex = math.floor(uniform(0,len(active)))
pos = active[randindex]
found = False
for attempt in range(k):
offsetx = uniform(mindis, 2 * mindis)
offsety = uniform(mindis, 2 * mindis)
offsetz = uniform(mindis, 2 * mindis)
samplex = offsetx * random.choice([1,-1])
sampley = offsety * random.choice([1,-1])
samplez = offsetz * random.choice([1,-1])
sample = numpy.array((samplex, sampley, samplez))
sample = numpy.add(sample, pos)
xcoor = math.floor(sample.item(0) / cellsize)
ycoor = math.floor(sample.item(1) / cellsize)
zcoor = math.floor(sample.item(2) / cellsize)
attemptindex = xcoor + nrofx * (ycoor + nrofy * zcoor)
if attemptindex >= 0 and attemptindex < nrofx * nrofy * nrofz and numpy.all([sample, default]) == True and xcoor > 0 and ycoor > 0 and zcoor > 0 :
test = True
for testx in range(-1,2):
for testy in range(-1, 2):
for testz in range(-1, 2):
testindex = (xcoor + testx) + nrofx * ((ycoor + testy) + nrofy * (zcoor + testz))
if testindex >=0 and testindex < nrofx * nrofy * nrofz :
neighbour = grid[testindex]
if numpy.all([neighbour, sample]) == False:
if numpy.all([neighbour, default]) == False:
distance = numpy.linalg.norm(sample - neighbour)
if distance > mindis:
test = False
if test == True and len(active)<len(grid):
found = True
grid[attemptindex] = sample
active.append(sample)
if found == False:
del active[randindex]
for printout in range(len(grid)):
print("<" + str(active[printout][0]) + "," + str(active[printout][1]) + "," + str(active[printout][2]) + ">")
print(len(grid))
My code seems to run forever.
Therefore I tried to add a print(len(active)) in the last of the while loop.
Surprisingly, I think I discovered the bug as the length of the active list just keep increasing! (It is supposed to be the same length as the grid) I think the problem is caused by the active.append(), but I can't figure out where is the problem as the code is literally the 90% the same as the one made by Mr.Shiffman.
I don't want to free ride this but I have already checked again and again while correcting again and again for this code :(. Still, I don't know where the bug is. (why do the active[] keep appending!?)
Thank you for the precious time.
Hi :)
i have the following python code that generates points lying on a sphere's surface
from math import sin, cos, pi
toRad = pi / 180
ox = 10
oy = -10
oz = 50
radius = 10.0
radBump = 3.0
angleMin = 0
angleMax = 360
angleOffset = angleMin * toRad
angleRange = (angleMax - angleMin) * toRad
steps = 48
angleStep = angleRange / steps
latMin = 0
latMax = 180
latOffset = latMin * toRad
if (latOffset < 0):
latOffset = 0;
latRange = (latMax - latMin) * toRad
if (latRange > pi):
latRange = pi - latOffset;
latSteps = 48
latAngleStep = latRange / latSteps
for lat in range(0, latSteps):
ang = lat * latAngleStep + latOffset
z = cos(ang) * radius + oz
radMod = sin(ang) * radius
for a in range(0, steps):
x = sin(a * angleStep + angleOffset) * radMod + ox
y = cos(a * angleStep + angleOffset) * radMod + oy
print "%f %f %f"%(x,y,z)
after that i plot the points with gnuplot using splot 'datafile'
can you give any hints on how to create deformations on that sphere?
like "mountains" or "spikes" on it?
(something like the openbsd logo ;) : https://https.openbsd.org/images/tshirt-23.gif )
i know it is a trivial question :( but thanks for your time :)
DsP
The approach that springs to my mind, especially with the way you compute a set of points that are not explicitly connected, is to find where the point goes on the sphere's surface, then move it by a distance and direction determined by a set of control points. The control points could have smaller effects the further away they are. For example:
# we have already computed a points position on the sphere, and
# called it x,y,z
for p in controlPoints:
dx = p.x - x
dy = p.y - y
dz = p.z - z
xDisplace += 1/(dx*dx)
yDisplace += 1/(dy*dy)
zDisplace += 1/(dz*dz) # using distance^2 displacement
x += xDisplace
y += yDisplace
z += zDisplace
By changing the control points you can alter the sphere's shape
By changing the movement function, you can alter the way the points shape the sphere
You could get really tricky and have different functions for different points:
# we have already computed a points position on the sphere, and
# called it x,y,z
for p in controlPoints:
xDisplace += p.displacementFunction(x)
yDisplace += p.displacementFunction(y)
zDisplace += p.displacementFunction(z)
x += xDisplace
y += yDisplace
z += zDisplace
If you do not want all control points affecting every point in the sphere, just build that into the displacement function.
How's this?
from math import sin, cos, pi, radians, ceil
import itertools
try:
rng = xrange # Python 2.x
except NameError:
rng = range # Python 3.x
# for the following calculations,
# - all angles are in radians (unless otherwise specified)
# - latitude is in [-pi/2..pi/2]
# - longitude is in [-pi..pi)
MIN_LAT = -pi/2 # South Pole
MAX_LAT = pi/2 # North Pole
MIN_LON = -pi # Far West
MAX_LON = pi # Far East
def floatRange(start, end=None, step=1.0):
"Floating-point range generator"
start += 0.0 # cast to float
if end is None:
end = start
start = 0.0
steps = int(ceil((end-start)/step))
return (start + k*step for k in rng(0, steps+1))
def patch2d(xmin, xmax, ymin, ymax, step=1.0):
"2d rectangular grid generator"
if xmin>xmax:
xmin,xmax = xmax,xmin
xrange = floatRange(xmin, xmax, step)
if ymin>ymax:
ymin,ymax = ymax,ymin
yrange = floatRange(ymin, ymax, step)
return itertools.product(xrange, yrange)
def patch2d_to_3d(xyIter, zFn):
"Convert 2d field to 2.5d height-field"
mapFn = lambda a: (a[0], a[1], zFn(a[0],a[1]))
return itertools.imap(mapFn, xyIter)
#
# Representation conversion functions
#
def to_spherical(lon, lat, rad):
"Map from spherical to spherical coordinates (identity function)"
return lon, lat, rad
def to_cylindrical(lon, lat, rad):
"Map from spherical to cylindrical coordinates"
# angle, z, radius
return lon, rad*sin(lat), rad*cos(lat)
def to_cartesian(lon, lat, rad):
"Map from spherical to Cartesian coordinates"
# x, y, z
cos_lat = cos(lat)
return rad*cos_lat*cos(lon), rad*cos_lat*sin(lon), rad*sin(lat)
def bumpySphere(gridSize, radiusFn, outConv):
lonlat = patch2d(MIN_LON, MAX_LON, MIN_LAT, MAX_LAT, gridSize)
return list(outConv(*lonlatrad) for lonlatrad in patch2d_to_3d(lonlat, radiusFn))
# make a plain sphere of radius 10
sphere = bumpySphere(radians(5.0), lambda x,y: 10.0, to_cartesian)
# spiky-star-function maker
def starFnMaker(xWidth, xOffset, yWidth, yOffset, minRad, maxRad):
# make a spiky-star function:
# longitudinal and latitudinal triangular waveforms,
# joined as boolean intersection,
# resulting in a grid of positive square pyramids
def starFn(x, y, xWidth=xWidth, xOffset=xOffset, yWidth=yWidth, yOffset=yOffset, minRad=minRad, maxRad=maxRad):
xo = ((x-xOffset)/float(xWidth)) % 1.0 # xo in [0.0..1.0), progress across a single pattern-repetition
xh = 2 * min(xo, 1.0-xo) # height at xo in [0.0..1.0]
xHeight = minRad + xh*(maxRad-minRad)
yo = ((y-yOffset)/float(yWidth)) % 1.0
yh = 2 * min(yo, 1.0-yo)
yHeight = minRad + yh*(maxRad-minRad)
return min(xHeight, yHeight)
return starFn
# parameters to spike-star-function maker
width = 2*pi
horDivs = 20 # number of waveforms longitudinally
horShift = 0.0 # longitudinal offset in [0.0..1.0) of a wave
height = pi
verDivs = 10
verShift = 0.5 # leave spikes at the poles
minRad = 10.0
maxRad = 15.0
deathstarFn = starFnMaker(width/horDivs, width*horShift/horDivs, height/verDivs, height*verShift/verDivs, minRad, maxRad)
deathstar = bumpySphere(radians(2.0), deathstarFn, to_cartesian)
so i finally created the deformation using a set of control points that "pull" the spherical
surface. it is heavilly OO and ugly though ;)
thanks for all the help !!!
to use it > afile and with gnuplot : splot 'afile' w l
DsP
from math import sin, cos, pi ,sqrt,exp
class Point:
"""a 3d point class"""
def __init__(self,x,y,z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return "%f %f %f\n"%(self.x,self.y,self.z)
def __str__(self):
return "point centered: %f %f %f\n"%(self.x,self.y,self.z)
def distance(self,b):
return sqrt((self.x - b.x)**2 +(self.y - b.y)**2 +(self.z -b.z)**2)
def displaceTowards(self,b):
self.x
class ControlPoint(Point):
"""a control point that deforms positions of other points"""
def __init__(self,p):
Point.__init__(self,p.x,p.y,p.z)
self.deformspoints=[]
def deforms(self,p):
self.deformspoints.append(p)
def deformothers(self):
self.deformspoints.sort()
#print self.deformspoints
for i in range(0,len(self.deformspoints)):
self.deformspoints[i].x += (self.x - self.deformspoints[i].x)/2
self.deformspoints[i].y += (self.y - self.deformspoints[i].y)/2
self.deformspoints[i].z += (self.z - self.deformspoints[i].z)/2
class Sphere:
"""returns points on a sphere"""
def __init__(self,radius,angleMin,angleMax,latMin,latMax,discrStep,ox,oy,oz):
toRad = pi/180
self.ox=ox
self.oy=oy
self.oz=oz
self.radius=radius
self.angleMin=angleMin
self.angleMax=angleMax
self.latMin=latMin
self.latMax=latMax
self.discrStep=discrStep
self.angleRange = (self.angleMax - self.angleMin)*toRad
self.angleOffset = self.angleMin*toRad
self.angleStep = self.angleRange / self.discrStep
self.latOffset = self.latMin*toRad
self.latRange = (self.latMax - self.latMin) * toRad
self.latAngleStep = self.latRange / self.discrStep
if(self.latOffset <0):
self.latOffset = 0
if(self.latRange > pi):
self.latRange = pi - latOffset
def CartesianPoints(self):
PointList = []
for lat in range(0,self.discrStep):
ang = lat * self.latAngleStep + self.latOffset
z = cos(ang) * self.radius + self.oz
radMod = sin(ang)*self.radius
for a in range(0,self.discrStep):
x = sin(a*self.angleStep+self.angleOffset)*radMod+self.ox
y = cos(a*self.angleStep+self.angleOffset)*radMod+self.oy
PointList.append(Point(x,y,z))
return PointList
mysphere = Sphere(10.0,0,360,0,180,50,10,10,10)
mylist = mysphere.CartesianPoints()
cpoints = [ControlPoint(Point(0.0,0.0,0.0)),ControlPoint(Point(20.0,0.0,0.0))]
deforpoints=[]
for cp in cpoints:
for p in mylist:
if(p.distance(cp) < 15.0):
cp.deforms(p)
"""print "cp ",cp,"deforms:"
for dp in cp.deformspoints:
print dp ,"at distance", dp.distance(cp)"""
cp.deformothers()
out= mylist.__repr__()
s = out.replace(","," ")
print s