Simulating the transition of packages in a distribution line - python

I am new to python and am trying to run a simulation of a warehouse logistics. The problem is composed of four main agents:
a shed, trucks, motorcycles and a distribution line. The truck enters the shed in one side with a specified amount of boxes, it goes to center of the shed, stops and start unloading the boxes to the distribution line, the distribution line moves the boxes to the other side of the shed where motorcycles pickup one box each.
The objective is to vary the size of the shed and distribution line to find the shape that can deliver more boxes in fixed amount of time (or compute the time taken to distribute a fixed amount of boxes, as in my code for now)
The distribution line is a rectangle, a grid with variable amount of rows and columns, depending on the size of the shed, let's say each cell has 0,50m on each side.
In the code I simulated the truck passing through the shed, and the amount of trucks passing as iterations, the problems is:
how to simulate the boxes moving through the grid (distribution line) from one side to the other, maybe accumulating in the stock until a bike arrives, and have the motorcycles "grab" them and go out after the boxes arrive?
I tried to count the boxes with "+= 1" function but I don't know why it's not working (would not be very realistic as well)
This is the main code:
import time
from Vehicles import Truck, Motorbike
bike1 = Motorbike(10, 1)
truck1 = Truck(10, int(input("Enter how many loads the truck has: ")))
num_iterations = int(input("Enter number of iterations: "))
start = time.time()
shed_width = 4
shed_length = 12
truck_path = int(shed_length * truck1.truck_speed/2)
for n in range(num_iterations):
truck_middle = False
while truck_middle is not True:
for i in range(truck_path):
x = 100/truck_path
if i == truck_path/2:
truck_middle = True
else:
#the bar here is to just have some visual feedback while the code runs
print("\r[%-60s] %d%%" % ('=' * i, x * i), end='')
time.sleep(0.1)
print("\ntruck is in the middle")
truck_middle = True
# while truck_middle is True:
# box = 0
# if box < truck1.truck_load:
# box += 1
# else:
# truck_middle = False
print("This was iteration: " + str(n+1))
time.sleep(0.01)
end = time.time()
print("\nDone! \nThe simulation took " + str(end - start) + " seconds to complete!")
I also created a class in a file called "Vehicles" for the truck and the motorcycles, where I can define their speed and the load they can carry:
class Truck:
def __init__(self, truck_speed, truck_load):
self.truck_speed = truck_speed
self.truck_load = truck_load
class Motorbike:
def __init__(self, motorbike_speed, motorbike_load):
self.motorbike_speed = motorbike_speed
self.motorbike_load = motorbike_load
I am open to code suggestions, indications of libraries and other resources I can search and study, any help will be much appreciated! thanks!

box = 0
while truck_middle == True:
if box < truck1.truck_load:
box += 1
else:
truck_middle = False
In your way, box will always be 1 and truck_middle is always True, and it goes in a dead loop

Related

How to properly add gradually increasing/decreasing space between objects?

