├── evolutionary ├── optimization │ ├── __init__.py │ └── diff_evolve.py ├── chromosomes │ ├── __init__.py │ ├── common.py │ ├── netlist.py │ └── chain.py ├── getch.py ├── circuits.py ├── plotting.py └── cgp.py ├── .gitignore ├── LICENSE ├── examples ├── lowpass.py ├── nand.py └── inverter.py └── README.md /evolutionary/optimization/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /evolutionary/chromosomes/__init__.py: -------------------------------------------------------------------------------- 1 | import chain 2 | import netlist 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /evolutionary/getch.py: -------------------------------------------------------------------------------- 1 | class _Getch: 2 | """Gets a single character from standard input. Does not echo to the 3 | screen.""" 4 | def __init__(self): 5 | try: 6 | self.impl = _GetchWindows() 7 | except ImportError: 8 | self.impl = _GetchUnix() 9 | 10 | def __call__(self): return self.impl() 11 | 12 | 13 | class _GetchUnix: 14 | def __init__(self): 15 | import tty, sys 16 | 17 | def __call__(self): 18 | import sys, tty, termios 19 | fd = sys.stdin.fileno() 20 | old_settings = termios.tcgetattr(fd) 21 | try: 22 | tty.setraw(sys.stdin.fileno()) 23 | ch = sys.stdin.read(1) 24 | finally: 25 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 26 | return ch 27 | 28 | 29 | class _GetchWindows: 30 | def __init__(self): 31 | import msvcrt 32 | 33 | def __call__(self): 34 | import msvcrt 35 | return msvcrt.getch() 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Henrik Forstén 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /evolutionary/circuits.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | 4 | 5 | class spice_thread(threading.Thread): 6 | def __init__(self, spice_in): 7 | threading.Thread.__init__(self) 8 | self.spice_in = spice_in 9 | self.result = None 10 | if self.spice_in!=None: 11 | self.spice = subprocess.Popen(['ngspice','-n'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) 12 | def run(self): 13 | if self.spice_in!=None: 14 | output = self.spice.communicate(self.spice_in) 15 | self.result = (output[1],self.parse_output(output[0])) 16 | 17 | def parse_output(self,output): 18 | value={} 19 | output=output.split('\n') 20 | index=1 21 | current = () 22 | for line in xrange(len(output)): 23 | temp=output[line].replace(',','').split() 24 | if len(temp)>0: 25 | if temp[0]=='Index': 26 | if line+22 and current!=(): 34 | try: 35 | float(temp[1]),float(temp[2]) 36 | except: 37 | continue 38 | index+=1 39 | value[current][0].append(float(temp[1])) 40 | value[current][1].append(float(temp[2])) 41 | return value 42 | -------------------------------------------------------------------------------- /evolutionary/chromosomes/common.py: -------------------------------------------------------------------------------- 1 | import random 2 | from math import log10 3 | 4 | def value_dist(val): 5 | if len(val)==3: 6 | return val[3](*val[:2]) 7 | else: 8 | if val[0]>0: 9 | return log_dist(*val) 10 | else: 11 | return random.uniform(*val) 12 | 13 | def argmin(x): 14 | if len(x) < 2: 15 | return x[0] 16 | bestv,idx = x[0],0 17 | for e,i in enumerate(x[1:],1): 18 | if ibestv: 29 | bestv = i 30 | idx = e 31 | return idx 32 | 33 | def normalize_list(lst): 34 | """Return normalized list that sums to 1""" 35 | s = sum(lst) 36 | return [i/float(s) for i in lst] 37 | 38 | def multipliers(x): 39 | """Convert values with si multipliers to numbers""" 40 | try: 41 | return float(x) 42 | except: 43 | pass 44 | try: 45 | a = x[-1] 46 | y = float(x[:-1]) 47 | endings = {'G':9,'Meg':6,'k':3,'m':-3,'u':-6,'n':-9,'p':-12,'s':0} 48 | return y*(10**endings[a]) 49 | except: 50 | raise ValueError("I don't know what {} means".format(x)) 51 | 52 | def log_dist(a,b): 53 | """Generates exponentially distributed random numbers. 54 | Gives better results for resistor, capacitor and inductor values 55 | than the uniform distribution.""" 56 | if a <= 0 or a>b: 57 | raise ValueError("Value out of range. Valid range is (0,infinity).") 58 | return 10**(random.uniform(log10(a),log10(b))) 59 | 60 | def same(x): 61 | #True if all elements are same 62 | return reduce(lambda x,y:x==y,x) 63 | 64 | def lst_random(lst, probs): 65 | """Return element[i] with probability probs[i].""" 66 | s = sum(probs) 67 | r = random.uniform(0,s) 68 | t = 0 69 | for i in xrange(len(lst)): 70 | t += probs[i] 71 | if r <= t: 72 | return lst[i] 73 | return lst[-1]#Because of rounding errors or something? 74 | -------------------------------------------------------------------------------- /examples/lowpass.py: -------------------------------------------------------------------------------- 1 | from math import log 2 | 3 | title = 'Low-pass filter' 4 | 5 | #List of simulations 6 | #Put SPICE simulation options between.control and .endc lines 7 | # 8 | spice_commands=[ 9 | """ 10 | .control 11 | ac dec 30 10 1e5 12 | print vdb(out) 13 | option rshunt = 1e12 14 | .endc 15 | Vin in 0 ac 1 16 | Rload out 0 {aunif(10k,500)} 17 | cload out 0 100p 18 | """ 19 | ] 20 | 21 | inputs = ['in'] 22 | outputs = ['out'] 23 | 24 | #Dictionary of the availabe parts 25 | parts = {'R':{'nodes':2,'value':(0.1,1e6),}, 26 | 'C':{'nodes':2,'value':(1e-12,1e-5)}, 27 | 'L':{'nodes':2,'value':(1e-9,1e-3)}, 28 | } 29 | 30 | def _fitness_function1(f,k,**kwargs): 31 | """k is the name of measurement. eq. v(out)""" 32 | if k[0]=='v': 33 | #-100dB/decade 34 | return -43.43*log(f)+300 if f>=1000 else 0 35 | elif k[0]=='i': 36 | #Goal for current use is 0 37 | return 0 38 | 39 | def _constraint1(f,x,k,**kwargs): 40 | if k[0]=='v': 41 | if f>8000: 42 | return x <= -20 43 | if f>1000: 44 | return x<=0.5 45 | elif f<100: 46 | return -5=3 and k[1:3] == 'db': 55 | plt.ylabel("Output (dB)") 56 | elif k[0]=='v': 57 | plt.ylabel("Output (V)") 58 | elif k[0]=='i': 59 | plt.ylabel("Output (A)") 60 | 61 | plt.savefig(path_join(output_path,strftime("%Y-%m-%d %H:%M:%S")+'-'+k+'-'+str(name)+'.png')) 62 | 63 | data = input() 64 | save_plot(*data) 65 | -------------------------------------------------------------------------------- /examples/nand.py: -------------------------------------------------------------------------------- 1 | #TTL NAND-gate optimization 2 | #Very slow simulation and usually takes somewhere close to 30-40 generations 3 | #to find a working gate 4 | title = 'nand' 5 | 6 | #Put SPICE device models used in the simulations here. 7 | models=""" 8 | .model 2N3906 PNP(Is=455.9E-18 Xti=3 Eg=1.11 Vaf=33.6 Bf=204 Ise=7.558f 9 | + Ne=1.536 Ikf=.3287 Nk=.9957 Xtb=1.5 Var=100 Br=3.72 10 | + Isc=529.3E-18 Nc=15.51 Ikr=11.1 Rc=.8508 Cjc=10.13p Mjc=.6993 11 | + Vjc=1.006 Fc=.5 Cje=10.39p Mje=.6931 Vje=.9937 Tr=10n Tf=181.2p 12 | + Itf=4.881m Xtf=.7939 Vtf=10 Rb=10, level=1) 13 | .model 2N3904 NPN(Is=6.734f Xti=3 Eg=1.11 Vaf=74.03 Bf=416.7 Ne=1.259 14 | + Ise=6.734f Ikf=66.78m Xtb=1.5 Br=.7371 Nc=2 Isc=0 Ikr=0 Rc=1 15 | + Cjc=3.638p Mjc=.3085 Vjc=.75 Fc=.5 Cje=4.493p Mje=.2593 Vje=.75 16 | + Tr=239.5n Tf=301.2p Itf=.4 Vtf=4 Xtf=2 Rb=10, level=1) 17 | """ 18 | 19 | #5V power supply with series resistance of 10 ohms. 20 | #Bypass capacitor with series resistance of 0.1 ohms. 21 | #10k ohm and 100pF of load 22 | common=""" 23 | Vc na 0 5 24 | Rc na vc 10 25 | cv na nb 10n 26 | rcv nb vc 100m 27 | rload out 0 10k 28 | cload out 0 100p 29 | """ 30 | 31 | inputs = ['in1','in2'] 32 | outputs = ['out'] 33 | special_nodes = ['vc'] 34 | 35 | #List of simulations 36 | #Put SPICE simulation options between.control and .endc lines 37 | spice_commands=[ 38 | #Functionality 39 | """ 40 | .control 41 | tran 5n 100u 42 | print v(out) 43 | print i(vc) 44 | print i(Vpwl1) 45 | print i(Vpwl2) 46 | .endc 47 | Vpwl1 in1 0 0 PWL(0 0 20u 0 20.05u 5 40u 5 40.05u 0 50u 0 50.05u 5 60u 5 60.05u 0 70u 0 70.05u 5) 48 | Vpwl2 in2 0 0 PWL(0 0 10u 0 10.05u 5 20u 5 20.05u 0 30u 0 30.05u 5 40u 5 40.05u 0 60u 0 60.05u 5) 49 | """, 50 | #Input isolation test 1 51 | """ 52 | .control 53 | tran 10u 20u 54 | print v(in1) 55 | .endc 56 | Vin in2 0 0 PWL(0 0 5u 0 15u 5 20u 5) 57 | rin in1 0 100k 58 | """ 59 | , 60 | #Input isolation test 2 61 | """ 62 | .control 63 | tran 10u 20u 64 | print v(in2) 65 | .endc 66 | Vin in1 0 0 PWL(0 0 5u 0 15u 5 20u 5) 67 | rin in2 0 100k 68 | """ 69 | 70 | ] 71 | 72 | #Dictionary of the availabe parts 73 | parts = {'R':{'nodes':2,'value':(1,1e6)},#Resistors 74 | #'C':{'nodes':2,'value':(1e-12,1e-7)},#Capacitors 75 | #'L':{'nodes':2,'value':(1e-10,1e-5)},#Inductors 76 | 'Q':{'nodes':3,'model':('2N3904','2N3906')},#NPN/PNP transistors 77 | } 78 | 79 | def _goal(f,k,**kwargs): 80 | """k is the name of measurement. eq. v(out)""" 81 | #Functionality 82 | if k=='v(out)': 83 | if (30.05e-670.05e-6): 84 | return 0 85 | return kwargs['extra'][0] 86 | #Current 87 | elif k[0]=='i': 88 | #Goal for current use is 0 89 | return 0 90 | #Input isolation 91 | elif k in ('v(in1)','v(in2)'): 92 | return 0 93 | 94 | def _constraint0(f,x,k,**kwargs): 95 | if k[0] == 'v': 96 | if (32e-672e-6): 97 | return x<1 98 | if f<20e-6 or (45e-6x>kwargs['extra'][0]-0.1 100 | return True 101 | 102 | def _weight(x,**kwargs): 103 | """Weighting function for scoring""" 104 | #Low weight when glitches are allowed 105 | if abs(x-20e-6)<8e-6: 106 | return 0.004 107 | if abs(x-60e-6)<8e-6: 108 | return 0.004 109 | #High weight on the edges 110 | if 0kwargs['extra'][0]+0.2 104 | elif f>=kwargs['extra'][0]+0.2: 105 | return xkwargs['extra'][0]+0.2 119 | if k[0]=='v' and f>350e-9: 120 | #And below it after the transition on the input 121 | return x350e-9: 133 | return x>kwargs['extra'][0]+0.2 134 | if k[0]=='i': 135 | return abs(x)<10e-3+0.1/kwargs['generation']**0.5 136 | return True 137 | 138 | population=500#Too small population might not converge, but is faster to simulate 139 | max_parts=10#Maximum number of parts 140 | 141 | mutation_rate=0.70 142 | crossover_rate=0.10 143 | 144 | #Because constraint functions change every generation score might 145 | #increase even when better circuit is found 146 | plot_every_generation = True 147 | fitness_function=[_goalinv,_transient_goal_inv,_transient_goal_inv2] 148 | constraints=[_constraint0,_constraint1,_constraint2] 149 | constraint_weight=[1000,1000,1000] 150 | constraint_free_generations = 1 151 | gradual_constraints = True 152 | constraint_ramp = 30 153 | fitness_weight=[{'v(out)':lambda f,**kwargs: 15 if (f<0.5 or f>4.5) else 0.1,'i(vc)':lambda f,**kwargs:kwargs['generation']*100,'i(vin)':lambda f,**kwargs:kwargs['generation']*100},{'v(out)':2,'i(vc)':lambda f,**kwargs:kwargs['generation']*100,'i(vin)':1000},{'v(out)':2,'i(vc)':lambda f,**kwargs:kwargs['generation']*100,'i(vin)':1000}] 154 | 155 | extra_value=[(0.5,4.5)]#This is used as transition value 156 | 157 | plot_titles=[{'v(out)':"DC sweep",'i(vc)':"Current from power supply",'i(vin)':'Current from logic input'},{'v(out)':'Step response'},{'v(out)':'Step response'}] 158 | plot_yrange={'v(out)':(-0.5,5.5),'i(vin)':(-0.05,0.01),'i(vc)':(-0.05,0.01)} 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | evolutionary-circuits 2 | ===================== 3 | 4 | Automatically generates analog circuits using evolutionary algorithms. 5 | 6 | Requires ngspice, python and pypy (Yes, both of them). Also probably only works on a linux OS. 7 | 8 | Interesting stuff happens in cgp.py, circuits.py is used to communicate with 9 | ngspice, plotting.py is used to implement plotting with matplotlib when running 10 | with pypy. getch.py gets a single character from output. 11 | 12 | Work is currently in progress and documentation is missing. This is 13 | currently hard to use and requires a knowledge of how to use SPICE. 14 | 15 | Simulation can be stopped with a keyboard interrupt. Progress is saved after 16 | every generation and can be continued by just running the script again. 17 | 18 | #Installing 19 | 20 | ##ngspice 21 | 22 | Download [ngpspice](http://ngspice.sourceforge.net/) either the git version or the 23 | current stable release, whichever you want. Git release often has performance 24 | improvements and possibly new features, but it also comes with the best bugs. If 25 | you are new to using SPICE it might be better to install latest stable 26 | version(As of 2012-09-10 this is version 24). 27 | 28 | Compile and install ngspice with following commands: 29 | 30 | $ mkdir release 31 | $ cd release 32 | $ ../configure --enable-xspice --disable-debug 33 | $ make 34 | $ sudo make install 35 | 36 | #Usage 37 | 38 | The program is used by saving simulation settings in separate file and 39 | running it with command: "pypy cgp.py ". See examples 40 | folder for example scripts. 41 | 42 | Using pypy is necessary. CPython raises PicklingError, because it can't 43 | pickle functions inside classes, which is required for multithreading. 44 | 45 | ##Simulation settings 46 | 47 | Every name starting with underscore("\_") is ignored by cgp.py and they can be used for 48 | internal functions in settings file. 49 | 50 | Required settings are: 51 | * title: Title of the simulation, this will be the name of the folder where 52 | output is placed. 53 | * max\_parts: Maximum number of devices allowed. 54 | * spice\_commands: SPICE simulation commands, can be string if there is only one 55 | simulation or list of string for more than one simulation. 56 | * parts: A dictionary of available parts. 57 | * fitness\_function: The goal function, that circuits are ranked against. 58 | 59 | Optional settings are: 60 | * common: Common constructs in SPICE simulation, eg. input resistance, output 61 | load... 62 | * models: String of SPICE models. 63 | * constraints: One function or a list of functions for constraints. Some entries 64 | in list can be None. Length of list must equal the number of SPICE 65 | simulations. 66 | * population: Number of circuits in one generation. Default is 1000. 67 | * nodes: Number of nodes where parts can be attached. Default is same as 68 | max\_parts. 69 | * elitism: Number of circuits copied straight into a new generation. Default is 70 | 1. 71 | * mutation\_rate: Probability of mutations. Default is 0.7. 72 | * crossover\_rate: Probability of crossovers. Default is 0.2. 73 | * fitness\_weight: Weightings for fitness functions. Can be either numbers or 74 | functions of type "lambda x,\*\*kwargs", where x is simulation x-axis value, 75 | kwargs has keys: "extra\_value", "generation", possibly more as they are added. 76 | List of dictionaries. One list elements corresponds to one 77 | SPICE simulation. Dictionary keys are measurements('v(n2)','i(n1')...), and 78 | values are the weights the measurement is multiplied. Value can also be 79 | a function that takes input value and returns a number. 80 | * extra\_value: List of tuples that are minimum and maximum of extra values that chromosome can hold. This is returned 81 | to fitness function as argument extra\_value. This can be used as example for 82 | transition voltage of an inverter. 83 | * log\_file: Filename where simulation log is saved. 84 | * plot\_titles: List of dictionaries of plot titles. 85 | * plot\_yrange: List of dictionaries of plot Y-axis ranges. Can be useful if you 86 | turn the output plots into an animation, this avoids the rescaling of the 87 | axes. 88 | * selection\_weight: Higher values make better performing circuits being picked 89 | more often and smaller values make selection more fair, 0.0 makes selection completely random. 90 | Default is 1.0 and values should be bigger than zero. Usable range us probably from 0.5 to 1.5. Don't set this too high or entropy in the gene pool is lost and genetic algorithm doesn't converge. 91 | You should only touch this if you know what you are doing. 92 | * constraint\_weight: Weights for constraints, similar to fitness\_weight. 93 | Score added is percentage of values not passing times this score. 94 | * max\_mutations: Maximum number of mutations without re-evaluating the fitness 95 | function. 96 | * constraint\_free\_generations: Number of generations before constraint 97 | functions are enabled. 98 | * gradual\_constraints: Apply constraint function scores gradually, default is 99 | True. 100 | * constraint\_ramp: Number of generations before constraint scores are at 101 | maximum levels. Does nothing if gradual\_constraints if False. 102 | * random\_circuits: Percentage of random circuits in new generation. Default is 103 | 0.01(1%). 104 | * plot\_every\_generation: True to plot every generation even if the best score 105 | is same or worse than the last generation. Useful is weights increase when 106 | current generation increases, doesn't need to be enabled for 107 | "gradual\_constraints" to work properly. Default is False. 108 | * default\_scoring: Use default scoring(Squared error difference and weighting). 109 | * custom\_scoring: User defined scoring function. None if none is given. Both 110 | user defined and default scoring can be active at the same time, in this case 111 | scores are summed. Function should take two arguments: "result" and 112 | \*\*kwargs. result is dictionary of measurements, each dictionary value is 113 | tuple of two lists: simulation x-axis and y-axis values. Example: 114 | {'v(n2)':[0.0,0.1,0.2],[1.0,1.1,1.2]}. Should return a score that has a type 115 | of float. 116 | * timeout: SPICE simulation default timeout. This is raised automatically if 117 | default is too low. 118 | * seed: Seed circuits to be added in the first generation. All of the devices 119 | need to be in the parts dictionary. Can be a list or string. 120 | * seed\_copies: Copies of seed circuits. 121 | 122 | First you need to decide what components you want to allow and add them to 123 | the "parts" dictionary. The dictionary key is the name of component in SPICE, and the value 124 | should be an another dictionary with contents: 125 | 126 | 'nodes': Number of leads in the component 127 | 'value': True/False, component has a value(Example: True for resistors and 128 | capacitors, their values are resistance and capacitance. 129 | False for example transistors) 130 | 'min': Exponent of the components minimum value. Minimum value will be 131 | 10^(min) 132 | 'max': Exponent of the components maximum value. 133 | 'spice': Extra options for spice. This is appended to the end of components 134 | description in spice. This is used for example transistors model. 135 | 136 | Next you need SPICE simulation commands and a optional part models. Ground node is hard coded in SPICE to be 0. 137 | 138 | Add "print "(eg. "print v(n2)" or "print i(n2)") in the spice commands to get an output to the program. You can print more than one measurement from one spice simulation. 139 | 140 | Other settings you should specify are the fitness function or a list of them if you have more than one simulation. These are the goals of the evolution, that circuits are scored against. 141 | 142 | Definition of the fitness function is: 143 | 144 | def fitness\_function(a,b,**kwargs) 145 | 146 | First argument is the input value of the measurement(eg. voltage, current, 147 | decibels, phase...). Second is the name of the SPICE measurement(eg. v(n2), 148 | vdb(n3), i(vin)). \*\*kwargs has the optional extra\_value of the chromosome and 149 | the current generation. 150 | 151 | For example a constant fitness function: 152 | 153 | def goal(v,k,**kwargs): 154 | """v is input, either voltage, frequency, time... depends on spice 155 | simulation. k is the name of node being measured""" 156 | return 1 157 | 158 | Another fitness function: A spice simulation with more than one measurement and example use of \*\*kwargs. This 159 | example has a current and voltage measurements: 160 | 161 | def goal(v,k,**kwargs): 162 | if k[0]=='v': #Voltage 163 | return kwargs['extra_value'][0] #1V 164 | elif k[0]=='i': #Current 165 | return 1e-3 if kwargs['generation]<10 else 0 166 | 167 | Discontinuous fitness function that is 0 before time is 100ns and 5 after it: 168 | 169 | def goal(t,k,**kwargs): 170 | #t is now time 171 | return 0 if t<=100e-9 else 5 172 | 173 | Fitness weights are either constants that scores are multiplied with, or 174 | functions that individual values of spice simulation are weighted with, if you want 175 | to for example weight some part of the results higher than other parts. They 176 | should be in list of dictionaries where number of elements in list is same as 177 | number of spice simulations. The nth element is used with nth spice simulation. Keys 178 | of the dictionaries are names of measurements(eg. 'v(n2)') and values are 179 | weights. 180 | 181 | For example a constants weights with 4 simulations with second one having two 182 | measurements: 183 | 184 | fitness_weight=[{'v(n2)':1},{'v(n2)':1,'i(vin)':50},{'v(n2)':1},{'v(n2)':0.05}] 185 | 186 | 187 | See examples folder for examples. Due to the rapid development examples might not 188 | always work, but I try my best to keep them up to date. 189 | -------------------------------------------------------------------------------- /evolutionary/optimization/diff_evolve.py: -------------------------------------------------------------------------------- 1 | import random 2 | #Differential evolution optimizer based on Scipy implementation: 3 | #http://python-scipy.sourcearchive.com/documentation/0.6.0/classscipy_1_1sandbox_1_1rkern_1_1diffev_1_1DiffEvolver.html 4 | 5 | def argmin(x): 6 | if len(x) < 2: 7 | return x[0] 8 | bestv,idx = x[0],0 9 | for e,i in enumerate(x[1:],1): 10 | if i 0.5 27 | if None, then calculated from pop0 using a heuristic 28 | strategy -- tuple specifying the differencing/crossover strategy 29 | The first element is one of 'rand', 'best', 'rand-to-best' to specify 30 | how to obtain an initial trial vector. 31 | The second element is either 1 or 2 (or only 1 for 'rand-to-best') to 32 | specify the number of difference vectors to add to the initial trial. 33 | The third element is (currently) 'bin' to specify binomial crossover. 34 | eps -- if the maximum and minimum function values of a given generation are 35 | with eps of each other, convergence has been achieved. 36 | 37 | DiffEvolver.frombounds(func, lbound, ubound, npop, crossover_rate=0.5, 38 | scale=None, strategy=('rand', 2, 'bin'), eps=1e-6) 39 | Randomly initialize the population within given rectangular bounds. 40 | lbound -- lower bound vector 41 | ubound -- upper bound vector 42 | npop -- size of population 43 | 44 | Public Methods 45 | -------------- 46 | solve(newgens=100) 47 | Run the minimizer for newgens more generations. Return the best parameter 48 | vector from the whole run. 49 | 50 | Public Members 51 | -------------- 52 | best_value -- lowest function value in the history 53 | best_vector -- minimizing vector 54 | best_val_history -- list of best_value's for each generation 55 | best_vec_history -- list of best_vector's for each generation 56 | population -- current population 57 | pop_values -- respective function values for each of the current population 58 | generations -- number of generations already computed 59 | func, args, crossover_rate, scale, strategy, eps -- from constructor 60 | """ 61 | 62 | def __init__(self, func, pop0, args=(), crossover_rate=0.5, scale=None, 63 | strategy=('rand', 2, 'bin'), eps=1e-6, lbound=None, ubound=None): 64 | self.func = func 65 | self.population = pop0 66 | self.npop, self.ndim = len(self.population),len(self.population[0]) 67 | self.args = args 68 | self.crossover_rate = crossover_rate 69 | self.strategy = strategy 70 | self.eps = eps 71 | self.lbound = lbound 72 | self.ubound = ubound 73 | self.bounds = lbound!=None and ubound!=None 74 | 75 | self.pop_values = [self.func(m, *args) for m in self.population] 76 | bestidx = argmin(self.pop_values) 77 | self.best_vector = self.population[bestidx] 78 | self.best_value = self.pop_values[bestidx] 79 | 80 | if scale is None: 81 | self.scale = self.calculate_scale() 82 | else: 83 | self.scale = scale 84 | 85 | self.generations = 0 86 | self.best_val_history = [] 87 | self.best_vec_history = [] 88 | 89 | self.jump_table = { 90 | ('rand', 1, 'bin'): (self.choose_rand, self.diff1, self.bin_crossover), 91 | ('rand', 2, 'bin'): (self.choose_rand, self.diff2, self.bin_crossover), 92 | ('best', 1, 'bin'): (self.choose_best, self.diff1, self.bin_crossover), 93 | ('best', 2, 'bin'): (self.choose_best, self.diff2, self.bin_crossover), 94 | ('rand-to-best', 1, 'bin'): 95 | (self.choose_rand_to_best, self.diff1, self.bin_crossover), 96 | } 97 | 98 | def clear(self): 99 | self.best_val_history = [] 100 | self.best_vec_history = [] 101 | self.generations = 0 102 | self.pop_values = [self.func(m, *self.args) for m in self.population] 103 | 104 | def frombounds(cls, func, lbound, ubound, npop, crossover_rate=0.5, 105 | scale=None, x0=None, strategy=('rand', 2, 'bin'), eps=1e-6): 106 | if x0==None: 107 | pop0 = [[random.random()*(ubound[i]-lbound[i]) + lbound[i] for i in xrange(len(lbound))] for c in xrange(npop)] 108 | else: 109 | pop0 = [0]*npop 110 | for e,x in enumerate(x0): 111 | if len(x)!=len(lbound): 112 | raise ValueError("Dimension of x0[{}] is incorrect".format(e)) 113 | if any(not lbound[i]<=x[i]<=ubound[i] for i in xrange(len(lbound))): 114 | raise ValueError("x0[{}] not inside the bounds.".format(e)) 115 | for i in xrange(e,npop,len(x0)): 116 | pop0[i] = x 117 | delta = 0.3 118 | pop0 = [[delta*(random.random()*(ubound[i]-lbound[i]) + lbound[i])+p[i] for i in xrange(len(lbound))] for p in pop0] 119 | pop0 = [[lbound[i] if p[i]ubound[i] else p[i]) for i in xrange(len(lbound))] for p in pop0] 120 | #Make sure to include x0 121 | pop0[:len(x0)] = x0 122 | return cls(func, pop0, crossover_rate=crossover_rate, scale=scale, 123 | strategy=strategy, eps=eps, lbound=lbound, ubound=ubound) 124 | frombounds = classmethod(frombounds) 125 | 126 | def calculate_scale(self): 127 | rat = abs(max(self.pop_values)/self.best_value) 128 | rat = min(rat, 1./rat) 129 | return max(0.3, 1.-rat) 130 | 131 | def bin_crossover(self, oldgene, newgene): 132 | new = oldgene[:] 133 | for i in xrange(len(oldgene)): 134 | if random.random() < self.crossover_rate: 135 | new[i] = newgene[i] 136 | return new 137 | 138 | def select_samples(self, candidate, nsamples): 139 | possibilities = range(self.npop) 140 | possibilities.remove(candidate) 141 | random.shuffle(possibilities) 142 | return possibilities[:nsamples] 143 | 144 | def diff1(self, candidate): 145 | i1, i2 = self.select_samples(candidate, 2) 146 | y = [(self.population[i1][c] - self.population[i2][c]) for c in xrange(self.ndim)] 147 | y = [self.scale*i for i in y] 148 | return y 149 | 150 | def diff2(self, candidate): 151 | i1, i2, i3, i4 = self.select_samples(candidate, 4) 152 | y = ([(self.population[i1][c] - self.population[i2][c]+self.population[i3][c] - self.population[i4][c]) for c in xrange(self.ndim)]) 153 | y = [self.scale*i for i in y] 154 | return y 155 | 156 | def choose_best(self, candidate): 157 | return self.best_vector 158 | 159 | def choose_rand(self, candidate): 160 | i = self.select_samples(candidate, 1)[0] 161 | return self.population[i] 162 | 163 | def choose_rand_to_best(self, candidate): 164 | return ((1-self.scale) * self.population[candidate] + 165 | self.scale * self.best_vector) 166 | 167 | def get_trial(self, candidate): 168 | chooser, differ, crosser = self.jump_table[self.strategy] 169 | chosen = chooser(candidate) 170 | diffed = differ(candidate) 171 | new = [chosen[i] + diffed[i] for i in xrange(self.ndim)] 172 | trial = crosser(self.population[candidate],new) 173 | if self.bounds: 174 | if random.random() < 0.2: 175 | trial = self.hug_bounds(trial) 176 | else: 177 | trial = self.mirror_bounds(trial) 178 | return trial 179 | 180 | def mirror_bounds(self,trial): 181 | """Mirrors values over bounds back to bounded area, 182 | or randomly generates a new coordinate if mirroring failed.""" 183 | for i in xrange(self.ndim): 184 | if trial[i]self.ubound[i]: 189 | trial[i] = 2*self.ubound[i]-trial[i] 190 | if trial[i]>self.ubound[i]: 191 | trial[i] = random.random()*(self.ubound[i]-self.lbound[i]) + self.lbound[i] 192 | return trial 193 | 194 | def hug_bounds(self,trial): 195 | """Rounds values over bounds to bounds""" 196 | for i in xrange(self.ndim): 197 | if trial[i]self.ubound[i]: 200 | trial[i] = self.ubound[i] 201 | return trial 202 | 203 | def converged(self): 204 | return max(self.pop_values) - min(self.pop_values) <= self.eps 205 | 206 | def solve(self, newgens=100): 207 | """Run for newgens more generations. 208 | 209 | Return best parameter vector from the entire run. 210 | """ 211 | for gen in xrange(self.generations+1, self.generations+newgens+1): 212 | for candidate in range(self.npop): 213 | trial = self.get_trial(candidate) 214 | trial_value = self.func(trial, *self.args) 215 | if trial_value < self.pop_values[candidate]: 216 | self.population[candidate] = trial 217 | self.pop_values[candidate] = trial_value 218 | if trial_value < self.best_value: 219 | self.best_vector = trial 220 | self.best_value = trial_value 221 | self.best_val_history.append(self.best_value) 222 | self.best_vec_history.append(self.best_vector) 223 | if self.converged(): 224 | break 225 | self.generations = gen 226 | return self.best_vector 227 | -------------------------------------------------------------------------------- /evolutionary/chromosomes/netlist.py: -------------------------------------------------------------------------------- 1 | from common import * 2 | 3 | class Device: 4 | """Represents a single component""" 5 | def __init__(self,name,nodes,cost=0,*args): 6 | #Name of the component(eg. "R1") 7 | self.spice_name = name 8 | #N-tuple of values 9 | self.values = args 10 | self.nodes = nodes 11 | self.cost = cost 12 | def __repr__(self): 13 | return self.spice_name+str(id(self))+' '+' '.join(map(str,self.nodes))+' '+' '.join(map(str,*self.values)) 14 | 15 | def random_element(parts,node_list,fixed_node=None): 16 | #Return random circuit element from parts list 17 | name = random.choice(parts.keys()) 18 | part = parts[name] 19 | spice_line = [] 20 | if 'value' in part.keys(): 21 | minval,maxval = part['value'][:2] 22 | spice_line.append(log_dist(minval,maxval)) 23 | nodes = [random.choice(node_list) for i in xrange(part['nodes'])] 24 | while same(nodes): 25 | nodes = [random.choice(node_list) for i in xrange(part['nodes'])] 26 | if fixed_node!=None: 27 | nodes[0]=fixed_node 28 | random.shuffle(nodes) 29 | if 'model' in part: 30 | if type(part['model'])!=str: 31 | spice_line.append(random.choice(part['model'])) 32 | else: 33 | spice_line.append(part['model']) 34 | if 'cost' in part: 35 | cost = part['cost'] 36 | else: 37 | cost = 0 38 | return Device(name,nodes,cost,spice_line) 39 | 40 | def mutate_value(element,parts,rel_amount=None): 41 | i = random.randint(0,len(element.values)-1) 42 | val = element.values[i] 43 | name = element.spice_name 44 | if rel_amount==None: 45 | try: 46 | val[i] = log_dist(parts[name]['value'][0],parts[name]['value'][1]) 47 | except: 48 | return element 49 | else: 50 | try: 51 | temp = val[i]*(2*random.random-1)*rel_amount 52 | if parts[name]['value'][0]<=temp<=parts[name]['value'][1]: 53 | val[i] = temp 54 | except: 55 | return element 56 | try: 57 | cost = parts[element.spice_name]['cost'] 58 | except KeyError: 59 | cost = 0 60 | return Device(element.spice_name,element.nodes,cost,val) 61 | 62 | class Chromosome: 63 | """Class that contains one circuit and all of it's parameters""" 64 | def __init__(self,max_parts,parts_list,nodes,extra_value=None): 65 | #Maximum number of components in circuit 66 | self.max_parts = max_parts 67 | #List of nodes 68 | self.nodes = nodes 69 | self.parts_list = parts_list 70 | #Generates randomly a circuit 71 | self.elements = [random_element(self.parts_list,self.nodes) for i in xrange(random.randint(1,int(0.75*max_parts)))] 72 | self.extra_range = extra_value 73 | if extra_value!=None: 74 | self.extra_value = [random.uniform(*i) for i in self.extra_range] 75 | else: 76 | self.extra_value = None 77 | 78 | def __repr__(self): 79 | return '\n'.join(map(str,self.elements)) 80 | 81 | def get_connected_node(self): 82 | """Randomly returns one connected node""" 83 | if len(self.elements)>0: 84 | device = random.choice(self.elements) 85 | return random.choice(device.nodes) 86 | else: 87 | return 'n1' 88 | 89 | def value_bounds(self): 90 | bounds = [] 91 | for e in self.elements: 92 | if 'value' in self.parts_list[e.spice_name]: 93 | bounds.append(self.parts_list[e.spice_name]['value'][:2]) 94 | if self.extra_value != None: 95 | bounds.extend(self.extra_range) 96 | return bounds 97 | 98 | def get_values(self): 99 | values = [] 100 | for e in self.elements: 101 | if 'value' in self.parts_list[e.spice_name]: 102 | values.append(e.values[0][0]) 103 | if self.extra_value != None: 104 | values.extend(self.extra_value) 105 | return values 106 | 107 | def set_values(self, values): 108 | i = 0 109 | for e in self.elements: 110 | if 'value' in self.parts_list[e.spice_name]: 111 | e.values = ([values[i]],) 112 | i += 1 113 | if self.extra_value != None: 114 | self.extra_value = values[i:] 115 | return None 116 | 117 | def crossover(self, other): 118 | #if len(self.instructions)r2: 125 | r1,r2=r2,r1 126 | if r==0: 127 | #Two point crossover 128 | self.elements = self.elements[:r1]+other.elements[r1:r2]+self.elements[r2:] 129 | self.elements = self.elements[:self.max_parts] 130 | else: 131 | #Single point crossover 132 | self.elements = self.elements[:r1]+other.elements[r1:] 133 | self.elements = self.elements[:self.max_parts] 134 | 135 | def mutate(self): 136 | m = random.randint(0,7) 137 | i = random.randint(0,len(self.elements)-1) 138 | if m==0: 139 | #Change value of one component 140 | m = random.randint(0,1) 141 | if m==0: 142 | #New value 143 | self.elements[i] = mutate_value(self.elements[i],self.parts_list) 144 | else: 145 | #Slight change 146 | self.elements[i] = mutate_value(self.elements[i],self.parts_list,rel_amount=0.1) 147 | elif m==1: 148 | #Add one component if not already maximum number of components 149 | if len(self.elements)1: 153 | #Replace one component with open circuit 154 | del self.elements[i] 155 | elif m==3 and len(self.elements)>1: 156 | #Replace one component with open circuit 157 | nodes = self.elements[i].nodes 158 | random.shuffle(nodes) 159 | try: 160 | n1 = nodes[0] 161 | n2 = nodes[1] 162 | except IndexError: 163 | return None#Device doesn't have two nodes 164 | del self.elements[i] 165 | for element in self.elements: 166 | element.nodes = [(n1 if i==n2 else i) for i in element.nodes] 167 | elif m==4: 168 | #Replace one component keeping one node connected 169 | fixed_node = random.choice(self.elements[i].nodes) 170 | del self.elements[i] 171 | self.elements.append(random_element(self.parts_list,self.nodes,fixed_node=fixed_node)) 172 | elif m==5: 173 | #Shuffle list of elements(better crossovers) 174 | random.shuffle(self.elements) 175 | elif m==6: 176 | #Change the extra_value 177 | if self.extra_range!=None: 178 | i = random.randint(0,len(self.extra_value)-1) 179 | self.extra_value[i] = random.uniform(*self.extra_range[i]) 180 | else: 181 | self.mutate() 182 | elif m==7: 183 | #Relabel nodes 184 | l = len(self.elements)-1 185 | n1 = random.choice(self.elements[random.randint(0,l)].nodes) 186 | n2 = random.choice(self.elements[random.randint(0,l)].nodes) 187 | tries = 0 188 | while tries<10 or n1!=n2: 189 | n2 = random.choice(self.elements[random.randint(0,l)].nodes) 190 | tries+=1 191 | for element in self.elements: 192 | element.nodes = [(n1 if i==n2 else (n2 if i==n1 else i)) for i in element.nodes] 193 | 194 | 195 | def spice(self,options): 196 | """Generate the input to SPICE""" 197 | program = options+'\n' 198 | for i in self.elements: 199 | program+=str(i)+'\n' 200 | return program 201 | 202 | 203 | def random_circuit(parts, inst_limit, sigma, inputs, outputs, special_nodes, special_node_prob, extra_value=None): 204 | """Generates a random circuit. 205 | parts - dictionary of available devices. 206 | inst_limit - maximum number of nodes 207 | sigma - standard deviation of nodes. 208 | inputs - input nodes. 209 | outputs - output nodes. 210 | special_nodes - power supplies and other useful but not necessary nodes. 211 | special_node_prob - Probability of having a special node in single instruction, 212 | can be a list or single number. 213 | """ 214 | #Add the ground 215 | special = special_nodes[:] 216 | if '0' not in special: 217 | special.append('0') 218 | special.extend(range(1,inst_limit)) 219 | if max(len(inputs),len(outputs)) > inst_limit: 220 | raise ValueError("Number of allowed nodes is too small.") 221 | special = special + inputs + outputs 222 | c = Chromosome(inst_limit,parts,special,extra_value=extra_value) 223 | 224 | #Check for input and outputs 225 | has_input = False 226 | has_output = False 227 | for e in c.elements: 228 | if any(i in e.nodes for i in inputs): 229 | has_input = True 230 | if any(o in e.nodes for o in outputs): 231 | has_output = True 232 | if not has_input: 233 | c.elements[0].nodes[0] = random.choice(inputs) 234 | random.shuffle(c.elements[0].nodes) 235 | if not has_output: 236 | c.elements[-1].nodes[0] = random.choice(outputs) 237 | random.shuffle(c.elements[-1].nodes) 238 | return c 239 | 240 | 241 | def parse_circuit(circuit, inst_limit, parts, sigma, inputs, outputs, special_nodes, special_node_prob, extra_value=None): 242 | devices = [] 243 | special = special_nodes[:] 244 | if '0' not in special: 245 | special.append('0') 246 | if max(len(inputs),len(outputs)) > inst_limit: 247 | raise ValueError("Number of allowed nodes is too small.") 248 | special = special + inputs + outputs 249 | #Devices starting with same substring are sorted longest 250 | #first to check longest possible device names first 251 | sorted_dev = sorted(parts.keys(),reverse=True) 252 | nodes = {} 253 | len_nodes = 1 254 | for n,line in enumerate(circuit.splitlines()): 255 | if not line: 256 | #Ignores empty lines 257 | continue 258 | 259 | #Current device fields 260 | d_spice = [] 261 | #Try all the devices 262 | for dev in sorted_dev: 263 | if line.startswith(dev): 264 | #Found matching device from parts list 265 | line = line.split() 266 | d_nodes = line[1:parts[dev]['nodes']+1] 267 | for node in d_nodes: 268 | if node not in special and node not in nodes: 269 | nodes[node] =len_nodes 270 | len_nodes += 1 271 | d_nodes = [nodes[node] if node in nodes else node for node in d_nodes] 272 | d_spice = line[parts[dev]['nodes']+1:] 273 | for e in xrange(len(d_spice)): 274 | #Correct types and change SPICE multipliers to bare numbers. 275 | try: 276 | d_spice[e] = multipliers(d_spice[e]) 277 | except: 278 | #Not a number. 279 | pass 280 | devices.append(Device(dev,d_nodes,0,d_spice)) 281 | break 282 | 283 | else: 284 | #Device not found 285 | print "Couldn't find device in line {}:{}\nIgnoring this line".format(n,line) 286 | #def __init__(self,max_parts,parts_list,nodes,extra_value=None): 287 | special.extend(map(str,range(1,inst_limit))) 288 | circuit = Chromosome(inst_limit, parts, special, extra_value) 289 | circuit.elements = devices 290 | return circuit 291 | 292 | #parts = { 'R':{'value':(1,1e6),'nodes':2}, 'C':{'value':(1e-12,1e-3),'nodes':2}, 'Q':{'model':('2N3904','2N3906'),'kwvalues':{'w':(1e-7,1e-5),'l':(1e-7,1e-5)},'nodes':3} } 293 | ##r = Device('R',100,None,None,0) 294 | ##c = Device('C',1e-5,None,None,0) 295 | ##q = Device('Q',None,{'w':1,'l':2},'2N3904',0) 296 | #c = random_circuit(parts, 10, 2, ['in1','in2'],['out'], ['vc','vd'], [0.1,0.1],extra_value=[(0,5)]) 297 | #print c 298 | #c.mutate() 299 | #print 300 | #print c 301 | #print c.value_bounds() 302 | #d = c.get_values() 303 | #print d 304 | #c.set_values(d) 305 | #print c 306 | #print c.extra_value 307 | -------------------------------------------------------------------------------- /evolutionary/chromosomes/chain.py: -------------------------------------------------------------------------------- 1 | import random 2 | import ast 3 | from common import * 4 | 5 | class Device: 6 | def __init__(self, name, value, kwvalues, model, cost): 7 | self.name = name 8 | self.value = value 9 | self.kwvalues = kwvalues 10 | self.model = model 11 | if cost!=None: 12 | self.cost = cost 13 | else: 14 | self.cost = 0 15 | 16 | def spice(self, nodes, device_number=None): 17 | kw = '' 18 | if self.kwvalues != None: 19 | for k in self.kwvalues.keys(): 20 | kw += ' '+k+'='+str(self.kwvalues[k]) 21 | if device_number==None: 22 | device_number = str(id(self)) 23 | else: 24 | device_number = str(device_number) 25 | return self.name + device_number +' '+ ' '.join(map(str,nodes)) + ' '+ (str(self.value) if self.value!=None else '') + (self.model if self.model!=None else '') + kw 26 | 27 | def __repr__(self): 28 | return self.spice('-',1) 29 | 30 | def mutatevalue(self, r): 31 | """Mutates device value. If r is 3-tuple third value is a random 32 | number distribution. Two first are lower and upper limits.""" 33 | if len(r)==3: 34 | self.value = r[3](*r[:2]) 35 | else: 36 | if r[0]>0: 37 | self.value = log_dist(*r) 38 | else: 39 | self.value = random.uniform(*r) 40 | 41 | def mutatekwvalue(self, r): 42 | """Mutates keyword values. Same logic as in value mutations.""" 43 | kw = random.choice(self.kwvalues.keys()) 44 | r = r[kw] 45 | if len(r)==3: 46 | self.kwvalues[kw] = r[3](*r[:2]) 47 | else: 48 | if r[0]>0: 49 | self.kwvalues[kw] = log_dist(*r) 50 | else: 51 | self.kwvalues[kw] = random.uniform(*r) 52 | 53 | def random_device(parts): 54 | """Generates a random device from parts list. 55 | "sigma" is the gaussian distribution standard deviation, 56 | which is used for generating connecting nodes.""" 57 | name = random.choice(parts.keys()) 58 | r = parts[name] 59 | kw,value,model = [None]*3 60 | cost = 0 61 | if 'kwvalues' in r.keys(): 62 | kw = {i:value_dist(r['kwvalues'][i]) for i in r['kwvalues'].keys()} 63 | if 'value' in r.keys(): 64 | value = value_dist(r['value']) 65 | if 'model' in r.keys(): 66 | model = r['model'] if type(r['model'])==str else random.choice(r['model']) 67 | if 'cost' in r.keys(): 68 | cost = r['cost'] 69 | return Device(name, value, kw, model, cost) 70 | 71 | def random_instruction(parts, sigma, special_nodes, special_node_prob, mu=0.5): 72 | """Generate random instruction with random device. 73 | "sigma" is the standard deviation of nodes.""" 74 | d = random_device(parts) 75 | nodes = [int(round(random.gauss(mu,sigma))) for i in xrange(parts[d.name]['nodes'])] 76 | while same(nodes): 77 | nodes = [int(round(random.gauss(mu,sigma))) for i in xrange(parts[d.name]['nodes'])] 78 | #Sprinkle some special nodes 79 | if type(special_node_prob)==list: 80 | for i in xrange(len(nodes)): 81 | if random.random() < special_node_prob[i]: 82 | nodes[i] = lst_random(special_nodes,special_node_prob) 83 | else: 84 | for i in xrange(len(nodes)): 85 | if random.random() < special_node_prob: 86 | nodes[i] = random.choice(special_nodes) 87 | 88 | command = random.randint(0,1) 89 | return Instruction(command, d, sigma, nodes, special_nodes, special_node_prob) 90 | 91 | class Instruction: 92 | def __init__(self, command, device, sigma, args, special_nodes, special_node_prob): 93 | self.commands = 2 94 | self.command = command 95 | self.device = device 96 | self.sigma = sigma 97 | self.args = args 98 | self.special_nodes = special_nodes 99 | self.special_node_prob = special_node_prob 100 | 101 | def __call__(self, current_node, device_number): 102 | #Transform relative nodes to absolute by adding current node 103 | nodes = [current_node + i if type(i)==int else i for i in self.args] 104 | #nodes = [i if i>=0 else 0 for i in nodes] 105 | if self.command == 0: 106 | #Connect 107 | return (self.device.spice(nodes,device_number),current_node) 108 | if self.command == 1: 109 | #Connect and move 110 | return (self.device.spice(nodes,device_number),current_node+1) 111 | #if self.command == 2: 112 | # #Move to current_node + self.args 113 | # return ('',current_node + self.args) 114 | raise Exception("Invalid instruction: {}".format(self.command)) 115 | 116 | def __repr__(self): 117 | return self.__call__(0,1)[0] 118 | 119 | def mutate(self, parts): 120 | #Possible mutations 121 | m = [self.device.value!=None,self.device.kwvalues!=None, 122 | self.device.model!=None,True,self.command in (0,1)] 123 | m = [i for i,c in enumerate(m) if c==True] 124 | r = random.choice(m) 125 | if r == 0: 126 | #Change value 127 | self.device.mutatevalue(parts[self.device.name]['value']) 128 | elif r == 1: 129 | #Change kwvalue 130 | self.device.mutatekwvalue(parts[self.device.name]['kwvalues']) 131 | elif r == 2: 132 | #Change model 133 | models = parts[self.device.name]['model'] 134 | if type(models)!=str and len(parts[self.device.name]['model'])>1: 135 | models = set(models) 136 | #Remove current model 137 | models.discard(self.device.model) 138 | self.device.model = random.choice(list(models)) 139 | elif r == 3: 140 | #Change type of the instruction 141 | old = self.command 142 | new = set(range(self.commands)) 143 | new.discard(old) 144 | self.command = random.choice(list(new)) 145 | elif r == 4: 146 | #Change nodes 147 | self.args = [int(random.gauss(0,self.sigma)) for i in xrange(parts[self.device.name]['nodes'])] 148 | while same(self.args): 149 | self.args = [int(random.gauss(0,self.sigma)) for i in xrange(parts[self.device.name]['nodes'])] 150 | #Sprinkle some special nodes 151 | if type(self.special_node_prob)==list: 152 | for i in xrange(len(self.args)): 153 | if random.random() < self.special_node_prob[i]: 154 | self.args[i] = lst_random(self.special_nodes,self.special_node_prob) 155 | else: 156 | for i in xrange(len(self.args)): 157 | if random.random() < self.special_node_prob: 158 | self.args[i] = random.choice(self.special_nodes) 159 | 160 | 161 | def random_circuit(parts, inst_limit, sigma, inputs, outputs, special_nodes, special_node_prob, extra_value=None): 162 | """Generates a random circuit. 163 | parts - dictionary of available devices. 164 | inst_limit - maximum number of instructions. 165 | sigma - standard deviation of nodes. 166 | inputs - input nodes. 167 | outputs - output nodes. 168 | special_nodes - power supplies and other useful but not necessary nodes. 169 | special_node_prob - Probability of having a special node in single instruction, 170 | can be a list or single number. 171 | """ 172 | #Normalize probabilities and check that probabilities are valid 173 | special = special_nodes[:] 174 | if type(special_node_prob)==list: 175 | if not all(0 inst_limit: 188 | raise ValueError("Instruction limit is too small.") 189 | special = special + inputs + outputs 190 | if type(special_node_prob)==list: 191 | special_node_prob = special_node_prob + [0.1]*(len(inputs)+len(outputs)) 192 | inst = [random_instruction(parts, sigma, special, special_node_prob) for i in xrange(random.randint(max(len(inputs),len(outputs)),inst_limit))] 193 | for e,i in enumerate(inputs): 194 | nodes = inst[e].args 195 | nodes[argmin(nodes)] = i 196 | for e,i in enumerate(outputs,1): 197 | nodes = inst[-e].args 198 | nodes[argmax(nodes)] = i 199 | return Circuit(inst, parts, inst_limit, (parts,sigma,special, special_node_prob), extra_value) 200 | 201 | class Circuit: 202 | def __init__(self, inst, parts, inst_limit, inst_args, extra_value=None): 203 | #Everything necessary to make a new instruction 204 | self.inst_args = inst_args 205 | #List of instructions in the circuit 206 | self.instructions = inst 207 | #List of devices 208 | self.parts = parts 209 | #Max instructions 210 | self.inst_limit = inst_limit 211 | 212 | if extra_value!=None: 213 | self.extra_range = extra_value 214 | self.extra_value = [random.uniform(*i) for i in self.extra_range] 215 | else: 216 | self.extra_value = None 217 | 218 | def spice(self, commands): 219 | current_node = self.inst_limit 220 | program = '' 221 | for device_number,inst in enumerate(self.instructions,1): 222 | t = inst(current_node,device_number) 223 | program += t[0]+'\n' 224 | current_node = t[1] 225 | return commands+program 226 | 227 | def __repr__(self): 228 | return self.spice('') 229 | 230 | def mutate(self): 231 | #Available mutations 232 | m = [ 233 | len(self.instructions)>0, 234 | len(self.instructions)>1, 235 | len(self.instructions)>1, 236 | len(self.instructions)r2: 271 | r1,r2=r2,r1 272 | if r==0: 273 | #Two point crossover 274 | self.instructions = self.instructions[:r1]+other.instructions[r1:r2]+self.instructions[r2:] 275 | self.instructions = self.instructions[:self.inst_limit] 276 | else: 277 | #Single point crossover 278 | self.instructions = self.instructions[:r1]+other.instructions[r1:] 279 | self.instructions = self.instructions[:self.inst_limit] 280 | 281 | def value_bounds(self): 282 | """Return bounds of values for optimization.""" 283 | bounds = [] 284 | for ins in self.instructions: 285 | if ins.device.value != None: 286 | bounds.append(self.parts[ins.device.name]['value']) 287 | if ins.device.kwvalues != None: 288 | for kw in sorted(self.parts[ins.device.name]['kwvalues'].keys()): 289 | bounds.append(self.parts[ins.device.name]['kwvalues'][kw]) 290 | if self.extra_value != None: 291 | bounds.extend(self.extra_range) 292 | return bounds 293 | 294 | def get_values(self): 295 | values = [] 296 | for ins in self.instructions: 297 | if ins.device.value != None: 298 | values.append(ins.device.value) 299 | if ins.device.kwvalues != None: 300 | for kw in sorted(self.parts[ins.device.name]['kwvalues'].keys()): 301 | values.append(ins.device.kwvalues[kw]) 302 | if self.extra_value != None: 303 | values.extend(self.extra_value) 304 | return values 305 | 306 | def set_values(self,values): 307 | i = 0 308 | for ins in self.instructions: 309 | if ins.device.value != None: 310 | ins.device.value = values[i] 311 | i += 1 312 | if ins.device.kwvalues != None: 313 | for kw in sorted(self.parts[ins.device.name]['kwvalues'].keys()): 314 | ins.device.kwvalues[kw] = values[i] 315 | i += 1 316 | if self.extra_value != None: 317 | self.extra_value = values[i:] 318 | return None 319 | 320 | def parse_circuit(circuit, inst_limit, parts, sigma, inputs, outputs, special_nodes, special_node_prob, extra_value=None): 321 | """Converts netlist to chromosome format.""" 322 | if '0' not in special_nodes: 323 | special_nodes.append('0') 324 | special = special_nodes + inputs + outputs 325 | #Devices starting with same substring are sorted longest 326 | #first to check longest possible device names first 327 | sorted_dev = sorted(parts.keys(),reverse=True) 328 | instructions = [] 329 | #Table for converting circuit nodes to chromosome nodes 330 | nodes = {} 331 | len_nodes = 1 332 | current_node = 0 333 | for n,line in enumerate(circuit.splitlines()): 334 | if not line: 335 | #Ignores empty lines 336 | continue 337 | 338 | #Current device fields 339 | d_spice = [] 340 | #Try all the devices 341 | for dev in sorted_dev: 342 | if line.startswith(dev): 343 | #Found matching device from parts list 344 | current_node += 1#Increase current node 345 | line = line.split() 346 | d_nodes = line[1:parts[dev]['nodes']+1] 347 | for node in d_nodes: 348 | if node not in special and node not in nodes: 349 | nodes[node] =len_nodes 350 | len_nodes += 1 351 | 352 | d_spice = line[parts[dev]['nodes']+1:] 353 | for e in xrange(len(d_spice)): 354 | #Correct types and change SPICE multipliers to bare numbers. 355 | try: 356 | d_spice[e] = multipliers(d_spice[e]) 357 | except: 358 | #Not a number. 359 | pass 360 | if 'value' in parts[dev]: 361 | value = float(d_spice[0]) 362 | if not parts[dev]['value'][0]<=value<=parts[dev]['value'][1]: 363 | raise ValueError("Value of component on line {} is out of bounds\n{}\nBounds defined in the parts dictionary are: {} to {}".format(n,' '.join(line),parts[dev]['value'][0],parts[dev]['value'][1])) 364 | d_spice = d_spice[1:] 365 | else: 366 | value = None 367 | if 'model' in parts[dev]: 368 | model = d_spice[0] 369 | d_spice = d_spice[1:] 370 | else: 371 | model = None 372 | if 'kwvalues' in parts[dev]: 373 | d_spice = ["'"+i[:i.index('=')]+"'"+i[i.index('='):] for i in d_spice] 374 | kwvalues = ast.literal_eval('{'+', '.join(d_spice).replace('=',':')+'}') 375 | else: 376 | kwvalues = None 377 | if 'cost' in parts[dev]: 378 | cost = parts['cost'] 379 | else: 380 | cost = 0 381 | device = Device(dev, value, kwvalues, model, cost) 382 | node_temp = [nodes[node] - current_node + inst_limit if node in nodes else node for node in d_nodes] 383 | instructions.append(Instruction(1, device, sigma, node_temp, special, special_node_prob)) 384 | break 385 | 386 | else: 387 | #Device not found 388 | print "Couldn't find device in line {}:{}\nIgnoring this line".format(n,line) 389 | if len(instructions) > inst_limit: 390 | raise ValueError("Maximum number of devices is too small for seed circuit.") 391 | return Circuit(instructions, parts, inst_limit, (parts, sigma, special, special_node_prob), extra_value) 392 | 393 | #parts = { 'R':{'value':(1,1e6),'nodes':2}, 'C':{'value':(1e-12,1e-3),'nodes':2}, 'Q':{'model':('2N3904','2N3906'),'kwvalues':{'w':(1e-7,1e-5),'l':(1e-7,1e-5)},'nodes':3} } 394 | #r = Device('R',100,None,None,0) 395 | #c = Device('C',1e-5,None,None,0) 396 | #q = Device('Q',None,{'w':1,'l':2},'2N3904',0) 397 | #c = random_circuit(parts, 10, 2, ['in1','in2'],['out'], ['vc','vd'], [0.1,0.1]) 398 | #print c 399 | #c.mutate() 400 | #print c 401 | #seed=""" 402 | #R1 n4 n3 1k 403 | #R2 out n3 1k 404 | #Q11 n5 n4 in1 2N3904 405 | #Q12 n5 n4 in2 2N3904 406 | #Q13 out n5 0 2N3904 407 | #""" 408 | #inputs = ['in1','in2'] 409 | #outputs = ['out'] 410 | #special = [] 411 | #print parse_circuit(seed, 10, parts, 2, inputs, outputs, special, 0.1) 412 | -------------------------------------------------------------------------------- /evolutionary/cgp.py: -------------------------------------------------------------------------------- 1 | try: 2 | PLOTTING='matplotlib' 3 | import matplotlib.pyplot as plt 4 | except: 5 | import subprocess 6 | PLOTTING='external' 7 | 8 | import circuits 9 | import random 10 | from copy import deepcopy 11 | from time import strftime,time 12 | import re 13 | import pickle 14 | from os.path import join as path_join 15 | import os 16 | import getch 17 | import sys 18 | import multiprocessing 19 | import chromosomes 20 | from optimization.diff_evolve import * 21 | inf = 1e30 22 | 23 | def multipliers(x): 24 | """Convert values with si multipliers to numbers""" 25 | try: 26 | return float(x) 27 | except: 28 | pass 29 | try: 30 | a = x[-1] 31 | y = float(x[:-1]) 32 | endings = {'G':9,'Meg':6,'k':3,'m':-3,'u':-6,'n':-9,'p':-12,'s':0} 33 | return y*(10**endings[a]) 34 | except: 35 | raise ValueError("I don't know what {} means".format(x)) 36 | 37 | class CGP: 38 | """ 39 | Evolutionary circuits. 40 | 41 | pool_size: Amount of circuits in one generation 42 | nodes: maximum number of available nodes, nodes are named n0,n1,n2,.. and ground node is 0(not n0) 43 | parts_list: List of circuit elements available 44 | max_parts: Integer of maximum number of circuit elements in one circuit 45 | elitism: Integer of number of circuits to clone driectly to next generation 46 | mutation_rate: Mutation propability, float in range 0.0-1.0, but not exactly 1.0, because mutation can be applied many times. 47 | crossover_rate: Propability of crossover. 48 | fitnessfunction: List of functions to test circuits against 49 | fitness_weight: Scores from fitness functions are weighted with this weight 50 | spice_sim_commands: Commands for SPICE simulator 51 | log: Log file to save progress 52 | plot_titles: Titles for the plots of cricuits 53 | """ 54 | def __init__(self, 55 | population, 56 | parts, 57 | max_parts, 58 | elitism, 59 | mutation_rate, 60 | crossover_rate, 61 | fitness_function, 62 | constraints, 63 | spice_commands, 64 | log_file, 65 | title='', 66 | common='', 67 | models='', 68 | fitness_weight=None, 69 | constraint_weight=None, 70 | nodes=None, 71 | resumed=False, 72 | extra_value=None, 73 | plot_titles=None, 74 | plot_yrange=None, 75 | selection_weight=1, 76 | max_mutations=3, 77 | **kwargs): 78 | 79 | if kwargs['chromosome'] == 'netlist': 80 | self.chromosome = chromosomes.netlist 81 | elif kwargs['chromosome'] == 'chain': 82 | self.chromosome = chromosomes.chain 83 | else: 84 | raise ValueError("Invalid chromosome") 85 | #Correct the types 86 | if hasattr(fitness_function, '__call__'): 87 | self.ff=[fitness_function] 88 | else: 89 | self.ff=fitness_function 90 | 91 | if hasattr(constraints, '__call__'): 92 | self.constraints=[constraints] 93 | else: 94 | self.constraints = constraints 95 | 96 | if hasattr(spice_commands, '__call__'): 97 | self.spice_commands=[spice_commands+common+models] 98 | else: 99 | self.spice_commands=[i+common+models for i in spice_commands] 100 | 101 | if fitness_weight==None: 102 | self.fitness_weight=[{}] 103 | else: 104 | self.fitness_weight = fitness_weight 105 | 106 | if self.constraints == None: 107 | self.constraints = [None for i in xrange(len(spice_commands))] 108 | 109 | if constraint_weight==None: 110 | self.constraint_weight=[100 for i in xrange(len(spice_commands))] 111 | else: 112 | self.constraint_weight = constraint_weight 113 | 114 | if len(self.spice_commands)>len(self.fitness_weight): 115 | raise Exception('Fitness function weight list length is incorrect') 116 | if len(self.spice_commands)>len(self.ff): 117 | raise Exception('Not enough fitness functions') 118 | if len(self.spice_commands)>len(self.constraints): 119 | raise Exception('Not enough constraints. Use None for no constraint') 120 | if len(self.spice_commands)>len(self.constraint_weight): 121 | raise Exception('Not enough constraint weights.') 122 | sim = map(self.parse_sim_options,self.spice_commands) 123 | 124 | print strftime("%Y-%m-%d %H:%M:%S") 125 | for e,i in enumerate(sim,1): 126 | print 'Simulation {0} - Type: {1}, Logarithmic plot: {2}'.format(e,i[0],str(i[1])) 127 | if i[3]: 128 | print 'Temperature specified in simulation' 129 | self.temperatures = True 130 | #TODO write the current temperature in the plot 131 | print 132 | 133 | self.timeout = kwargs['timeout'] 134 | self.c_free_gens = kwargs['constraint_free_generations'] 135 | self.rnd_circuits = kwargs['random_circuits'] 136 | self.gradual_constraints = kwargs['gradual_constraints'] 137 | self.constraint_ramp = kwargs['constraint_ramp'] 138 | self.plot_all_gens = kwargs['plot_every_generation'] 139 | 140 | self.def_scoring = kwargs['default_scoring'] 141 | self.c_rank = kwargs['custom_scoring'] 142 | 143 | if type(self.c_free_gens) not in (int,long,float): 144 | raise Exception('constraint_free_generations must be a number') 145 | if type(self.gradual_constraints) != bool: 146 | raise Exception('gradual_constraints must be of type "bool"') 147 | if type(self.constraint_ramp) not in (int,long,float): 148 | raise Exception('constraint_ramp must be a number') 149 | if type(self.plot_all_gens)!=bool: 150 | raise Exception('plot_every_generation must be of type "bool"') 151 | self.overflowed = 0 152 | 153 | self.sigma = kwargs['node_stddev'] 154 | self.inputs = kwargs['inputs'] 155 | self.outputs = kwargs['outputs'] 156 | self.special_nodes = kwargs['special_nodes'] 157 | self.special_node_prob = kwargs['special_node_prob'] 158 | 159 | #sim_type is list of SPICE simulation types(ac,dc,tran...) 160 | self.sim_type = [sim[i][0] for i in xrange(len(sim))] 161 | #Boolean for each simulation for logarithm or linear plot 162 | self.log_plot = [sim[i][1] for i in xrange(len(sim))] 163 | #Maximum value of frequency or sweep 164 | self.frange = [sim[i][2] for i in xrange(len(sim))] 165 | self.constraints_filled = False 166 | if nodes == None: 167 | self.nodes = max_parts 168 | else: 169 | self.nodes = nodes 170 | self.max_parts = max_parts 171 | self.extra_value = extra_value 172 | self.selection_weight = selection_weight 173 | 174 | #FIXME weight function plotting is currently disabled 175 | self.plot_weight=False 176 | #if all(i==None for i in self.fitness_weight): 177 | # self.plot_weight=False 178 | #else: 179 | # self.plot_weight=True 180 | 181 | if all(i==None for i in self.constraints): 182 | self.plot_constraints=False 183 | else: 184 | self.plot_constraints=True 185 | 186 | self.pool_size=population 187 | self.parts_list = parts 188 | self.generation=0 189 | self.elitism=elitism 190 | self.alltimebest=(float('inf'),float('inf')) 191 | self.mrate = mutation_rate 192 | self.crate = crossover_rate 193 | self.max_mutations = max_mutations 194 | self.logfile = log_file 195 | if not resumed: 196 | log_file.write("Spice simulation commands:\n"+'\n'.join(self.spice_commands)+'\n\n\n') 197 | 198 | self.plot_titles = plot_titles 199 | self.plot_yrange = plot_yrange 200 | 201 | #Directory to save files in 202 | self.directory = title 203 | 204 | #Generate seed circuits 205 | seed = kwargs['seed'] 206 | self.seed_copies = kwargs['seed_copies'] 207 | self.seed_circuits = [] 208 | if seed!=None: 209 | if type(seed)==str: 210 | seed = [seed] 211 | for element in seed: 212 | self.seed_circuits.append(self.chromosome.parse_circuit(element, 213 | self.max_parts, self.parts_list, self.sigma, self.inputs, self.outputs, 214 | self.special_nodes, self.special_node_prob, self.extra_value)) 215 | 216 | 217 | 218 | def parse_sim_options(self,option): 219 | """Parses spice simulation commands for ac,dc,trans and temp words. 220 | If ac simulation is found plotting scale is made logarithmic""" 221 | m = re.search(r'\n(ac|AC) [a-zA-Z1-9]* [0-9\.]* [0-9\.]* [0-9\.]*[a-zA-Z]?[0-9\.]*',option) 222 | temp = ('.temp' in option) or ('.dtemp' in option) 223 | if m!=None: 224 | m = m.group(0).split() 225 | return m[0],False if m[1]=='lin' else True,multipliers(m[-1]),temp 226 | m = re.search(r'\n(dc|DC) [a-zA-Z1-9]* [0-9\.]* [0-9\.]*',option) 227 | if m!=None: 228 | m = m.group(0).split() 229 | return m[0],False,multipliers(m[-1]),temp 230 | m = re.search(r'\n(tran|TRAN) [0-9\.]*[a-zA-Z]? [0-9\.]*[a-zA-Z]?',option) 231 | if m!=None: 232 | m = m.group(0).split() 233 | return m[0],False,multipliers(m[-1]),temp 234 | else: 235 | return 0,False,0,temp 236 | 237 | def eval_single(self, ckt, options): 238 | """Used in plotting, when only 1 circuits needs to be simulated""" 239 | program = ckt.spice(options) 240 | thread = circuits.spice_thread(program) 241 | thread.start() 242 | thread.join(2*self.timeout) 243 | return thread.result 244 | 245 | def _optimize(self,pool): 246 | 247 | def g(self,ckt,x): 248 | ckt.set_values(x) 249 | return self.rank_pool([ckt])[0][0] 250 | 251 | for e,c in enumerate(pool): 252 | c = c[1] 253 | bounds = c.value_bounds() 254 | if len(bounds)==0: 255 | continue 256 | lbound = [b[0] for b in bounds] 257 | ubound = [b[1] for b in bounds] 258 | x0 = [c.get_values()] 259 | h = lambda x:g(self,c,x) 260 | try: 261 | d = DiffEvolver.frombounds(h,lbound,ubound, 30, x0=x0,strategy=('best',2,'bin')) 262 | gens = 2 263 | prev = pool[e][0] 264 | d.solve(2) 265 | while gens < 10: 266 | gens += 2 267 | d.solve(2) 268 | if not d.best_value + gens - 4 < prev: 269 | break 270 | prev = d.best_value 271 | c.set_values(d.best_vector) 272 | #print d.best_val_history 273 | print e,gens,pool[e][0],d.best_value 274 | pool[e] = (d.best_value,c) 275 | del d 276 | except KeyboardInterrupt: 277 | return 278 | return pool 279 | 280 | def optimize_values(self, pool): 281 | best_score = pool[0][0] 282 | circuits = min(max(2*multiprocessing.cpu_count(),int(0.01*len(pool))),4*multiprocessing.cpu_count()) 283 | i = 1 284 | #Already reached the best possible score 285 | if best_score == 0: 286 | return pool 287 | #Circuits to optimize 288 | op = [pool[0]] 289 | indices = [0] 290 | for score in xrange(1,len(pool)): 291 | if pool[score][0]>inf/2: 292 | break 293 | delta = (pool[score-1][0]-pool[0][0])/(score+1) 294 | while i < len(pool) and len(op) best_score*100: 296 | break 297 | if abs(pool[i][0]-best_score)> 0.01 and abs(1-pool[i][0]/best_score): 298 | #Make sure scores are different enough and score is 299 | #worth optimizing 300 | if all(abs(pool[i][0]-p[0])>delta for p in op): 301 | op.append(pool[i]) 302 | indices.append(i) 303 | i += random.randint(1,min(2,score/circuits)) 304 | 305 | print "Optimizing" 306 | 307 | try: 308 | l = len(op) 309 | cpool = multiprocessing.Pool() 310 | partitions = multiprocessing.cpu_count() 311 | op = [op[i*l/partitions:(i+1)*l/partitions] for i in xrange(partitions)] 312 | p = cpool.map(self._optimize,op) 313 | cpool.close() 314 | except KeyboardInterrupt: 315 | cpool.terminate() 316 | return 317 | op2 = [] 318 | for thread in p: 319 | op2.extend(thread) 320 | for e,i in enumerate(indices): 321 | pool[i] = op2[e] 322 | return pool 323 | 324 | def rank_pool(self,pool): 325 | """Multithreaded version of self.rank, computes scores for whole pool""" 326 | try: 327 | scores = [0]*len(pool) 328 | 329 | lasterror = None 330 | timeouts = 0 331 | for i in xrange(len(self.spice_commands)): 332 | errors = 0 333 | skipped = 0 334 | for t in xrange(len(pool)): 335 | if scores[t]>=inf: 336 | continue 337 | thread = circuits.spice_thread(pool[t].spice(self.spice_commands[i])) 338 | thread.start() 339 | thread.join(self.timeout) 340 | if thread.is_alive(): 341 | timeouts += 1 342 | if self.generation==1: 343 | if t10: 380 | #All simulations failed 381 | raise SyntaxError("Simulation {} failed for every circuit.\nSpice returned {}".format(i,lasterror)) 382 | #Don't increase timeout when there is only 1 circuit 383 | if len(pool) > 1: 384 | if timeouts != 0: 385 | print '{} simulation(s) timed out'.format(timeouts) 386 | if timeouts > len(pool)/10: 387 | if timeouts > len(pool)/2: 388 | self.timeout *= 1.5 389 | print "Increasing timeout length by 50%, to {}".format(self.timeout) 390 | else: 391 | self.timeout *= 1.25 392 | print "Increasing timeout length by 25%, to {}".format(self.timeout) 393 | return [(scores[i],pool[i]) for i in xrange(len(pool))] 394 | except KeyboardInterrupt: 395 | return 396 | 397 | def _rank(self,x,i,k,extra=None,circuit=None): 398 | """Score of single circuit against single fitness function 399 | x is a dictionary of measurements, i is number of simulation, k is the measurement to score""" 400 | total=0.0 401 | func = self.ff[i] 402 | try:#fitness_weight might be None, or it might be list of None, or list of dictionary that contains None 403 | weight = self.fitness_weight[i][k] 404 | except (KeyError,TypeError,IndexError): 405 | weight = lambda x,**kwargs:1 406 | #If no weight function create function that returns one for all inputs 407 | if type(weight) in (int,long,float): 408 | c = weight 409 | weight = lambda x,**kwargs:float(c)#Make a constant anonymous function 410 | 411 | try: 412 | f = x[k][0]#Input 413 | v = x[k][1]#Output 414 | y = float(max(f)) 415 | except: 416 | return inf 417 | #Sometimes spice doesn't simulate whole frequency range 418 | #I don't know why, so I just check if spice returned the whole range 419 | if y<0.99*self.frange[i]: 420 | return inf 421 | 422 | 423 | con_filled = True 424 | con_penalty=0 425 | for p in xrange(1,len(f)): 426 | try: 427 | total+=weight( f[p],extra=extra, generation=self.generation)*(f[p]-f[p-1])*( func(f[p],k,extra=extra, generation=self.generation) - v[p] )**2 428 | except TypeError as t: 429 | print 'Fitness function returned invalid value, while testing {} of simulation {}'.format(k,i) 430 | print t 431 | raise t 432 | except OverflowError: 433 | self.overflowed += 1 434 | total=inf 435 | pass 436 | except KeyboardInterrupt: 437 | return 438 | if self.constraints[i]!=None and self.c_free_gens<=self.generation : 439 | con=self.constraints[i]( f[p],v[p],k,extra=extra,generation=self.generation ) 440 | if con==None: 441 | print 'Constraint function {} return None, for input: ({},{},{},extra={},generation={})'.format(i,f[p],v[p],k,extra,self.generation) 442 | if con==False: 443 | con_penalty+=1 444 | con_filled=False 445 | 446 | total/=y 447 | if total<0: 448 | return inf 449 | #if self.generation>5 and self.generation%5: 450 | # if circuit!=None and con_filled: 451 | # total+=sum([element.cost if hasattr(element,'cost') else 0 for element in circuit.elements]) 452 | #if con_penalty>1e5: 453 | # con_penalty=1e5 454 | if self.c_free_gens>self.generation: 455 | return 1000*total+self.constraint_weight[i]*10 456 | if self.gradual_constraints: 457 | if self.generation>=self.constraint_ramp-self.c_free_gens: 458 | m = 1.0 459 | else: 460 | m = (self.generation-self.c_free_gens)/float(self.constraint_ramp) 461 | total+=m*con_penalty*self.constraint_weight[i]/float(len(f)) 462 | else: 463 | total+=con_penalty*self.constraint_weight[i]/float(len(f)) 464 | return total*1000+20000*(not con_filled) 465 | 466 | def printpool(self): 467 | """Prints all circuits and their scores in the pool""" 468 | for f,c in self.pool: 469 | print f,c 470 | print 471 | 472 | def save_plot(self,circuit,i,name='',**kwargs): 473 | v = circuit.evaluate(self.spice_commands[i],self.timeout)[1] 474 | #For every measurement in results 475 | for k in v.keys(): 476 | score = self._rank(v,i,k,extra=circuit.extra_value) 477 | 478 | plt.figure() 479 | freq = v[k][0] 480 | gain = v[k][1] 481 | goal_val = [self.ff[i](f,k,extra=circuit.extra_value,generation=self.generation) for f in freq] 482 | #if self.plot_weight: 483 | # weight_val = [self.fitness_weight[i](c,k) for c in freq] 484 | if self.constraints[i]!=None and self.plot_constraints: 485 | constraint_val = [(freq[c],gain[c]) for c in xrange(len(freq)) if not self.constraints[i](freq[c],gain[c],k,extra=circuit.extra_value,generation=self.generation)] 486 | 487 | if self.log_plot[i]==True:#Logarithmic plot 488 | plt.semilogx(freq,gain,'g',basex=10) 489 | plt.semilogx(freq,goal_val,'b',basex=10) 490 | #if self.plot_weight: 491 | # plt.semilogx(freq,weight_val,'r--',basex=10) 492 | if self.plot_constraints: 493 | plt.plot(*zip(*constraint_val), marker='.', color='r', ls='') 494 | else: 495 | plt.plot(freq,gain,'g') 496 | plt.plot(freq,goal_val,'b') 497 | #if self.plot_weight: 498 | # plt.plot(freq,weight_val,'r--') 499 | if self.plot_constraints: 500 | plt.plot(*zip(*constraint_val), marker='.', color='r', ls='') 501 | 502 | # update axis ranges 503 | ax = [] 504 | ax[0:4] = plt.axis() 505 | # check if we were given a frequency range for the plot 506 | if k in self.plot_yrange.keys(): 507 | plt.axis([min(freq),max(freq),self.plot_yrange[k][0],self.plot_yrange[k][1]]) 508 | else: 509 | plt.axis([min(freq),max(freq),min(-0.5,-0.5+min(goal_val)),max(1.5,0.5+max(goal_val))]) 510 | 511 | if self.sim_type[i]=='dc': 512 | plt.xlabel("Input (V)") 513 | if self.sim_type[i]=='ac': 514 | plt.xlabel("Input (Hz)") 515 | if self.sim_type[i]=='tran': 516 | plt.xlabel("Time (s)") 517 | 518 | try: 519 | plt.title(self.plot_titles[i][k]) 520 | except: 521 | plt.title(k) 522 | 523 | plt.annotate('Generation '+str(self.generation),xy=(0.05,0.95),xycoords='figure fraction') 524 | if score!=None: 525 | plt.annotate('Score '+'{0:.2f}'.format(score),xy=(0.75,0.95),xycoords='figure fraction') 526 | plt.grid(True) 527 | # turn on the minor gridlines to give that awesome log-scaled look 528 | plt.grid(True,which='minor') 529 | if len(k)>=3 and k[1:3] == 'db': 530 | plt.ylabel("Output (dB)") 531 | elif k[0]=='v': 532 | plt.ylabel("Output (V)") 533 | elif k[0]=='i': 534 | plt.ylabel("Output (A)") 535 | 536 | plt.savefig(path_join(self.directory,strftime("%Y-%m-%d %H:%M:%S")+'-'+k+'-'+name+'.png')) 537 | 538 | def step(self): 539 | self.generation+=1 540 | 541 | def sf(pool,weight=1): 542 | #Select chromosomes from pool using weighted probablities 543 | r=random.random()**weight 544 | return random.choice(pool[:1+int(len(pool)*r)])[1] 545 | 546 | #Update best 547 | if self.generation==1: 548 | #Randomly generate some circuits 549 | newpool = [self.chromosome.random_circuit(self.parts_list, self.max_parts, self.sigma, self.inputs, self.outputs, self.special_nodes, self.special_node_prob, extra_value=self.extra_value) for i in xrange(self.pool_size)] 550 | #Generate seed circuits 551 | for i,circuit in enumerate(self.seed_circuits): 552 | newpool[i]=circuit 553 | #Set seed circuit extra values 554 | if self.extra_value!=None: 555 | best_value = (inf,None) 556 | for c,value in enumerate(self.extra_value): 557 | #10 test values for one extra_value 558 | for x in xrange(11): 559 | ev = value[0]+(value[1]-value[0])*(x/10.0) 560 | newpool[i].extra_value[c] = ev 561 | score = 0 562 | for command in xrange(len(self.spice_commands)): 563 | v = self.eval_single(newpool[i],self.spice_commands[command])[1] 564 | for k in v.keys(): 565 | score += self._rank(v,command,k,extra=newpool[i].extra_value) 566 | if 0 < score < best_value[0]: 567 | #Better value found 568 | best_value = (score,ev) 569 | #Set the extra value to the best one found 570 | print "Seed circuit {}, best found extra_value {}: {}".format(i,c,best_value[1]) 571 | newpool[i].extra_value[c] = best_value[1] 572 | #No extra values 573 | score = 0 574 | for command in xrange(len(self.spice_commands)): 575 | v = self.eval_single(newpool[i],self.spice_commands[command])[1] 576 | for k in v.keys(): 577 | score += self._rank(v,command,k,extra=newpool[i].extra_value) 578 | print "Seed circuit {}, score: {}".format(i,score) 579 | self.plotbest(newpool,0) 580 | for copy in xrange(self.seed_copies-1): 581 | newpool[copy*len(self.seed_circuits)+i] = deepcopy(newpool[i]) 582 | 583 | else: 584 | if self.elitism!=0: 585 | #Pick self.elitism amount of best performing circuits to the next generation 586 | newpool=[self.pool[i][1] for i in xrange(self.elitism)] 587 | else: 588 | newpool=[] 589 | 590 | #FIXME this should be enabled or disabled in the simulation settings 591 | #if (not self.constraints_filled) and (self.alltimebest[0]<10000): 592 | # print 'Constraint filling solution found' 593 | # print 'Optimizing for number of elements' 594 | # self.constraints_filled = True 595 | self.best=self.pool[0] 596 | 597 | #We have already chosen "self.elitism" of circuits in the new pool 598 | newsize=self.elitism 599 | while newsize<(1.0-self.rnd_circuits)*self.pool_size: 600 | newsize+=1 601 | c=deepcopy(sf(self.pool,weight=self.selection_weight)) #selected chromosome 602 | if random.random()<=self.crate:#crossover 603 | d=sf(self.pool,weight=self.selection_weight) 604 | c.crossover(d) 605 | 606 | if random.random()<=self.mrate:#mutation 607 | c.mutate() 608 | tries=0 609 | while random.random()<=self.mrate and tries self.pool_size/10): 638 | print "{0}\% of the circuits score overflowed, can't continue reliably. Try to decrease scoring weights.".format(100*float(self.overlflowed)/self.pool_size) 639 | exit() 640 | 641 | self.overflowed = 0 642 | 643 | #Optimize values 644 | if self.generation == 5 or self.generation % 10 == 0: 645 | self.pool = self.optimize_values(self.pool) 646 | self.pool = sorted(self.pool) 647 | 648 | print "Simulations per second: {}".format(round((len(self.spice_commands)*self.pool_size)/(time()-start),1)) 649 | print "Time per generation: {} seconds".format(round(time()-start,1)) 650 | if self.c_free_gens== self.generation: 651 | if self.constraints != None: 652 | print "Constraints enabled" 653 | self.alltimebest = (inf,None) 654 | 655 | if self.gradual_constraints and self.generation>self.c_free_gens: 656 | if self.generation-self.c_free_gens>self.constraint_ramp: 657 | self.gradual_constraints = False 658 | 659 | 660 | if self.plot_all_gens or self.pool[0][0]self.c_free_gens: 673 | if self.generation-self.c_free_gens