Beispiel #1
0
    def get_actives(self, block_material: BlockMaterial):
        '''
        method will go through and set the attributes block_material.active_nodes and active_args.
        active_nodes will include all output_nodes, a subset of main_nodes and input_nodes.
        '''
        ezLogging.info("%s - Inside get_actives" % (block_material.id))
        block_material.active_nodes = set(np.arange(self.main_count, self.main_count+self.output_count))
        block_material.active_args = set()
        #block_material.active_ftns = set()

        # add feeds into the output_nodes
        for node_input in range(self.main_count, self.main_count+self.output_count):
            block_material.active_nodes.update([block_material[node_input]])

        for node_index in reversed(range(self.main_count)):
            if node_index in block_material.active_nodes:
                # then add the input nodes to active list
                block_material.active_nodes.update(block_material[node_index]["inputs"])
                block_material.active_args.update(block_material[node_index]["args"])
            else:
                pass
            
        # sort
        block_material.active_nodes = sorted(list(block_material.active_nodes))
        ezLogging.debug("%s - active nodes: %s" % (block_material.id, block_material.active_nodes))
        block_material.active_args = sorted(list(block_material.active_args))
        ezLogging.debug("%s - active args: %s" % (block_material.id, block_material.active_args))
Beispiel #2
0
    def evaluate(self,
                 block_material: BlockMaterial,
                 block_def,#: BlockDefinition, 
                 training_datapair: ezData,
                 validation_datapair: ezData):
        '''
        stuff the old code has but unclear why
        
            gpus = tf.config.experimental.list_physical_devices('GPU')
            #tf.config.experimental.set_virtual_device_configuration(gpus[0],[
                    tf.config.experimental.VirtualDeviceConfiguration(memory_limit = 1024*3)
                    ])
        '''
        ezLogging.info("%s - Start evaluating..." % (block_material.id))
        try:
            self.build_graph(block_material, block_def, training_datapair)
        except Exception as err:
            ezLogging.critical("%s - Build Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return

        try:
            output = self.train_graph(block_material, block_def, training_datapair, validation_datapair)
        except Exception as err:
            ezLogging.critical("%s - Train Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return
        
        block_material.output = output # TODO make sure it is a list
Beispiel #3
0
 def fill_args(self, block_def: BlockDefinition,
               block_material: BlockMaterial):
     '''
     TODO
     '''
     block_material.args = [None] * block_def.arg_count
     for arg_index, arg_type in enumerate(block_def.arg_types):
         block_material.args[arg_index] = arg_type()
Beispiel #4
0
 def build_block(self, block_def: BlockDefinition, indiv_id):
     '''
     TODO
     '''
     block_material = BlockMaterial(block_def.nickname)
     block_material.set_id(indiv_id)
     self.fill_args(block_def, block_material)
     self.fill_genome(block_def, block_material)
     block_def.get_actives(block_material)
     return block_material
Beispiel #5
0
    def standard_build_graph(self,
                             block_material: BlockMaterial,
                             block_def,#: BlockDefinition, 
                             input_layers = None):
        '''
        trying to generalize the graph building process similar to standard_evaluate()

        For Transfer Learning:
        we expect input_layers to be None, and later when we call function(*inputs, *args), we want to pass in
        an empty list for inputs.
        This also guarentees that no matter how many 'active nodes' we have for our transfer learning block,
        we will only ever use one pretrained model...no inputs are shared between nodes so the models never connect!
        '''
        # add input data
        if input_layers is not None:
            for i, input_layer in enumerate(input_layers):
                block_material.evaluated[-1*(i+1)] = input_layer

        # go solve
        for node_index in block_material.active_nodes:
            if node_index < 0:
                # do nothing. at input node
                continue
            elif node_index >= block_def.main_count:
                # do nothing NOW. at output node. we'll come back to grab output after this loop
                continue
            else:
                # main node. this is where we evaluate
                function = block_material[node_index]["ftn"]
                
                inputs = []
                if input_layers is not None:
                    node_input_indices = block_material[node_index]["inputs"]
                    for node_input_index in node_input_indices:
                        inputs.append(block_material.evaluated[node_input_index])
                    ezLogging.debug("%s - Eval %i; input index: %s" % (block_material.id, node_index, node_input_indices))

                args = []
                node_arg_indices = block_material[node_index]["args"]
                for node_arg_index in node_arg_indices:
                    args.append(block_material.args[node_arg_index].value)
                ezLogging.debug("%s - Eval %i; arg index: %s, value: %s" % (block_material.id, node_index, node_arg_indices, args))

                ezLogging.debug("%s - Eval %i; Function: %s, Inputs: %s, Args: %s" % (block_material.id, node_index, function, inputs, args))
                block_material.evaluated[node_index] = function(*inputs, *args)

        output = []
        if not block_material.dead:
            for output_index in range(block_def.main_count, block_def.main_count+block_def.output_count):
                output.append(block_material.evaluated[block_material.genome[output_index]])
                
        ezLogging.info("%s - Ending evaluating...%i output" % (block_material.id, len(output)))
        return output
Beispiel #6
0
    def evaluate(self,
                 block_material: BlockMaterial,
                 block_def,#: BlockDefinition, 
                 training_datapair: ezData,
                 validation_datapair: ezData):
        ezLogging.info("%s - Start evaluating..." % (block_material.id))

        try:
            self.build_graph(block_material, block_def, training_datapair)
        except Exception as err:
            ezLogging.critical("%s - Build Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return

        block_material.output = [training_datapair, validation_datapair]
Beispiel #7
0
def mutate_single_argvalue(mutant_material: BlockMaterial,
                           block_def):  #: BlockDefinition):
    '''
    instead of looking for a different arg index in .args with the same arg type,
    mutate the value stored in this arg index.
    '''
    ezLogging.info("%s - Inside mutate_single_argvalue" % (mutant_material.id))
    if len(mutant_material.active_args) > 0:
        # if block has arguments, then there is something to mutate
        choices = np.arange(block_def.arg_count)
        choices = rnd.choice(choices, size=len(choices),
                             replace=False)  #randomly reorder
        for arg_index in choices:
            mutant_material.args[arg_index].mutate()
            ezLogging.info("%s - Mutated node %i; new arg value: %s" %
                           (mutant_material.id, arg_index,
                            mutant_material.args[arg_index]))
            if arg_index in mutant_material.active_args:
                # active_arg finally mutated
                ezLogging.debug("%s - Mutated node %i - active" %
                                (mutant_material.id, arg_index))
                mutant_material.need_evaluate = True
                break
            else:
                ezLogging.debug("%s - Mutated node %i - inactive" %
                                (mutant_material.id, arg_index))
    else:
        # won't actually mutate
        ezLogging.warning("%s - No active args to mutate" %
                          (mutant_material.id))
Beispiel #8
0
 def evaluate(self,
              block_material: BlockMaterial,
              block_def,#: BlockDefinition, 
              training_datapair: ezData,
              validation_datapair: ezData=None):
     ezLogging.info("%s - Start evaluating..." % (block_material.id))
     output_list = self.standard_evaluate(block_material, block_def, training_datapair.x)
     #training_datapair.x = output_list[0]
     #block_material.output = training_datapair
     block_material.output = output_list
Beispiel #9
0
 def evaluate(self,
              block_material: BlockMaterial,
              block_def, #: BlockDefinition,
              training_datapair: ezData,
              validation_datapair: ezData):
     ezLogging.info("%s - Start evaluating..." % (block_material.id))
     
     output_list = self.standard_evaluate(block_material, block_def, [training_datapair.pipeline])
     training_datapair.pipeline = output_list[0] #assuming only outputs the pipeline
     
     block_material.output = [training_datapair, validation_datapair]
Beispiel #10
0
    def evaluate(self,
                 block_material: BlockMaterial,
                 block_def,#: BlockDefinition, 
                 training_datapair: ezData,
                 validation_datapair: ezData):
        ezLogging.info("%s - Start evaluating..." % (block_material.id))

        try:
            # check if this fails our conditions
            if block_def.main_count != 1:
                raise self.TooManyMainNodes(block_def.main_count)
            if 0 not in block_material.active_nodes:
                raise self.NoActiveMainNodes(block_material.active_nodes)
            # good to continue to build
            self.build_graph(block_material, block_def, training_datapair)
        except Exception as err:
            ezLogging.critical("%s - Build Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return

        block_material.output = [training_datapair, validation_datapair]
Beispiel #11
0
    def evaluate(self,
                 block_material: BlockMaterial,
                 block_def,#: BlockDefinition, 
                 training_datapair: ezData,
                 validation_datapair: ezData):
        ezLogging.info("%s - Start evaluating..." % (block_material.id))
        try:
            self.build_graph(block_material, block_def, training_datapair.pipeline_wrapper)
        except Exception as err:
            ezLogging.critical("%s - Build Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return

        try:
            # outputs a list of the validation metrics
            output = self.train_graph(block_material, block_def, training_datapair, validation_datapair)
        except Exception as err:
            ezLogging.critical("%s - Train Graph; Failed: %s" % (block_material.id, err))
            block_material.dead = True
            import pdb; pdb.set_trace()
            return
        
        block_material.output = [None, output] # TODO make sure it is a list
Beispiel #12
0
def mutate_single_argindex(mutant_material: BlockMaterial,
                           block_def):  #: BlockDefinition):
    '''
    search through the args and try to find a matching arg_type and use that arg index instead
    '''
    ezLogging.info("%s - Inside mutate_single_argindex" % (mutant_material.id))
    if len(mutant_material.active_args) > 0:
        # then there is something to mutate
        choices = []  # need to find those nodes with 'args' filled
        #weights = [] # option to sample node_index by the number of args for each node
        for node_index in range(block_def.main_count):
            if len(mutant_material[node_index]["args"]) > 0:
                choices.append(node_index)
                #weights.append(len(mutant_material[node_index]["args"]))
            else:
                pass

        choices = rnd.choice(choices, size=len(choices),
                             replace=False)  #randomly reorder
        for node_index in choices:
            ith_arg = rnd.choice(
                np.arange(len(mutant_material[node_index]["args"])))
            current_arg = mutant_material[node_index]["args"][ith_arg]
            arg_dtype = block_def.get_node_dtype(mutant_material, node_index,
                                                 "args")[ith_arg]
            new_arg = block_def.get_random_arg(arg_dtype,
                                               exclude=[current_arg])
            if new_arg is None:
                # failed to find a new_arg
                continue
            else:
                mutant_material[node_index]["args"][ith_arg] = new_arg
                ezLogging.info(
                    "%s - Mutated node %i; ori arg index: %i, new arg index: %i"
                    % (mutant_material.id, node_index, current_arg, new_arg))
                if node_index in mutant_material.active_nodes:
                    # active_node finally mutated
                    ezLogging.debug("%s - Mutated node %i - active" %
                                    (mutant_material.id, node_index))
                    mutant_material.need_evaluate = True
                    break
                else:
                    ezLogging.debug("%s - Mutated node %i - inactive" %
                                    (mutant_material.id, node_index))
    else:
        # won't actually mutate
        ezLogging.warning("%s - No active args to mutate" %
                          (mutant_material.id))
Beispiel #13
0
    def evaluate(self,
                 block_material: BlockMaterial,
                 block_def,#: BlockDefinition, 
                 training_datapair: ezData,
                 validation_datapair: ezData):
        ezLogging.info("%s - Start evaluating..." % (block_material.id))
        
        # going to treat training + validation as separate block_materials!
        output = []
        for datapair in [training_datapair, validation_datapair]:
            single_output_list = self.standard_evaluate(block_material, block_def, [datapair.pipeline])
            datapair.pipeline = single_output_list[0]
            if block_material.dead:
                return []
            else:
                output.append(datapair)
                self.preprocess_block_evaluate(block_material) #prep for next loop through datapair
 
        block_material.output = output
Beispiel #14
0
    def get_lisp(self, block_material: BlockMaterial):
        '''
        the idea is to help with seeding, we have the ability to collapse an individual into a tree
        and then collapse into a lisp string representation

        note we'll be using active nodes a lot here...and don't forget that "active_nodes will include
        all output_nodes, a subset of main_nodes and input_nodes."
        '''
        block_material.lisp = []

        # get actives just in case it has changed since last evaluated...but it really shouldn't have!
        self.get_actives(block_material)

        # each output will have it's own tree
        #for ith_output in range(self.output_count):

        # first going to create a dictionary of the active nodes inputs
        _active_dict = {}
        #output_node = self.main_count+ith_output
        #_active_dict['output'] = block_material[output_node]
        for ith_node in reversed(block_material.active_nodes):
            if (ith_node<0) or (ith_node>=self.main_count):
                #input or ouptput node
                continue
            func = block_material[ith_node]['ftn']
            inputs = block_material[ith_node]['inputs']
            args = block_material[ith_node]['args']

            # now start to shape it into a lisp
            lisp = ['%s' % func.__name__]
            for _input in inputs:
                # attach an 'n' to remind us that this is a node number and not an arg
                # later we'll go through and replace each node with it's own entry in _active_dict
                lisp.append('%in' % _input)
            for _arg in args:
                lisp.append('%s' % str(block_material.args[_arg]))

            # and now throw the lisp into our _active_dict (tree)
            _active_dict['%i' % ith_node] = lisp

        # at this point we have the ith node and arg values for each active node
        # now we'll go through the list and replace each node with each entry from the dict
        # this is how we slowly build out the branches of the trees
        for ith_node in block_material.active_nodes:
            if (ith_node<0) or (ith_node>=self.main_count):
                #input or ouptput node
                continue
            lisp = _active_dict[str(ith_node)]
            new_lisp = []
            for i, val in enumerate(lisp):
                if i == 0:
                    # 0th position in lisp should be the function. keep and append.
                    pass
                elif val.endswith('n'):
                    if int(val[:-1]) < 0:
                        '''
                        decided not to use the datatype and just assume that input datatypes match
                        ...this stuff unecessarily complicates loading in an individual
                        so going to just pass it as -1n or -2n etc
                        
                        #input node so we want to instead pass in the datatype we expect
                        # -1th genome is 0th input
                        # -2nd genome is 1th input... genome_node*-1 - 1 = input_node
                        val = self.input_dtypes[(int(val[:-1])*-1)-1]'''
                        pass
                        
                    else:
                        # then it's a node number, replace with that node's new_lisp
                        val = _active_dict[str(val[:-1])] #[:-1] to remove 'n'
                else:
                    # then it's an arg and we pass it as such
                    pass
                new_lisp.append(val)
            # replace lisp with new_lisp in the dict
            _active_dict[str(ith_node)] = new_lisp

        # now all that should be left are the output nodes
        # each output node produces it's own tree so it's own final lisp
        for ith_output in range(self.output_count):
            final_node = block_material[self.main_count+ith_output]
            # convert the final list into a string
            lisp_str = str(_active_dict[str(final_node)])
            # since it was a list of strings, there will be a mess of quotes inside
            # so replace any quotes, and spaces while we're at it
            lisp_str = lisp_str.replace("'","").replace('"','').replace(" ", "")
            # that's our tree. so append to lisp
            block_material.lisp.append(lisp_str)
Beispiel #15
0
    def fill_genome(self, block_def: BlockDefinition,
                    block_material: BlockMaterial):
        '''
        TODO
        '''
        block_material.genome = [None] * block_def.genome_count
        block_material.genome[(
            -1 * block_def.input_count):] = ["InputPlaceholder"
                                             ] * block_def.input_count

        # fill main nodes
        for node_index in range(block_def.main_count):
            ftns = block_def.get_random_ftn(return_all=True)
            for ftn in ftns:
                # find inputs
                input_dtypes = block_def.operator_dict[ftn]["inputs"]
                input_index = [None] * len(input_dtypes)
                for ith_input, input_dtype in enumerate(input_dtypes):
                    input_index[ith_input] = block_def.get_random_input(
                        block_material, req_dtype=input_dtype, _max=node_index)
                if None in input_index:
                    # failed to fill it in; try another ftn
                    continue
                else:
                    pass

                # find args
                arg_dtypes = block_def.operator_dict[ftn]["args"]
                arg_index = [None] * len(arg_dtypes)
                for ith_arg, arg_dtype in enumerate(arg_dtypes):
                    arg_index[ith_arg] = block_def.get_random_arg(
                        req_dtype=arg_dtype)
                if None in arg_index:
                    # failed to fill it in; try another ftn
                    continue
                else:
                    pass

                # all complete
                block_material[node_index] = {
                    "ftn": ftn,
                    "inputs": input_index,
                    "args": arg_index
                }
                break
            # error check that node got filled
            if block_material[node_index] is None:
                print(
                    "GENOME ERROR: no primitive was able to fit into current genome arrangment"
                )
                import pdb
                pdb.set_trace()
                exit()

        # fill output nodes
        for ith_output, node_index in enumerate(
                range(block_def.main_count,
                      block_def.main_count + block_def.output_count)):
            req_dtype = block_def.output_dtypes[ith_output]
            block_material[node_index] = block_def.get_random_input(
                block_material,
                req_dtype=req_dtype,
                _min=0,
                _max=block_def.main_count)
Beispiel #16
0
def mutate_single_ftn(mutant_material: BlockMaterial,
                      block_def):  #: BlockDefinition):
    '''
    pick nodes at random and mutate the ftn-index until an active node is selected
    will mutate the function to anything with matching input/arg dtype.
    if the expected input datatypes don't match the current genome,
    it will find a new input/arg that will match
    '''
    ezLogging.info("%s - Inside mutate_single_ftn" % (mutant_material.id))
    choices = np.arange(block_def.main_count)
    choices = rnd.choice(choices, size=len(choices),
                         replace=False)  #randomly reorder
    for node_index in choices:
        # note, always will be a main_node
        current_ftn = mutant_material[node_index]["ftn"]
        req_output_dtype = block_def.operator_dict[current_ftn]["output"]
        new_ftn = block_def.get_random_ftn(req_dtype=req_output_dtype,
                                           exclude=[current_ftn])
        ezLogging.debug("%s - Mutated node %i - possible new ftn: %s" %
                        (mutant_material.id, node_index, new_ftn))

        # make sure input_dtypes match
        req_input_dtypes = block_def.operator_dict[new_ftn]["inputs"]
        new_inputs = [None] * len(req_input_dtypes)
        for input_index in mutant_material[node_index]["inputs"]:
            exist_input_dtype = block_def.get_node_dtype(
                mutant_material, input_index,
                "output")  #instead of verify from current node, goes to input
            for ith_input, (new_input, input_dtype) in enumerate(
                    zip(new_inputs, req_input_dtypes)):
                if (new_input is None) and (input_dtype == exist_input_dtype):
                    # then we can match an existing input with one of our required inputs
                    new_inputs[ith_input] = input_index
                else:
                    pass
        # now try and fill in anything still None
        for ith_input, (new_input, input_dtype) in enumerate(
                zip(new_inputs, req_input_dtypes)):
            if new_input is None:
                new_inputs[ith_input] = block_def.get_random_input(
                    mutant_material, req_dtype=input_dtype, _max=node_index)
        # if there is still 'None' then we failed to fit this ftn in...try another ftn
        if None in new_inputs:
            continue
        else:
            ezLogging.debug("%s - Mutated node %i - possible new inputs: %s" %
                            (mutant_material.id, node_index, new_inputs))

        # make sure arg_dtypes match
        req_arg_dtypes = block_def.operator_dict[new_ftn]["args"]
        new_args = [None] * len(req_arg_dtypes)
        exist_arg_dtypes = block_def.get_node_dtype(mutant_material,
                                                    node_index, "args")
        for arg_index, exist_arg_dtype in zip(
                mutant_material[node_index]["args"], exist_arg_dtypes):
            for ith_arg, (new_arg, req_arg_dtype) in enumerate(
                    zip(new_args, req_arg_dtypes)):
                if (new_arg is None) and (req_arg_dtypes == exist_arg_dtype):
                    new_args[ith_arg] = arg_index
                else:
                    pass
        # now try and fill in anything still None
        for ith_arg, (new_arg,
                      req_arg_dtype) in enumerate(zip(new_args,
                                                      req_arg_dtypes)):
            if new_arg is None:
                new_args[ith_arg] = block_def.get_random_arg(
                    req_dtype=req_arg_dtype)
        # if there is still 'None' then we failed to fit this ftn ...try another ftn
        if None in new_args:
            continue
        else:
            ezLogging.debug("%s - Mutated node %i - possible new args: %s" %
                            (mutant_material.id, node_index, new_args))

        # at this point we found a ftn and fit inputs and args
        mutant_material[node_index]["ftn"] = new_ftn
        mutant_material[node_index]["inputs"] = new_inputs
        mutant_material[node_index]["args"] = new_args
        ezLogging.debug(
            "%s - Mutated node %i; old_ftn: %s, new_ftn: %s, new_inputs: %s, new_args %s"
            % (mutant_material.id, node_index, current_ftn, new_ftn,
               new_inputs, new_args))
        if node_index in mutant_material.active_nodes:
            # active_node finally mutated
            ezLogging.debug("%s - Mutated node %i - active" %
                            (mutant_material.id, node_index))
            mutant_material.need_evaluate = True
            break
        else:
            ezLogging.debug("%s - Mutated node %i - inactive" %
                            (mutant_material.id, node_index))
            pass
Beispiel #17
0
def mutate_single_input(mutant_material: BlockMaterial,
                        block_def):  #: BlockDefinition):
    '''
    pick nodes at random and mutate the input-index until an active node is selected
    when mutating inputs, it will look for a node that outputs the matching datatype of the current node's input
    so it can fail at doing so and won't mutate that node
    '''
    ezLogging.info("%s - Inside mutate_single_input" % (mutant_material.id))
    choices = np.arange(block_def.main_count + block_def.output_count)
    choices = rnd.choice(choices, size=len(choices),
                         replace=False)  #randomly reorder
    for node_index in choices:
        if node_index < block_def.main_count:
            # then we are mutating a main-node (expect a node-dict)
            num_inputs_into_node = len(mutant_material[node_index]["inputs"])
            ith_input = rnd.choice(np.arange(num_inputs_into_node))
            current_input_index = mutant_material[node_index]["inputs"][
                ith_input]
            req_dtype = block_def.get_node_dtype(mutant_material, node_index,
                                                 "inputs")[ith_input]
            new_input = block_def.get_random_input(
                mutant_material,
                req_dtype=req_dtype,
                _max=node_index,
                exclude=[current_input_index])
            if new_input is None:
                # failed to find new input, will have to try another node to mutate
                continue
            else:
                mutant_material[node_index]["inputs"][ith_input] = new_input
                ezLogging.debug(
                    "%s - Mutated node %i; ori_input: %i, new_input: %i" %
                    (mutant_material.id, node_index, current_input_index,
                     new_input))
                if node_index in mutant_material.active_nodes:
                    # active_node finally mutated
                    ezLogging.debug("%s - Mutated node %i - active" %
                                    (mutant_material.id, node_index))
                    mutant_material.need_evaluate = True
                    break
                else:
                    ezLogging.debug("%s - Mutated node %i - inactive" %
                                    (mutant_material.id, node_index))
                    pass
        else:
            # then we are mtuating an output-node (expect a int index value)
            current_output_index = mutant_material[node_index]
            req_dtype = block_def.output_dtypes[node_index -
                                                block_def.main_count]
            new_output_index = block_def.get_random_input(
                mutant_material,
                req_dtype=req_dtype,
                _min=0,
                exclude=[current_output_index])
            if new_output_index is None:
                # failed to find new node
                continue
            else:
                mutant_material[node_index] = new_output_index
                ezLogging.debug(
                    "%s - Mutated node %i; ori_input: %i, new_input: %i" %
                    (mutant_material.id, node_index, current_output_index,
                     new_output_index))
                # active_node finally mutated
                ezLogging.debug("%s - Mutated node %i - active" %
                                (mutant_material.id, node_index))
                mutant_material.need_evaluate = True
                break
Beispiel #18
0
    def build_block_from_lisp(self, block_def: BlockDefinition, lisp: str,
                              indiv_id):
        '''
        the expectation here is that lisp is the string tree representation (not a file holding the str)
        that follows the format of how we build out a lisp in codes.block_definitions.block_definition.get_lisp()

        shoud look something like: [func1,[func2,-2n,-1n],-1n]
        
        we also can handle cases where we are 'reusing' the output of a node...thanks to this line
            ```lisp = lisp.replace(_active_dict[ith_node], "%in" % ith_node)```
        try with: lisp = '[mult,-1n,[mult,[sub,[mult,-1n,-1n],-2n],[sub,[mult,-1n,-1n],-2n]]]'

        NOTE: I think this currently only works if the block has 1 output!
        '''
        _active_dict = {}
        ith_node = 0
        while True:
            # from the start of the string, keep looking for lists []
            match = re.search("\[[0-9A-Za-z_\-\s.,']+\]", lisp)
            if match is None:
                # no more lists inside lisp. so we're done
                break
            else:
                # get the single element lisp
                _active_dict[ith_node] = lisp[match.start():match.end()]
                # now replace that element with the node number
                # add 'n' to distinguish from arg value
                lisp = lisp.replace(_active_dict[ith_node], "%in" % ith_node)
                # increment to next node
                ith_node += 1

                if ith_node >= 10**3:
                    # very unlikely to have more than 1000 nodes...prob something went wrong
                    ezLogging.error("something went wrong")
                    break

        # now build the individual
        block_material = BlockMaterial(block_def.nickname)
        block_material.set_id(indiv_id)
        block_material.args = [None] * block_def.arg_count
        block_material.genome = [None] * block_def.genome_count
        block_material.genome[(
            -1 * block_def.input_count):] = ["InputPlaceholder"
                                             ] * block_def.input_count

        ith_active_node = -1
        args_used = []  # so we don't overwrite an arg we already used
        active_main_nodes = sorted(
            np.random.choice(range(block_def.main_count),
                             size=len(_active_dict),
                             replace=False))
        for node_index in range(block_def.main_count):
            if node_index in active_main_nodes:
                ith_active_node += 1
                # fill node with what we got from the lisp
                lisp = _active_dict[ith_active_node].strip('][').split(',')
                for ftn in block_def.operator_dict.keys():
                    if ftn.__name__ == lisp[0]:
                        # we matched our lisp ftn with entry in operatordict
                        input_index = []
                        arg_index = []
                        ith_input = -1
                        ith_arg = -1
                        # now grab what we have in the lisp and make sure they match
                        for val in lisp[
                                1:]:  # [1:] #ignores the ftn in 0th element
                            if val.endswith('n'):
                                # then it's a node index
                                ith_input += 1
                                extracted_val = int(
                                    val[:-1])  # [:-1] to remove 'n'
                                if extracted_val >= 0:
                                    # then it's a main node
                                    input_index.append(
                                        active_main_nodes[int(extracted_val)])
                                    # verify that the data types match
                                    incoming_dtype = block_def.get_node_dtype(
                                        block_material,
                                        node_index=input_index[ith_input],
                                        key='output')
                                    expected_dtype = block_def.operator_dict[
                                        ftn]["inputs"][ith_input]
                                    if incoming_dtype != expected_dtype:
                                        ezLogging.error(
                                            "error in genome seeding...mismatching incoming + given data types"
                                        )
                                        import pdb
                                        pdb.set_trace()
                                        return None
                                    else:
                                        # all good
                                        pass
                                else:
                                    # then it's an input node
                                    input_index.append(extracted_val)
                            else:
                                # then it's an arg value
                                ith_arg += 1
                                req_arg_type = block_def.operator_dict[ftn][
                                    "args"][ith_arg]
                                poss_arg_index = block_def.get_random_arg(
                                    req_arg_type, exclude=args_used)
                                if poss_arg_index is None:
                                    ezLogging.error(
                                        "can't find matching arg type in seeding"
                                    )
                                    import pdb
                                    pdb.set_trace()
                                    return None
                                arg_index.append(poss_arg_index)
                                args_used.append(poss_arg_index)
                                # have to convert val which is still a string to expected datatype!
                                # kinda hacky but should work
                                if 'float' in req_arg_type.__name__.lower():
                                    val = float(val)
                                elif 'int' in req_arg_type.__name__.lower():
                                    val = int(val)
                                elif 'bool' in req_arg_type.__name__.lower():
                                    val = bool(val)
                                else:
                                    pass
                                block_material.args[
                                    poss_arg_index] = req_arg_type(value=val)

                        block_material[node_index] = {
                            "ftn": ftn,
                            "inputs": input_index,
                            "args": arg_index
                        }
                        break
                    else:
                        # ftn doesn't match our lisp
                        continue
            else:
                # then this is not an active main node...just fill it normally then
                # copy paste from fill_nodes
                ftns = block_def.get_random_ftn(return_all=True)
                for ftn in ftns:
                    # find inputs
                    input_dtypes = block_def.operator_dict[ftn]["inputs"]
                    input_index = [None] * len(input_dtypes)
                    for ith_input, input_dtype in enumerate(input_dtypes):
                        input_index[ith_input] = block_def.get_random_input(
                            block_material,
                            req_dtype=input_dtype,
                            _max=node_index)
                    if None in input_index:
                        # failed to fill it in; try another ftn
                        continue
                    else:
                        pass
                    # find args
                    arg_dtypes = block_def.operator_dict[ftn]["args"]
                    arg_index = [None] * len(arg_dtypes)
                    for ith_arg, arg_dtype in enumerate(arg_dtypes):
                        arg_index[ith_arg] = block_def.get_random_arg(
                            req_dtype=arg_dtype)
                    if None in arg_index:
                        # failed to fill it in; try another ftn
                        continue
                    else:
                        pass
                    # all complete
                    block_material[node_index] = {
                        "ftn": ftn,
                        "inputs": input_index,
                        "args": arg_index
                    }
                    break
                # error check that node got filled
                if block_material[node_index] is None:
                    print(
                        "GENOME ERROR: no primitive was able to fit into current genome arrangment"
                    )
                    import pdb
                    pdb.set_trace()
                    return None

        # output node
        # currently only works for 1 output node
        block_material[block_def.main_count] = active_main_nodes[-1]

        # now finish filling in the args
        for arg_index, arg_type in enumerate(block_def.arg_types):
            if block_material.args[arg_index] is None:
                block_material.args[arg_index] = arg_type()
            else:
                continue

        block_def.get_actives(block_material)
        return block_material