Optimizing a Python synth on Raspberry Pi - python
For the past few weeks I have been working on a project which is all very new to me, and I'm learning as I go. I'm building a synthesizer using a Raspberry Pi 2 and I'm coding it in Python3, as I have some basic knowledge of the language, but not much real experience. I've muddled through pretty well so far, but I have now hit the wall I knew I would hit eventually: performance.
I have been using Pygame and its Sound module to create the sounds I want, and then using my own mathematical algorithms to calculate the ADS(H)R volume envelope for every sound. I tweak this envelope using 8 potentiometers. 3 of them control the length in seconds of the Attack, Decay, Release and another one to set the Sustain level. Then I added 4 more pots which control the curvature of each part of the envelope (except one of them instead sets a Hold value for Sustain). I have a PiTFT screen connected as well which draws up the current shape and length of the entire envelope, as well as prints out the current values of ADSR.
To play sounds I use a 4x4 Adafruit Trellis board and with different button combinations I can play every note between C0 and C8.
I use SciPy and NumPy to create different kinds of soundwaves, as in Sine, Square, Triangle, Sawtooth, Pulse and Noise.
As I have been using regular for loops to change the volume of the sound according to the ADSR envelope, running the function PlaySound takes a while to complete (depending on my ADSR settings of course). This prompted me to try using threads. I don't know if I'm using it in the best way, of if I should use it at all, but it was the only way I could think of to achieve polyphony. Otherwise it had to wait until a sound was completed until it would resume the main loop. So now I can play several notes at the same time. Well, two notes at least. After that it lags and the third one doesn't seem to play until one of the previous sounds have finished.
I've done some tests and checks and I should be able to runt up to 4 threads at the same time, but I might be missing something. One guess is that the system itself has reserved two threads (cores) for other usage.
I realize also that Python is not the most efficient language to use, and I've been looking into Pure Data as well, but I'm having trouble wrapping my head around it (I prefer code over a click-and-drag-gui). I want to keep using Python for as long as possible. I might look into using pyo, but I think I'd have to mostly start from scratch with my code then (which I am willing to do, but I don't want to give up on my current code just yet).
So. Here's my question(s): How can I optimize this to be truly polyphonic? Two notes is not enough. Should I skip the threads altogether? Can I implement the ADSR envelope in a better, less costly way? How can I clean up my messy math? What other performance bottlenecks are there, that I have overlooked? The Pygame drawing to the screen seems to be negligable at the moment, as there is virtually no difference at all if I disable it completely. Here is my code so far:
import pygame
from pygame.mixer import Sound, get_init, pre_init, get_num_channels
from array import array
import RPi.GPIO as GPIO
import alsaaudio
import time
import Adafruit_Trellis
import Adafruit_MCP3008
import math
import _thread
import os
import multiprocessing
import numpy as np
from scipy import signal as sg
import struct
#print(str(multiprocessing.cpu_count()))
os.putenv('SDL_FBDEV','/dev/fb1')
fps = pygame.time.Clock()
FRAMERATE = 100
MINSEC = 1/FRAMERATE
BLUE = ( 0, 0, 255)
WHITE = (255, 255, 255)
DARKRED = (128, 0, 0)
DARKBLUE = ( 0, 0, 128)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
DARKGREEN = ( 0, 128, 0)
YELLOW = (255, 255, 0)
DARKYELLOW = (128, 128, 0)
BLACK = ( 0, 0, 0)
PTCH = [ 1.00, 1.059633027522936, 1.122324159021407, 1.18960244648318,
1.259938837920489, 1.335168195718654, 1.414067278287462,
1.498470948012232, 1.587767584097859, 1.681957186544343,
1.782262996941896, 1.888073394495413, 2.00 ]
FREQ = { # Parsed from http://www.phy.mtu.edu/~suits/notefreqs.html
'C0': 16.35, 'Cs0': 17.32, 'D0': 18.35, 'Ds0': 19.45, 'E0': 20.60,
'F0': 21.83, 'Fs0': 23.12, 'G0': 24.50, 'Gs0': 25.96, 'A0': 27.50,
'As0': 29.14, 'B0': 30.87, 'C1': 32.70, 'Cs1': 34.65, 'D1': 36.71,
'Ds1': 38.89, 'E1': 41.20, 'F1': 43.65, 'Fs1': 46.25, 'G1': 49.00,
'Gs1': 51.91, 'A1': 55.00, 'As1': 58.27, 'B1': 61.74, 'C2': 65.41,
'Cs2': 69.30, 'D2': 73.42, 'Ds2': 77.78, 'E2': 82.41, 'F2': 87.31,
'Fs2': 92.50, 'G2': 98.00, 'Gs2': 103.83, 'A2': 110.00, 'As2': 116.54,
'B2': 123.47, 'C3': 130.81, 'Cs3': 138.59, 'D3': 146.83, 'Ds3': 155.56,
'E3': 164.81, 'F3': 174.61, 'Fs3': 185.00, 'G3': 196.00, 'Gs3': 207.65,
'A3': 220.00, 'As3': 233.08, 'B3': 246.94, 'C4': 261.63, 'Cs4': 277.18,
'D4': 293.66, 'Ds4': 311.13, 'E4': 329.63, 'F4': 349.23, 'Fs4': 369.99,
'G4': 392.00, 'Gs4': 415.30, 'A4': 440.00, 'As4': 466.16, 'B4': 493.88,
'C5': 523.25, 'Cs5': 554.37, 'D5': 587.33, 'Ds5': 622.25, 'E5': 659.26,
'F5': 698.46, 'Fs5': 739.99, 'G5': 783.99, 'Gs5': 830.61, 'A5': 880.00,
'As5': 932.33, 'B5': 987.77, 'C6': 1046.50, 'Cs6': 1108.73, 'D6': 1174.66,
'Ds6': 1244.51, 'E6': 1318.51, 'F6': 1396.91, 'Fs6': 1479.98, 'G6': 1567.98,
'Gs6': 1661.22, 'A6': 1760.00, 'As6': 1864.66, 'B6': 1975.53, 'C7': 2093.00,
'Cs7': 2217.46, 'D7': 2349.32, 'Ds7': 2489.02, 'E7': 2637.02, 'F7': 2793.83,
'Fs7': 2959.96, 'G7': 3135.96, 'Gs7': 3322.44, 'A7': 3520.00,
'As7': 3729.31, 'B7': 3951.07,
'C8': 4186.01, 'Cs8': 4434.92, 'D8': 4698.64, 'Ds8': 4978.03,
}
buttons = ['A',PTCH[9],PTCH[10],PTCH[11],'B',PTCH[6],PTCH[7],PTCH[8],'C',PTCH[3],PTCH[4],PTCH[5],PTCH[12],PTCH[0],PTCH[1],PTCH[2] ]
octaves = { 'BASE':'0', 'A':'1', 'B':'2', 'C':'3', 'AB':'4', 'AC':'5', 'BC':'6', 'ABC':'7' }
class Note(pygame.mixer.Sound):
def __init__(self, frequency, volume=.1):
self.frequency = frequency
self.oktostop = False
Sound.__init__(self, self.build_samples())
self.set_volume(volume)
def playSound(self, Aval, Dval, Sval, Rval, Acurve, Dcurve, Shold, Rcurve, fps):
self.set_volume(0)
self.play(-1)
if Aval >= MINSEC:
Alength = round(Aval*FRAMERATE)
for num in range(0,Alength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Acurve[1]*pow(num*MINSEC,Acurve[0]))/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
else:
self.set_volume(100)
if Sval <= 1 and Sval > 0 and Dval >= MINSEC:
Dlength = round(Dval*FRAMERATE)
for num in range(0,Dlength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Dcurve[1]*pow(num*MINSEC,Dcurve[0])+100)/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
elif Sval <= 1 and Sval > 0 and Dval < MINSEC:
self.set_volume(Sval)
else:
self.set_volume(0)
if Shold >= MINSEC:
Slength = round(Shold*FRAMERATE)
for num in range(0,Slength+1):
fps.tick_busy_loop(FRAMERATE)
while True:
if self.oktostop:
if Sval > 0 and Rval >= MINSEC:
Rlength = round(Rval*FRAMERATE)
for num in range(0,Rlength+1):
fps.tick_busy_loop(FRAMERATE)
volume = (Rcurve[1]*pow(num*MINSEC,Rcurve[0])+(Sval*100))/100
self.set_volume(volume)
#print(fps.get_time()," ",str(volume))
self.stop()
break
def stopSound(self):
self.oktostop = True
def build_samples(self):
Fs = get_init()[0]
f = self.frequency
sample = Fs/f
x = np.arange(sample)
# Sine wave
#y = 0.5*np.sin(2*np.pi*f*x/Fs)
# Square wave
y = 0.5*sg.square(2*np.pi*f*x/Fs)
# Pulse wave
#sig = np.sin(2 * np.pi * x)
#y = 0.5*sg.square(2*np.pi*f*x/Fs, duty=(sig + 1)/2)
# Sawtooth wave
#y = 0.5*sg.sawtooth(2*np.pi*f*x/Fs)
# Triangle wave
#y = 0.5*sg.sawtooth(2*np.pi*f*x/Fs,0.5)
# White noise
#y = 0.5*np.random.uniform(-1.000,1.000,sample)
return y
pre_init(44100, -16, 2, 2048)
pygame.init()
screen = pygame.display.set_mode((480, 320))
pygame.mouse.set_visible(False)
CLK = 5
MISO = 6
MOSI = 13
CS = 12
mcp = Adafruit_MCP3008.MCP3008(clk=CLK, cs=CS, miso=MISO, mosi=MOSI)
Asec = 1.0
Dsec = 1.0
Ssec = 1.0
Rsec = 1.0
matrix0 = Adafruit_Trellis.Adafruit_Trellis()
trellis = Adafruit_Trellis.Adafruit_TrellisSet(matrix0)
NUMTRELLIS = 1
numKeys = NUMTRELLIS * 16
I2C_BUS = 1
trellis.begin((0x70, I2C_BUS))
# light up all the LEDs in order
for i in range(int(numKeys)):
trellis.setLED(i)
trellis.writeDisplay()
time.sleep(0.05)
# then turn them off
for i in range(int(numKeys)):
trellis.clrLED(i)
trellis.writeDisplay()
time.sleep(0.05)
posRecord = {'attack': [], 'decay': [], 'sustain': [], 'release': []}
octaval = {'A':False,'B':False,'C':False}
pitch = 0
tone = None
old_tone = None
note = None
volume = 0
#m = alsaaudio.Mixer('PCM')
#mastervol = m.getvolume()
sounds = {}
values = [0]*8
oldvalues = [0]*8
font = pygame.font.SysFont("comicsansms", 22)
while True:
fps.tick_busy_loop(FRAMERATE)
#print(fps.get_time())
update = False
#m.setvolume(int(round(MCP3008(4).value*100)))
#mastervol = m.getvolume()
values = [0]*8
for i in range(8):
# The read_adc function will get the value of the specified channel (0-7).
values[i] = mcp.read_adc(i)/1000
if values[i] >= 1:
values[i] = 1
# Print the ADC values.
#print('| {0:>4} | {1:>4} | {2:>4} | {3:>4} | {4:>4} | {5:>4} | {6:>4} | {7:>4} |'.format(*values))
#print(str(pygame.mixer.Channel(0).get_busy())+" "+str(pygame.mixer.Channel(1).get_busy())+" "+str(pygame.mixer.Channel(2).get_busy())+" "+str(pygame.mixer.Channel(3).get_busy())+" "+str(pygame.mixer.Channel(4).get_busy())+" "+str(pygame.mixer.Channel(5).get_busy())+" "+str(pygame.mixer.Channel(6).get_busy())+" "+str(pygame.mixer.Channel(7).get_busy()))
Sval = values[2]*Ssec
Aval = values[0]*Asec
if Sval == 1:
Dval = 0
else:
Dval = values[1]*Dsec
if Sval < MINSEC:
Rval = 0
else:
Rval = values[3]*Rsec
if Aval > 0:
if values[4] <= MINSEC: values[4] = MINSEC
Acurve = [round(values[4]*4,3),round(100/pow(Aval,(values[4]*4)),3)]
else:
Acurve = False
if Dval > 0:
if values[5] <= MINSEC: values[5] = MINSEC
Dcurve = [round(values[5]*4,3),round(((Sval*100)-100)/pow(Dval,(values[5]*4)),3)]
else:
Dcurve = False
Shold = values[6]*4*Ssec
if Rval > 0 and Sval > 0:
if values[7] <= MINSEC: values[7] = MINSEC
Rcurve = [round(values[7]*4,3),round(-Sval*100/pow(Rval,(values[7]*4)),3)]
else:
Rcurve = False
if update:
screen.fill((0, 0, 0))
scrnvals = ["A: "+str(round(Aval,2))+"s","D: "+str(round(Dval,2))+"s","S: "+str(round(Sval,2)),"R: "+str(round(Rval,2))+"s","H: "+str(round(Shold,2))+"s","ENV: "+str(round(Aval,2)+round(Dval,2)+round(Shold,2)+round(Rval,2))+"s"]
for line in range(len(scrnvals)):
text = font.render(scrnvals[line], True, (0, 128, 0))
screen.blit(text,(60*line+40, 250))
# Width of one second in number of pixels
ASCALE = 20
DSCALE = 20
SSCALE = 20
RSCALE = 20
if Aval >= MINSEC:
if Aval <= 1:
ASCALE = 80
else:
ASCALE = 20
# Attack
for yPos in range(0,101):
xPos = round(pow((yPos/Acurve[1]),(1/Acurve[0]))*ASCALE)
posRecord['attack'].append((int(xPos) + 40, int(-yPos) + 130))
if len(posRecord['attack']) > 1:
pygame.draw.lines(screen, DARKRED, False, posRecord['attack'], 2)
if Dval >= MINSEC:
if Dval <= 1:
DSCALE = 80
else:
DSCALE = 20
# Decay
for yPos in range(100,round(Sval*100)-1,-1):
xPos = round(pow(((yPos-100)/Dcurve[1]),(1/Dcurve[0]))*DSCALE)
#print(str(yPos)+" = "+str(Dcurve[1])+"*"+str(xPos)+"^"+str(Dcurve[0])+"+100")
posRecord['decay'].append((int(xPos) + 40 + round(Aval*ASCALE), int(-yPos) + 130))
if len(posRecord['decay']) > 1:
pygame.draw.lines(screen, DARKGREEN, False, posRecord['decay'], 2)
# Sustain
if Shold >= MINSEC:
for xPos in range(0,round(Shold*SSCALE)):
posRecord['sustain'].append((int(xPos) + 40 + round(Aval*ASCALE) + round(Dval*DSCALE), int(100-Sval*100) + 30))
if len(posRecord['sustain']) > 1:
pygame.draw.lines(screen, DARKYELLOW, False, posRecord['sustain'], 2)
if Rval >= MINSEC:
if Rval <= 1:
RSCALE = 80
else:
RSCALE = 20
# Release
for yPos in range(round(Sval*100),-1,-1):
xPos = round(pow(((yPos-round(Sval*100))/Rcurve[1]),(1/Rcurve[0]))*RSCALE)
#print(str(xPos)+" = (("+str(yPos)+"-"+str(round(Sval*100))+")/"+str(Rcurve[1])+")^(1/"+str(Rcurve[0])+")")
posRecord['release'].append((int(xPos) + 40 + round(Aval*ASCALE) + round(Dval*DSCALE) + round(Shold*SSCALE), int(-yPos) + 130))
if len(posRecord['release']) > 1:
pygame.draw.lines(screen, DARKBLUE, False, posRecord['release'], 2)
posRecord = {'attack': [], 'decay': [], 'sustain': [], 'release': []}
pygame.display.update()
tone = None
pitch = 0
time.sleep(MINSEC)
# If a button was just pressed or released...
if trellis.readSwitches():
# go through every button
for i in range(numKeys):
# if it was pressed, turn it on
if trellis.justPressed(i):
print('v{0}'.format(i))
trellis.setLED(i)
if i == 0:
octaval['A'] = True
elif i == 4:
octaval['B'] = True
elif i == 8:
octaval['C'] = True
else:
pitch = buttons[i]
button = i
# if it was released, turn it off
if trellis.justReleased(i):
print('^{0}'.format(i))
trellis.clrLED(i)
if i == 0:
octaval['A'] = False
elif i == 4:
octaval['B'] = False
elif i == 8:
octaval['C'] = False
else:
sounds[i].stopSound()
# tell the trellis to set the LEDs we requested
trellis.writeDisplay()
octa = ''
if octaval['A']:
octa += 'A'
if octaval['B']:
octa += 'B'
if octaval['C']:
octa += 'C'
if octa == '':
octa = 'BASE'
if pitch > 0:
tone = FREQ['C0']*pow(2,int(octaves[octa]))*pitch
if tone:
sounds[button] = Note(tone)
_thread.start_new_thread(sounds[button].playSound,(Aval, Dval, Sval, Rval, Acurve, Dcurve, Shold, Rcurve, fps))
print(str(tone))
GPIO.cleanup()
what you are doing at the moment, is firing a sound and giving up all control, until that sound has been played. The general approach here would be to change that and process one sample at a time and push that to a buffer, that is played back periodicaly. That sample would be a sum of all your voices/signals. That way, you can decide for every sample, if a new voice is to be triggered and you can decide how long to play a note while already playing it. One way to do this would be to install a timer, that triggers a callback-function every 1/48000 s if you want a samplingrate of 48kHz.
You could still use multithreading for parallel processing, if you need to process a lot of voices, but not one thread for one voice, that would be overkill in my opinions. If that is nescessary or not depends on how much filtering/processing you do and how effective/ineffective your program is.
e.g.
sample_counter = 0
output_buffer = list()
def callback_fct():
pitch_0 = 2
pitch_1 = 4
sample_counter += 1 #time in ms
signal_0 = waveform(sample_counter * pitch_0)
signal_1 = waveform(sample_counter * pitch_1)
signal_out = signal_0 * 0.5 + signal_1 *0.5
output_buffer.append(signal_out)
return 0
if __name__ == "__main__":
call_this_function_every_ms(callback_fct)
play_sound_from_outputbuffer() #plays sound from outputbuffer by popping samples from the beginning of the list.
Something like that. the waveform() function would give you sample-values based on the actual time times the desired pitch. In C you would do all that with pointers, that overflow at the end of the Wavetable, so you won't have to deal with the question, when you should reset your sample_counter without getting glitches in the waveform (it will get real big realy soon). But I am shure, there are more "pythonic" aproaches to that. Another good reason to do this in a more low level language is speed. As soon as you involve real DSP, you will count your processor clock ticks. At that point python may just have too much overhead.
You are right that python is probably one of the bottlenecks. Commercial soft-synths are, almost without exception, written in C++ to leverage all kinds of optimization - the most pertinent of these is use of vector processing units.
There are, nonetheless, plenty of optimizations open to you in Python:
You are calculating the envelope every sample, and in an expensive way (using pow() - which is not totally hardware accelerated on ARM Cortex CPUs. You can potentially pre-compute the transfer function and simply multiply this with each sample. I also suspect that at 44.1kHz or higher, you don't need to change the envelope every sample - perhaps every 100 or so is good enough.
Your oscillators are also calculated per-sample, and as far as I can tell, per note playback. Some of them are fairly cheap, but trig functions less so, Practical soft-synths use oscillator wave-tables and phase-accumulator as an approximation.
Things you have less control of
Accuracy: You are ultimately generating a 16-bit sample. I suspect that by default Python is using double precision for everything - which has a 48-bit mantissa - about 3 times wider than you need.
Double-precision maths functions are slow on ARM Cortex A parts - significantly so in fact. Single precision can go via the VPU with many operations you would use a lot in DSP such as MAC (multiply-accumulate) taking a single cycle (although they take something like 16 cycles to clear the pipeline). Double precision is orders of magnitude slower.
#Rantanplan's answer above alludes to the kind of software architecture soft-synths are built with - one which is event driven, with a render-handler called upon periodically to supply samples. A polyphonic softsynth an do these in parallel.
In a well optimized implementation the processing of each sample for each voice would involve:
* One lookup from the wave-table (having first calculated the buffer offset using integer maths)
* multiplication by the envelope
* Mix the sample with others in the output buffer.
The key to performance is that there are almost no flow control statements in this tight loop.
Periodically, possibly per callback interval, the envelope would be updated. This parallelizes for several adjacent samples at once on CPUs with VPUs - so that would be two-ways on an ARM Cortex A part.
Related
Bin-packing/Knapsack variation: Fitting discrete data into discrete servers
I have a Python coding task that appears to be some kind of variation of either bin-packing or knapsack problem, I'm not entirely sure. I have an option that seems to work, but I don't think it's the correct solution per se, as there may be edge cases that could fail. (I'm not a CS or math student, so my knowledge in algorithms/combinatorics is quite rudimentary.) The problem A user can choose a configuration of 3 data types: Small data is 1 GB Medium data is 1.5 GB Large data is 2 GB The console app asks: "How many small pieces you need? Medium? Large?", in order. I need to fit these pieces of data into the cheapest server configuration: Small server holds 10 GB, costs $68.84 Medium server holds 24 GB, costs $140.60 Large server holds 54 GB, costs $316.09 So for example, if the user chooses a total of 20 GB of data, the function should note that it would be cheaper to use 2 small servers rather than 1 medium server. The function that I wrote primarily uses division to look for whole numbers, with floor/ceil calls wherever appropriate. I wrote blocks that sequentially go through a configuration with just L servers, then L & M, then L, M & S, etc. Here is the function: def allocate_servers(setup): '''This function allocates servers based on user's inputs.''' # setup is a dict of type {'S':int, 'M':int, 'L':int}, each is amount of data needed # Global variables that initialise to 0 global COUNTER_S global COUNTER_M global COUNTER_L # Calculate total size need total_size = setup['S'] * PLANET_SIZES['S'] + \ setup['M'] * PLANET_SIZES['M'] + \ setup['L'] * PLANET_SIZES['L'] print('\nTotal size requirement: {} GB\n'.format(total_size)) # Find cheapest server combo # 1. Using just large servers x = total_size / SERVERS['L']['cap'] # Here and later cap is server capacity, eg 54 in this case if x <= 1: COUNTER_L = 1 else: COUNTER_L = int(ceil(x)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) # this function creates a dict and calculates prices OPTIONS.append(option) reset_counters() # 2. Using large and medium servers if x <= 1: COUNTER_L = 1 else: COUNTER_L = int(floor(x)) total_size_temp = total_size - SERVERS['L']['cap'] * COUNTER_L y = total_size_temp / SERVERS['M']['cap'] if y <= 1: COUNTER_M = 1 else: COUNTER_M = int(ceil(y)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # 3. Using large, medium and small servers if x <= 1: COUNTER_L = 1 else: COUNTER_L = int(floor(x)) total_size_temp = total_size - SERVERS['L']['cap'] * COUNTER_L y = total_size_temp / SERVERS['M']['cap'] if y <= 1: COUNTER_M = 1 else: COUNTER_M = int(floor(y)) total_size_temp = total_size_temp - SERVERS['M']['cap'] * COUNTER_M z = total_size_temp / SERVERS['S']['cap'] if z <= 1: COUNTER_S = 1 else: COUNTER_S = int(ceil(z)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # 4. Using large and small servers if x <= 1: COUNTER_L = 1 else: COUNTER_L = int(floor(x)) total_size_temp = total_size - SERVERS['L']['cap'] * COUNTER_L z = total_size_temp / SERVERS['S']['cap'] if z <= 1: COUNTER_S = 1 else: COUNTER_S = int(ceil(z)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # 5. Using just medium servers y = total_size / SERVERS['M']['cap'] if y <= 1: COUNTER_M = 1 else: COUNTER_M = int(ceil(y)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # 6. Using medium and small servers if y <= 1: COUNTER_M = 1 else: COUNTER_M = int(floor(y)) total_size_temp = total_size - SERVERS['M']['cap'] * COUNTER_M z = total_size_temp / SERVERS['S']['cap'] if z <= 1: COUNTER_S = 1 else: COUNTER_S = int(ceil(z)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # 7. Using just small servers z = total_size / SERVERS['S']['cap'] if z <= 1: COUNTER_S = 1 else: COUNTER_S = int(ceil(z)) option = generate_option(COUNTER_S, COUNTER_M, COUNTER_L) OPTIONS.append(option) reset_counters() # Comparing prices of options cheapest = min(OPTIONS, key = lambda option: option['total_price']) return cheapest I have a sense that something is wrong here. For example, when I input 100 small data, 350 medium and 50 large, I get this output: Total size requirement: 725.0 GB All calculated options: [{'L': 14, 'M': 0, 'S': 0, 'total_price': 4425.259999999999}, {'L': 13, 'M': 1, 'S': 0, 'total_price': 4249.77}, {'L': 13, 'M': 1, 'S': 0, 'total_price': 4249.77}, {'L': 13, 'M': 0, 'S': 3, 'total_price': 4315.6900000000005}, {'L': 0, 'M': 31, 'S': 0, 'total_price': 4358.599999999999}, {'L': 0, 'M': 30, 'S': 1, 'total_price': 4286.84}, {'L': 0, 'M': 0, 'S': 73, 'total_price': 5025.320000000001}] For the chosen planets you need: 0 Small servers 1 Medium servers 13 Large servers Price: $4249.77 The function seems to work as intended; however, I just manually checked that, for example, if I was to take 29 medium servers that leaves us with 725-696 = 29 GB, which I could fit onto 3 small servers. Total cost for 29 medium and 3 small is $4283.92, which is cheaper than the M : 30, S : 1 option, but doesn't even make it into the list. What am I missing here? I have a feeling that my algorithm is very crude and I'm potentially missing out on more optimal solutions. Do I need to literally go through every possible option, eg for 14/13/12/11/10... large servers, with medium/small combinations also iterating through every option? EDIT: I had a limited amount of time to solve this problem, so I managed to brute force it. I added for loops in my function, iterating over every possible result. So first with maximum amount of Large servers (say, 14), then 13 Large and rest Medium, then 12 Large and rest Medium, etc... It takes a while to run with large numbers (10k of each data type took maybe like 20 seconds?), but it seems to work.
You only need to consider configurations with less than 12 small servers (because you could replace 12 small with 5 medium) and less than 27 medium servers (because you could replace 27 medium with 12 large). You can loop over the number of small and medium servers and then calculate the number of large servers as max(0, ceil((need − 10 small − 24 medium) / 54)). from math import ceil def cost(cart): s, m, l = cart return 68.84 * s + 140.6 * m + 316.09 * l def cheapest(need): return min( ( (s, m, max(0, ceil((need - 10 * s - 24 * m) / 54))) for s in range(12) for m in range(27) ), key=cost, )
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
using turtle to make animation
I am simulating an animation using turtle package in python. The effect I want to achieve is like the .gif link I attached here: https://i.stack.imgur.com/prCyQ.gif (There are 200 concentric shells in my circle. The circle's radius changes over time with my own imported data set, and each shell's radius changes proportionally, each shell has a slight different bluish color but each will also change over time based on my dataset.) Here is my code generated. import csv from turtle import * def colorConvert(digit:float): if digit == 10: digit = 'A' elif digit == 11: digit = 'B' elif digit == 12: digit = 'C' elif digit == 13: digit = 'D' elif digit == 14: digit = 'E' elif digit == 15: digit = 'F' return digit bgcolor("black") radius=[] with open('D:/SURA/Archive/water-loss/output-sorption.csv') as loss_radius: reader = csv.reader(loss_radius,delimiter=',') for row in reader: radius.append(float(row[1])) conc = [] # convert to float when accessing elements with open('D:/SURA/Archive/output-water-concentration.csv') as water: reader = csv.reader(water, delimiter=',') conc = [list(map(float,row)) for row in reader] for i in range(301): for j in range(200): conc[i][j] -= 140 shell_radius=[[0]*200]*301 max=200 radius_max=radius[0] for i in range(0,301,30): for j in range(0,200,20): radius_max = radius[i] shell_radius[i][j] = radius_max * ((max - j) / max) digit5 = int(float(conc[i][j]) // 16) digit6 = int(((float(conc[i][j]) / 16) - int(float(conc[i][j]) / 16)) * 16) color_set = '#0000'+str(colorConvert(digit5))+str(colorConvert(digit6)) up() goto(0,-shell_radius[i][j]*0.05) down() pencolor(color_set) fillcolor(color_set) begin_fill() circle(shell_radius[i][j]*0.05) end_fill() exitonclick() (the last nested for should run from 0 to 301 and 0 to 200, I shorten them for saving time to show the final visual effect~) It works, but what I want to optimize is that after one graphing outer for loop finishes executing, the graph can disappear, and starts the next out for loop. Also, is there any way to only show the final graph after each outer for loop execution finishes? So that the demo can be the same as my gif(there is no drawing process displaying). Thanks in advance!!
You can change turtle's speed while drawing (turtle speed), makes the animation slow at the end of each frame. Also you can stop for a while using Python's sleep() function, between each frame. I guess you can instant draw with the fastest speed, and use tracer(False) and hideturtle() to hide it.
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.
8 puzzle using blind search (brute-force) and manhattan distance heuristic
I developed my own program in Python for solving 8-puzzle. Initially I used "blind" or uninformed search (basically brute-forcing) generating and exploring all possible successors and using breadth-first search. When it finds the "goal" state, it basically back-tracks to the initial state and delivers (what I believe) is the most optimized steps to solve it. Of course, there were initial states where the search would take a lot of time and generate over 100,000 states before finding the goal. Then I added the heuristic - Manhattan Distance. The solutions started coming exponentially quickly and with lot less explored states. But my confusion is that some of the times, the optimized sequence generated was longer than the one reached using blind or uninformed search. What I am doing is basically this: For each state, look for all possible moves (up, down, left and right), and generate the successor states. Check if state is repeat. If yes, then ignore it. Calculate Manhattan for the state. Pick out the successor(s) with lowest Manhattan and add at the end of the list. Check if goal state. If yes, break the loop. I am not sure whether this would qualify as greedy-first, or A*. My question is, is this an inherent flaw in the Manhattan Distance Heuristic that sometimes it would not give the most optimal solution or am i doing something wrong. Below is the code. I apologize that it is not a very clean code but being mostly sequential it should be simple to understand. I also apologize for a long code - I know I need to optimize it. Would also appreciate any suggestions/guidance for cleaning up the code. Here is what it is: import numpy as np from copy import deepcopy import sys # calculate Manhattan distance for each digit as per goal def mhd(s, g): m = abs(s // 3 - g // 3) + abs(s % 3 - g % 3) return sum(m[1:]) # assign each digit the coordinate to calculate Manhattan distance def coor(s): c = np.array(range(9)) for x, y in enumerate(s): c[y] = x return c ################################################# def main(): goal = np.array( [1, 2, 3, 4, 5, 6, 7, 8, 0] ) rel = np.array([-1]) mov = np.array([' ']) string = '102468735' inf = 'B' pos = 0 yes = 0 goalc = coor(goal) puzzle = np.array([int(k) for k in string]).reshape(1, 9) rnk = np.array([mhd(coor(puzzle[0]), goalc)]) while True: loc = np.where(puzzle[pos] == 0) # locate '0' (blank) on the board loc = int(loc[0]) child = np.array([], int).reshape(-1, 9) cmove = [] crank = [] # generate successors on possible moves - new states no repeats if loc > 2: # if 'up' move is possible succ = deepcopy(puzzle[pos]) succ[loc], succ[loc - 3] = succ[loc - 3], succ[loc] if ~(np.all(puzzle == succ, 1)).any(): # repeat state? child = np.append(child, [succ], 0) cmove.append('up') crank.append(mhd(coor(succ), goalc)) # manhattan distance if loc < 6: # if 'down' move is possible succ = deepcopy(puzzle[pos]) succ[loc], succ[loc + 3] = succ[loc + 3], succ[loc] if ~(np.all(puzzle == succ, 1)).any(): # repeat state? child = np.append(child, [succ], 0) cmove.append('down') crank.append(mhd(coor(succ), goalc)) if loc % 3 != 0: # if 'left' move is possible succ = deepcopy(puzzle[pos]) succ[loc], succ[loc - 1] = succ[loc - 1], succ[loc] if ~(np.all(puzzle == succ, 1)).any(): # repeat state? child = np.append(child, [succ], 0) cmove.append('left') crank.append(mhd(coor(succ), goalc)) if loc % 3 != 2: # if 'right' move is possible succ = deepcopy(puzzle[pos]) succ[loc], succ[loc + 1] = succ[loc + 1], succ[loc] if ~(np.all(puzzle == succ, 1)).any(): # repeat state? child = np.append(child, [succ], 0) cmove.append('right') crank.append(mhd(coor(succ), goalc)) for s in range(len(child)): if (inf in 'Ii' and crank[s] == min(crank)) \ or (inf in 'Bb'): puzzle = np.append(puzzle, [child[s]], 0) rel = np.append(rel, pos) mov = np.append(mov, cmove[s]) rnk = np.append(rnk, crank[s]) if np.array_equal(child[s], goal): print() print('Goal achieved!. Successors generated:', len(puzzle) - 1) yes = 1 break if yes == 1: break pos += 1 # generate optimized steps by back-tracking the steps to the initial state optimal = np.array([], int).reshape(-1, 9) last = len(puzzle) - 1 optmov = [] rank = [] while last != -1: optimal = np.insert(optimal, 0, puzzle[last], 0) optmov.insert(0, mov[last]) rank.insert(0, rnk[last]) last = int(rel[last]) # show optimized steps optimal = optimal.reshape(-1, 3, 3) print('Total optimized steps:', len(optimal) - 1) print() for s in range(len(optimal)): print('Move:', optmov[s]) print(optimal[s]) print('Manhattan Distance:', rank[s]) print() print() ################################################################ # Main Program if __name__ == '__main__': main() Here are some of the initial states and the optimized steps calculated if you would like to check (above code would give this option to choose between blind vs Informed search) Initial states - 283164507 Blind: 19 Manhattan: 21 - 243780615 Blind: 15 Manhattan: 21 - 102468735 Blind: 11 Manhattan: 17 - 481520763 Blind: 13 Manhattan: 23 - 723156480 Blind: 16 Manhattan: 20 I have deliberately chosen examples where results would be quick (within seconds or few minutes). Your help and guidance would be much appreciated. Edit: I have made some quick changes and managed to reduce some 30+ lines. Unfortunately can't do much at this time. Note: I have hardcoded the initial state and the blind vs informed choice. Please change the value of variable "string" for initial state and the variable "inf" [I/B] for Informed/Blind. Thanks!