I've trying to implement transition from an amount of space to another which is similar to acceleration and deceleration, except i failed and the only thing that i got from this was this infinite stack of mess, here is a screenshot showing this in action:
you can see a very black circle here, which are in reality something like 100 or 200 circles stacked on top of each other
and i reached this result using this piece of code:
def Place_circles(curve, circle_space, cs, draw=True, screen=None):
curve_acceleration = []
if type(curve) == tuple:
curve_acceleration = curve[1][0]
curve_intensity = curve[1][1]
curve = curve[0]
#print(curve_intensity)
#print(curve_acceleration)
Circle_list = []
idx = [0,0]
for c in reversed(range(0,len(curve))):
for p in reversed(range(0,len(curve[c]))):
user_dist = circle_space[curve_intensity[c]] + curve_acceleration[c] * p
dist = math.sqrt(math.pow(curve[c][p][0] - curve[idx[0]][idx[1]][0],2)+math.pow(curve [c][p][1] - curve[idx[0]][idx[1]][1],2))
if dist > user_dist:
idx = [c,p]
Circle_list.append(circles.circles(round(curve[c][p][0]), round(curve[c][p][1]), cs, draw, screen))
This place circles depending on the intensity (a number between 0 and 2, random) of the current curve, which equal to an amount of space (let's say between 20 and 30 here, 20 being index 0, 30 being index 2 and a number between these 2 being index 1).
This create the stack you see above and isn't what i want, i also came to the conclusion that i cannot use acceleration since the amount of time to move between 2 points depend on the amount of circles i need to click on, knowing that there are multiple circles between each points, but not being able to determine how many lead to me being unable to the the classic acceleration formula.
So I'm running out of options here and ideas on how to transition from an amount of space to another.
any idea?
PS: i scrapped the idea above and switched back to my master branch but the code for this is still available in the branch i created here https://github.com/Mrcubix/Osu-StreamGenerator/tree/acceleration .
So now I'm back with my normal code that don't possess acceleration or deceleration.
TL:DR i can't use acceleration since i don't know the amount of circles that are going to be placed between the 2 points and make the time of travel vary (i need for exemple to click circles at 180 bpm of one circle every 0.333s) so I'm looking for another way to generate gradually changing space.
First, i took my function that was generating the intensity for each curves in [0 ; 2]
Then i scrapped the acceleration formula as it's unusable.
Now i'm using a basic algorithm to determine the maximum amount of circles i can place on a curve.
Now the way my script work is the following:
i first generate a stream (multiple circles that need to be clicked at high bpm)
this way i obtain the length of each curves (or segments) of the polyline.
i generate an intensity for each curve using the following function:
def generate_intensity(Circle_list: list = None, circle_space: int = None, Args: list = None):
curve_intensity = []
if not Args or Args[0] == "NewProfile":
prompt = True
while prompt:
max_duration_intensity = input("Choose the maximum amount of curve the change in intensity will occur for: ")
if max_duration_intensity.isdigit():
max_duration_intensity = int(max_duration_intensity)
prompt = False
prompt = True
while prompt:
intensity_change_odds = input("Choose the odds of occurence for changes in intensity (1-100): ")
if intensity_change_odds.isdigit():
intensity_change_odds = int(intensity_change_odds)
if 0 < intensity_change_odds <= 100:
prompt = False
prompt = True
while prompt:
min_intensity = input("Choose the lowest amount of spacing a circle will have: ")
if min_intensity.isdigit():
min_intensity = float(min_intensity)
if min_intensity < circle_space:
prompt = False
prompt = True
while prompt:
max_intensity = input("Choose the highest amount of spacing a circle will have: ")
if max_intensity.isdigit():
max_intensity = float(max_intensity)
if max_intensity > circle_space:
prompt = False
prompt = True
if Args:
if Args[0] == "NewProfile":
return [max_duration_intensity, intensity_change_odds, min_intensity, max_intensity]
elif Args[0] == "GenMap":
max_duration_intensity = Args[1]
intensity_change_odds = Args[2]
min_intensity = Args[3]
max_intensity = Args[4]
circle_space = ([min_intensity, circle_space, max_intensity] if not Args else [Args[0][3],circle_space,Args[0][4]])
count = 0
for idx, i in enumerate(Circle_list):
if idx == len(Circle_list) - 1:
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
else:
curve_intensity.append(0)
else:
curve_intensity.append(1)
if random.randint(0,100) < intensity_change_odds:
if random.randint(0,100) > 50:
curve_intensity.append(2)
count += 1
else:
curve_intensity.append(0)
count += 1
else:
if curve_intensity:
if curve_intensity[-1] == 2 and not count+1 > max_duration_intensity:
curve_intensity.append(2)
count += 1
continue
elif curve_intensity[-1] == 0 and not count+1 > max_duration_intensity:
curve_intensity.append(0)
count += 1
continue
elif count+1 > 2:
curve_intensity.append(1)
count = 0
continue
else:
curve_intensity.append(1)
else:
curve_intensity.append(1)
curve_intensity.reverse()
if curve_intensity.count(curve_intensity[0]) == len(curve_intensity):
print("Intensity didn't change")
return circle_space[1]
print("\n")
return [circle_space, curve_intensity]
with this, i obtain 2 list, one with the spacing i specified, and the second one is the list of randomly generated intensity.
from there i call another function taking into argument the polyline, the previously specified spacings and the generated intensity:
def acceleration_algorithm(polyline, circle_space, curve_intensity):
new_circle_spacing = []
for idx in range(len(polyline)): #repeat 4 times
spacing = []
Length = 0
best_spacing = 0
for p_idx in range(len(polyline[idx])-1): #repeat 1000 times / p_idx in [0 ; 1000]
# Create multiple list containing spacing going from circle_space[curve_intensity[idx-1]] to circle_space[curve_intensity[idx]]
spacing.append(np.linspace(circle_space[curve_intensity[idx]],circle_space[curve_intensity[idx+1]], p_idx).tolist())
# Sum distance to find length of curve
Length += abs(math.sqrt((polyline[idx][p_idx+1][0] - polyline[idx][p_idx][0]) ** 2 + (polyline [idx][p_idx+1][1] - polyline[idx][p_idx][1]) ** 2))
for s in range(len(spacing)): # probably has 1000 list in 1 list
length_left = Length # Make sure to reset length for each iteration
for dist in spacing[s]: # substract the specified int in spacing[s]
length_left -= dist
if length_left > 0:
best_spacing = s
else: # Since length < 0, use previous working index (best_spacing), could also jsut do `s-1`
if spacing[best_spacing] == []:
new_circle_spacing.append([circle_space[1]])
continue
new_circle_spacing.append(spacing[best_spacing])
break
return new_circle_spacing
with this, i obtain a list with the space between each circles that are going to be placed,
from there, i can Call Place_circles() again, and obtain the new stream:
def Place_circles(polyline, circle_space, cs, DoDrawCircle=True, surface=None):
Circle_list = []
curve = []
next_circle_space = None
dist = 0
for c in reversed(range(0, len(polyline))):
curve = []
if type(circle_space) == list:
iter_circle_space = iter(circle_space[c])
next_circle_space = next(iter_circle_space, circle_space[c][-1])
for p in reversed(range(len(polyline[c])-1)):
dist += math.sqrt((polyline[c][p+1][0] - polyline[c][p][0]) ** 2 + (polyline [c][p+1][1] - polyline[c][p][1]) ** 2)
if dist > (circle_space if type(circle_space) == int else next_circle_space):
dist = 0
curve.append(circles.circles(round(polyline[c][p][0]), round(polyline[c][p][1]), cs, DoDrawCircle, surface))
if type(circle_space) == list:
next_circle_space = next(iter_circle_space, circle_space[c][-1])
Circle_list.append(curve)
return Circle_list
the result is a stream with varying space between circles (so accelerating or decelerating), the only issue left to be fixed is pygame not updating the screen with the new set of circle after i call Place_circles(), but that's an issue i'm either going to try to fix myself or ask in another post
the final code for this feature can be found on my repo : https://github.com/Mrcubix/Osu-StreamGenerator/tree/Acceleration_v02

How to measure if a value stayed same for a period of time?

I'm making a smart bicycle backlight with Raspberry Pi and a SenseHat on top of it.
I'm measuring the output values of the SenseHat's accelerometer. There are actually three values reported as x, y, z and what I'm trying to know is whether my bicycle has been stationary for let's say 15 seconds or more. So if these values have been staying the same for over 15 seconds I'll turn off the backlight. And then if they start changing and stay like that for over 15 seconds, I want it to engage and start functioning again.
So far I've implemented strobe effect that auto activates when the bicycle is in idle. A steering detection - I show arrow animations to left and right based on the object detection sensor i put near the handlebar. I also implemented a brake detection. Once the accelerometer detects the braking, i show full red light.
If you check the code, you can see it's a bit tricky since the whole thing is in while loop and I need to make this detection as an if condition and then add else below, put rest of my existing if conditions there (such as deacceleration or turning detection)
So how do I make Python measure certain values for 15 seconds without using time.delay and do things based on whether they change or stay the same?
while True:
ser.flushInput()
ser.flushOutput()
x, y, z = sense.get_accelerometer_raw().values()
x = round(x, 2)
y = round(y, 2)
z = round(z, 2)
print("x=%s, y=%s, z=%s" % (x, y, z))
input = ser.read() #serial input i'm getting from arduino, it tells me if my left or right steering sensors are triggerred.
yon = input.decode("utf-8")
int(yon)
if (z > 0.20): #If deacceleration is detected
fren() # brake function is called
else: # if no breaking is detected...
if (yon == "1"): #if left turn sensor triggered
sag_ok() #show left turn animation on led matrix
elif (yon =="2"): # if right turn sensor triggered
sol_ok() #show right turn animation on led matrix
else: #anything else
strobe() #show strobe effect if nothing else is detected
There's a number of smaller things that need to be addressed first:
input = ser.read(). input is actually a builtin and shouldn't be used as a variable name
int(yon) does nothing. You may well convert it to an int but the result is lost because you don't assign the result back to a name
if (z > 0.20) and all your other if checks - the brackets actually do nothing here; you can drop them.
A bigger thing to address: That loop is going full-pelt on the CPU core for absolutely no reason. Thousands of times a second, constantly. You should introduce a time.sleep to reduce the load.
Given that, you can achieve your desired output with a mixture of a Boolean flag and a record of when the accelerometer last gave 0 values.
import time
import datetime as dt
last_zeros = None
countdown_started = False
while True:
ser.flushInput()
ser.flushOutput()
x, y, z = sense.get_accelerometer_raw().values()
x = round(x, 2)
y = round(y, 2)
z = round(z, 2)
print("x=%s, y=%s, z=%s" % (x, y, z))
if x == 0 and y == 0 and z == 0:
if countdown_started:
duration = (dt.datetime.utcnow() - last_zeros_time).total_seconds()
if duration > 15:
# Do something to turn the light off here
continue
else:
countdown_started = True
last_zeros_time = dt.datetime.utcnow()
else:
countdown_started = False
sensor_input = ser.read()
yon = sensor_input.decode("utf-8")
if (z > 0.20):
fren()
else:
if (yon == "1"):
sag_ok()
elif (yon =="2"):
sol_ok()
else:
strobe()
time.sleep(0.5)
The basic idea is to keep track of the last time when motion (or non-motion) was detected. If it was more than 15 seconds ago, then turn off (on) the light.
Something like this:
from time import monotonic
TAIL_LIGHT_DELAY = 15
time_of_last_motion = monotinic()
time_of_last_stop = monotonic()
while True:
now = monotonic()
motion = (abs(x) > 0.2) or (abs(y) > 0.2) or (abs(z) > 0.2)
if motion:
time_of_last_motion = now
if now - time_of_last_stop > TAIL_LIGHT_DELAY:
turn_on_tail_light()
else:
time_of_last_stop = now
if now - time_of_last_motion > TAIL_LIGHT_DELAY:
turn_off_tail_light()

Count number of steps of turtle in a list - python

I am now three weeks into learning python and I'm stuck.
This is my code: (after that comes my question)
from turtle import Screen, Turtle
from random import randint, choice
def person_characteristics(people):
"""
Gives the turtle 'person' all it's characteristics / values.
"""
for person in people:
person.shape('circle')
person.shapesize(0.2)
person.speed('fastest')
person.penup()
x = randint(-200, 200) #turtle gets a random position
y = randint(-200, 200)
person.setpos(x, y)
person.showturtle()
def population(population_size):
"""
Makes a population, by making a list of turtles (persons).
population_size = type(int)
"""
people = []
for _ in range(population_size):
people.append(Turtle(visible=False))
return people
def random_walk(person, step_size, area_size):
"""
Makes the person walk around randomly within the borders.
step_size = type(int) -> determines how big of a step each person takes.
area_size = type(int) -> determines how big the area is where the persons are in.
"""
if -area_size < person.xcor() < area_size and -area_size < person.ycor() < area_size: #if person is within the borders then it moves randomly
person.right(randint(0, 360))
person.forward(step_size)
else:
person.right(180) #if person is outside the borders it turns around
person.forward(step_size)
def infect_random(people):
"""
Random person gets infected (a red color)
people = a list of persons achieved from de function population()
"""
infected = choice(people)
infected.color('red')
return infected
screen = Screen()
people = population(100)
person_characteristics(people)
infected_people = []
initial_infected = infect_random(people)
infected_people.append(initial_infected)
counted_infections = 1
#count_steps = 0
#healed_people = []
for _ in range(10): # determines the number of steps = time
for person in people:
random_walk(person, 30, 400)
for infected_person in infected_people:
if person.pencolor() != 'red' and person.distance(infected_person) < 30: #if a person gets close to the initial infected person it also
person.color('red') #gets infected & added to the list of infected persons
infected_people.append(person)
#count_steps +=1
#if count_steps = 5:
#infected_person.color('green')
#healed_people.append(infected_person)
#infected_people.remove(infected_person)
break
count_susceptible = len(people) - len(infected_people) #counts number of susceptible people
count_infected = len(infected_people) #counts number of infected people
print(count_susceptible)
print(count_infected)
screen.exitonclick()
I want to turn the infected_person green (=healed) and at the turtle to the list healed_people (& remove from the list infected_people) after it made 5 steps. My idea was to do this with a if statement however this does not work. My idea is in the code above. I know why it doesn't work: now it counts the total number of steps of every infected_person, instead of separately.
I think there probably is a very simple solution but I am very new to python so I have no idea how to do this. Can anyone help?
Thanks in advance!
(I prefer not to use a Class since I didn't learn that yet :)
from turtle import Screen, Turtle
from random import randint, choice
def person_characteristics(people):
"""
Gives the turtle 'person' all it's characteristics / values.
"""
for person in people:
person.shape('circle')
person.shapesize(0.2)
person.speed('fastest')
person.penup()
x = randint(-200, 200) #turtle gets a random position
y = randint(-200, 200)
person.setpos(x, y)
person.showturtle()
def population(population_size):
"""
Makes a population, by making a list of turtles (persons).
population_size = type(int)
"""
people = []
for _ in range(population_size):
people.append(Turtle(visible=False))
return people
def random_walk(person, step_size, area_size):
"""
Makes the person walk around randomly within the borders.
step_size = type(int) -> determines how big of a step each person takes.
area_size = type(int) -> determines how big the area is where the persons are in.
"""
if -area_size < person.xcor() < area_size and -area_size < person.ycor() < area_size: #if person is within the borders then it moves randomly
person.right(randint(0, 360))
person.forward(step_size)
else:
person.right(180) #if person is outside the borders it turns around
person.forward(step_size)
def infect_random(people):
"""
Random person gets infected (a red color)
people = a list of persons achieved from de function population()
"""
infected = choice(people)
infected.color('red')
return infected
screen = Screen()
people = population(100)
person_characteristics(people)
infected_people = []
people_steps=[0 for _ in range (len(people))]
initial_infected = infect_random(people)
infected_people.append(initial_infected)
counted_infections = 1
for _ in range(10): # determines the number of steps = time
for person in people:
random_walk(person, 30, 400)
people_steps[people.index(person)]+=1
if people_steps[people.index(person)]==5 and person.pencolor()=='red':
person.color('green')
infected_people.remove(person)
for infected_person in infected_people:
if person.pencolor() != 'red' and person.distance(infected_person) < 30: #if a person gets close to the initial infected person it also
person.color('red') #gets infected & added to the list of infected persons
infected_people.append(person)
people_steps[people.index(person)]=0
break
count_susceptible = len(people) - len(infected_people) #counts number of susceptible people
count_infected = len(infected_people) #counts number of infected people
print(count_susceptible)
print(count_infected)
screen.exitonclick()
You can use an auxiliary array to save the number of people's steps. If you create it with the same size you can work with the indexes. For example:
people=[person1, person2, person3]
people_steps=[number_of_steps1, number_of_steps2, number_of_steps3]
This is just a graphical representation.
But it is better to do it with a class so that the number of steps is part of it as an attribute.
I hope this helps. If you have any suggestions or questions, please let me know.

Python Slot Machine: Calculating Line Payouts

I'm a Python newbie attempting to create a slot machine simulator that mimics the payouts of the real machines. I'm running into an issue in calculating the line payouts, and I'm sure there's a smarter way of iterating through the lines and calculating them.
Defining some constants that I'll be using:
SymbolMap = ["Boats","Bonus","Buoys","Clams","Light Houses","Lobsters","Scatter","Seagulls","Starfish","Tuna","Wilds"]
#The ints in reels below are a simpler way to express the SymbolMap above
Reels = [[9,8,3,4,6,3,8,1,5,6,2,3,8,2,3,8,5,4,3,10,7,8,10,1,3,0,8,9,3,8,9,5,3,8,0,4,3,8,0,9,2,7,5,3,8,0,7],
[3,2,4,3,2,4,1,7,3,0,7,9,0,1,8,7,10,1,7,4,5,10,2,3,1,7,3,6,5,9,7,6,8,3,0,5,7,3,1,8,7,2,4,3,9,7,0],
[0,8,3,1,4,0,5,8,1,4,8,1,9,8,3,7,8,10,1,4,7,8,9,3,0,9,8,1,9,4,8,6,4,5,7,8,6,2,9,5,1,8,4,7,2,0,9],
[7,9,2,7,6,2,8,7,9,10,2,9,8,5,7,9,10,5,4,2,7,0,3,8,4,7,0,3,2,7,0,4,8,9,7,2,8,3,2,7,8,3,5,10,2,7,8],
[3,10,0,5,2,8,4,9,8,4,7,10,9,2,0,3,9,2,8,3,6,2,8,9,3,2,0,4,9,5,4,7,3,5,8,0,4,9,7,8,4,3,5,7,8,3,7]]
# Lines are the row to look for on each reel. i.e. Lines[0] is a straight line of the 2nd row.
# Lines[3] starts in top left corner of matrix, and forms inverted V shape.
Lines = [[1,1,1,1,1],
[0,0,0,0,0],
[2,2,2,2,2],
[0,1,2,1,0],
[2,1,0,1,2],
[2,2,1,0,0],
[0,0,1,2,2],
[1,2,1,0,1],
[1,0,1,2,1],
[2,1,1,1,0],
[0,1,1,1,2],
[1,2,2,1,0],
[1,0,0,1,2],
[1,1,2,1,0],
[1,1,0,1,2]]
#Payouts are how many credits won for symbols in a row. For example, Symbols[0] is Boats.
#2 boats is 0 credits, 3 boats is 25 credits, 4 boats is 100 credits, 5 boats is 500 credits.
#They must be continuous and from left to right. I.e. BOAT-BOAT-CLAM-BOAT-BOAT on a payline wins 0.
#Similarly, CLAM-CLAM-BOAT-BOAT-BOAT wins 0.
Payouts = [[0,25,100,500],
[0,0,0,0],
[0,25,100,500],
[0,5,30,200]]
#Initializing a 3X5 matrix to represent reels
SpinValues = [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]
#Initializing message
Message = ''
#Initializing TotalWin
TotalWin = 0
Spin logic, which properly generates a 3X5 matrix of random numbers. Is there a better way to handle the "if first 3 symbols match" portion, since I'll have to repeat again for 4 symbols and 5 symbols? Since each line is only entitled to one payout, I'll start with the 5 symbol payouts, then work down toward 2. I started with 3 because it's the most common and will be easiest to test. I'll also have to account for a wild equaling any symbol, which I haven't tried to tackle yet. Likewise, there is a scatter pay (meaning if you have X number of Scatter symbols anywhere in the matrix, you get a payout. That part will be easy). There is also a bonus game, which I'll be working on later:
def spin(linesPlayed, wager):
for i, object in enumerate(Reels):
length = len(Reels[i])
StopValue = random.randint(0,length-1)
SpinValues[1][i] = Reels[i][StopValue]
if StopValue == 0:
SpinValues[0][i] = Reels[i][-1]
else:
SpinValues[0][i] = Reels[i][StopValue - 1]
if StopValue == len(Reels[i])-1:
SpinValues[2][i] = Reels[i][0]
else:
SpinValues[2][i] = Reels[i][StopValue +1]
print(SpinValues[0])
print("\n")
print(SpinValues[1])
print("\n")
print(SpinValues[2])
for i in range(linesPlayed):
#if first 3 symbols match
if SpinValues[Lines[i][0]] == SpinValues[Lines[i][1]] == SpinValues[Lines[i][2]]:
PayTable(i,wager,3,SpinValues[Lines[i][0]])
#if first 4 symbols match
#if first 5 symbols match
#handle scatter pay
#wilds?
#handle bonus trigger
Handling wins:
def PayTable(i,wager,symbolCount,symbol):
LineWin = Payouts[symbol][symbolCount] * wager
TotalWin += Payouts[symbol][symbolCount] * wager
Message += "Line " + str(i) +" wins " + str(LineWin) + " credits with " + str(symbolCount) + " " + SymbolMap[symbol] + "!" + "\n"
I'm getting the error that both TotalWin and Message are undefined. I thought that I could defined them globally up top?
You need to use the global keyword in each function to access variables defined in a parent.
For example:
def PayTable(i,wager,symbolCount,symbol):
global TotalWin
global Message

Finding a series of patterns within a data stream

(This is in in Python, and code would be great, but I'm primarily interested in the algorithm.)
I'm monitoring an audio stream (PyAudio) and looking for a series of 5 pops (see the bottom for a visualization). I'm read()ing the stream and getting the RMS value for the block that I've just read (similar to this question). My problem is that I'm not looking for a single event, but instead a series of events (pops) that have some characteristics but aren't nearly as boolean as I'd like. What's the most straightforward (and performant) way to detect these five pops?
The RMS function gives me a stream like this:
0.000580998485254, 0.00045098391298, 0.00751436443973, 0.002733730043, 0.00160775708652, 0.000847808804511
It looks a bit more useful if I round (a similar stream) for you:
0.001, 0.001, 0.018, 0.007, 0.003, 0.001, 0.001
You can see the pop in item 3, and presumably as it quiets down in item 4, and maybe the tail end was during a fraction of item 5.
I want to detect 5 of those in a row.
My naive approach is to:
a) define what a pop is: Block's RMS is over .002. For at least 2 blocks but no more than 4 blocks. Started with silence and ends with silence.
Additionally, I'm tempted to define what silence is (to ignore the not quite loud but not quite silent blocks, but I'm not sure this makes more sense then considering 'pop' to be boolean).
b) Then have a state machine that keeps track of a bunch of variables and has a bunch of if statements. Like:
while True:
is_pop = isRMSAmplitudeLoudEnoughToBeAPop(stream.read())
if is_pop:
if state == 'pop':
#continuation of a pop (or maybe this continuation means
#that it's too long to be a pop
if num_pop_blocks <= MAX_POP_RECORDS:
num_pop_blocks += 1
else:
# too long to be a pop
state = 'waiting'
num_sequential_pops = 0
else if state == 'silence':
#possible beginning of a pop
state = 'pop'
num_pop_blocks += 1
num_silence_blocks = 0
else:
#silence
if state = 'pop':
#we just transitioned from pop to silence
num_sequential_pops += 1
if num_sequential_pops == 5:
# we did it
state = 'waiting'
num_sequential_pops = 0
num_silence_blocks = 0
fivePopsCallback()
else if state = 'silence':
if num_silence_blocks >= MAX_SILENCE_BLOCKS:
#now we're just waiting
state = 'waiting'
num_silence_blocks = 0
num_sequential_pops = 0
That code is not at all complete (and might have a bug or two), but illustrates my line of thinking. It's certainly more complex than I'd like it to be, which is why I'm asking for suggestions.
You might want to compute the simple moving average of the last P points, where P ~= 4 and plot the result together with your raw input data.
You could then use the maxima of the smoothed average as a pop. Define a maximum interval in which to see five pops and that could be what your after.
Adjust P for best fit.
I wouldn't be surprised if there wasn't already a Python module for this, but I haven't looked.
I ended up with what, to me, feels like a naive approach with an ongoing loop and a few variables to maintain and transition to new states. It occurred to me after finishing, though, that I should have explored hotword detection because 5 consecutive clicks are basically a hotword. And they have a pattern that I have to look for.
Anyways, here's my code:
POP_MIN_MS = 50
POP_MAX_MS = 150
POP_GAP_MIN_MS = 50
POP_GAP_MAX_MS = 200
POP_BORDER_MIN_MS = 500
assert POP_BORDER_MIN_MS > POP_GAP_MAX_MS
POP_RMS_THRESHOLD_MIN = 100
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 44100 # Sampling Rate -- frames per second
INPUT_BLOCK_TIME_MS = 50
INPUT_FRAMES_PER_BLOCK = int(RATE*INPUT_BLOCK_TIME_MS/1000)
POP_MIN_BLOCKS = POP_MIN_MS / INPUT_BLOCK_TIME_MS
POP_MAX_BLOCKS = POP_MAX_MS / INPUT_BLOCK_TIME_MS
POP_GAP_MIN_BLOCKS = POP_GAP_MIN_MS / INPUT_BLOCK_TIME_MS
POP_GAP_MAX_BLOCKS = POP_GAP_MAX_MS / INPUT_BLOCK_TIME_MS
POP_BORDER_MIN_BLOCKS = POP_BORDER_MIN_MS / INPUT_BLOCK_TIME_MS
def listen(self):
pops = 0
sequential_loud_blocks = 0
sequential_notloud_blocks = 0
stream = self.pa.open(
format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=INPUT_FRAMES_PER_BLOCK
)
states = {
'PENDING': 1,
'POPPING': 2,
'ENDING': 3,
}
state = states['PENDING']
while True:
amp = audioop.rms(stream.read(INPUT_FRAMES_PER_BLOCK), 2)
is_loud = (amp >= POP_RMS_THRESHOLD_MIN)
if state == states['PENDING']:
if is_loud:
# Only switch to POPPING if it's been quiet for at least the border
# period. Otherwise stay in PENDING.
if sequential_notloud_blocks >= POP_BORDER_MIN_BLOCKS:
state = states['POPPING']
sequential_loud_blocks = 1
# If it's now loud then reset the # of notloud blocks
sequential_notloud_blocks = 0
else:
sequential_notloud_blocks += 1
elif state == states['POPPING']:
if is_loud:
sequential_loud_blocks += 1
# TODO: Is this necessary?
sequential_notloud_blocks = 0
if sequential_loud_blocks > POP_MAX_BLOCKS:
# it's been loud for too long; this isn't a pop
state = states['PENDING']
pops = 0
#print "loud too long"
# since it has been loud and remains loud then no reason to reset
# the notloud_blocks count
else:
# not loud
if sequential_loud_blocks:
# just transitioned from loud. was that a pop?
# we know it wasn't too long, or we would have transitioned to
# PENDING during the pop
if sequential_loud_blocks < POP_MIN_BLOCKS:
# wasn't long enough
# go to PENDING
state = states['PENDING']
pops = 0
#print "not loud long enough"
else:
# just right
pops += 1
logging.debug("POP #%s", pops)
sequential_loud_blocks = 0
sequential_notloud_blocks += 1
else:
# it has been quiet. and it's still quiet
sequential_notloud_blocks += 1
if sequential_notloud_blocks > POP_GAP_MAX_BLOCKS:
# it was quiet for too long
# we're no longer popping, but we don't know if this is the
# border at the end
state = states['ENDING']
elif state == states['ENDING']:
if is_loud:
# a loud block before the required border gap. reset
# since there wasn't a gap, this couldn't be a valid pop anyways
# so just go back to PENDING and let it monitor for the border
sequential_loud_blocks = 1
sequential_notloud_blocks = 0
pops = 0
state = states['PENDING']
else:
sequential_notloud_blocks += 1
# Is the border time (500 ms right now) enough of a delay?
if sequential_notloud_blocks >= POP_BORDER_MIN_BLOCKS:
# that's a bingo!
if pops == 5:
stream.stop_stream()
# assume that starting now the channel is not silent
start_time = time.time()
print ">>>>> 5 POPS"
elapsed = time.time() - start_time
#time.time() may return fractions of a second, which is ideal
stream.start_stream()
# do whateve we need to do
state = states['PENDING']
pops = 0
It needs some formal testing. I found an issue just last night in which it wasn't resetting itself after a pop and then too-long quiet. My plan is to refactor and then feed it a stream of simulated RMS' (e.g., (0, 0, 0, 500, 200, 0, 200, 0, ...)) and ensure it detects (or doesn't detect) appropriately.

Categories