示例#1
0
    def create(self,popParam):

        indType = agent.individualTypes.find(popParam.indTypeName)
        netdef = popParam.genomeDefinition
        idGenerator = networks.idGenerators.find(netdef.idGenerator)

        genomeFactory = indType.genomeFactory
        if popParam.genomeFactory is not None: genomeFactory = popParam.genomeFactory
        factoryParam = popParam.factoryParam

        size = popParam.size

        genomes = []
        for i in range(size):
            netid = idGenerator.getNetworkId()
            net = NeuralNetwork(netid,netdef)

            # 创建输入神经元
            self.__createLayerNeuron(net,netdef,0,netdef.neuronCounts[0],netdef.models.input)

            # 创建输出神经元
            self.__createLayerNeuron(net,netdef,len(netdef.neuronCounts)-1,netdef.neuronCounts[-1], netdef.models.hidden)

            # 创建中间神经元(如果有)
            hiddenCount = netdef.neuronCounts[1:-1]
            if not collections.isEmpty(hiddenCount):
                for i in range(len(hiddenCount)):
                    self.__createLayerNeuron(net, netdef, i+1, netdef.neuronCounts[i+1], netdef.models.input)

            genomes.append(net)
            # 初始化连接
            if popParam.factoryParam.connectionRate <= 0:         #初始无连接
                continue
            elif popParam.factoryParam.connectionRate >= 1:       #初始全连接
                for i in range(len(net.neurons)-1):
                    for j,n1 in enumerate(net.neurons[i]):       # 取第i层神经元
                        for k,n2 in enumerate(net.neurons[i+1]): # 取第i+1层神经元
                            synapseid = idGenerator.getSynapseId(net,n1.id,n2.id)
                            synapse = Synapse(synapseid,0,n1.id,n2.id,netdef.models.synapse)
                            net.synapses.append(synapse)
            else:                                                # 以一定概率进行连接
                allsynapse = []
                for i in range(len(net.neurons)-1):
                    for j,n1 in enumerate(net.neurons[i]):       # 取第i层神经元
                        for k,n2 in enumerate(net.neurons[i+1]): # 取第i+1层神经元
                            allsynapse.append((n1,n2))
                synapsecount = int(len(allsynapse)*popParam.factoryParam.connectionRate)
                if synapsecount > 0 :
                    indexes = np.random.uniform(0,len(allsynapse)-1,synapsecount)
                    for i in indexes:
                        n1 = allsynapse[i][0]
                        n2 = allsynapse[i][1]
                        synapseid = idGenerator.getSynapseId(net, n1.id, n2.id)
                        synapse = Synapse(synapseid, 0, n1.id, n2.id, netdef.models.synapse)
                        net.synapses.append(synapse)


            logging.debug('# neat工厂创建网络:'+str(net))

        return genomes
 def getAllCacheNeuronIds(self):
     '''
     取得所有缓存的神经元id
     :return: list 按照从小到大排序的神经元id
     '''
     nids = [] if collections.isEmpty(self.neuronIdcaches) else list(self.neuronIdcaches.values())
     nids.sort()
     return nids
示例#3
0
 def recordIndividuals(self):
     logInds = self.__getLogIndividuals()
     if collections.isEmpty(logInds): return
     kwdatas = {}
     for ind in logInds:
         kwdatas['ind' + str(ind.id)] = str(ind)
     self.__recordSection('重要个体', **kwdatas)
     if self.callback is not None: self.callback('inds.record', self)
示例#4
0
 def __do_mutate_deletenode(self, ind, session):
     net = ind.genome
     ns = net.getHiddenNeurons()
     if collections.isEmpty(ns):
         return False, '', 'deletenode', None
     # 随机选择
     neuron = choice(ns)
     net.remove(neuron)
     return True, '', 'deletenode', neuron
 def getAllCacheSynasesIds(self,net=None):
     '''
     取得所有缓存的突触id
     :param net: 所属网络,如果为None,则为所有网络的合并
     :return: list 按照从小到大排序的神经元id
     '''
     sids = [] if collections.isEmpty(self.synapseIdcaches) else list(self.synapseIdcaches.values())
     sids.sort()
     return sids
示例#6
0
 def getPrevNeuron(self, neuronId):
     '''
     取得某神经元的前序连接神经元
     :param neuronId:
     :return:
     '''
     synapses = self.getInputSynapse(neuronId)
     if collections.isEmpty(synapses): return []
     return list(map(lambda s: self.getNeuron(id=s.fromId), synapses))
示例#7
0
 def getNextNeuron(self, neuronId):
     '''
     取得某神经元的后序连接神经元
     :param neuronId:
     :return:
     '''
     synapses = self.getOutputSynapse(neuronId)
     if collections.isEmpty(synapses): return []
     return list(map(lambda s: self.getNeuron(id=s.toId), synapses))
示例#8
0
 def __do_mutate_activationFunction(self, ind, session):
     net = ind.genome
     ns = net.getHiddenNeurons()
     if collections.isEmpty(ns):
         return False, '', 'modifyactivationFunction', None
     # 随机选择
     neuron = choice(ns)
     activationFunction = neuron._doSelectActiovationFunction()
     return True, '', 'modifyactivationFunction', str(
         neuron) + ':' + activationFunction.nameInfo.name
示例#9
0
 def __init__(self, **configuration):
     '''
     普通输入模型
     :param configuration: 模型缺省配置
     '''
     self.nameInfo = CommonInputNeurnModel.nameInfo
     self.configuration = configuration if not collections.isEmpty(
         configuration) else {}
     self.initStates = CommonInputNeurnModel.__initStates
     self.variables = CommonInputNeurnModel.__variables
示例#10
0
 def getNeuronLayerCount(self):
     '''
     每层神经元数量
     :return: dict 每层的神经元数量,key是层id,value是数量
     '''
     r = {}
     for index, ns in enumerate(self.neurons):
         if collections.isEmpty(ns): continue
         r[ns[0].layer] = r.get(ns[0].layer, 0) + 1
     return r
示例#11
0
 def __do_mutate_deleteconnection(self, ind, session):
     net = ind.genome
     synapses = net.getSynapses()
     if collections.isEmpty(synapses):
         return False, '', 'deleteconnection', None
     # 随机选择
     s = choice(synapses)
     net.remove(s)
     session.monitor.recordDebug(NeatMutate.name,
                                 'ind' + str(ind.id) + '删除连接', str(s.id))
     return True, '', 'deleteconnection', s
示例#12
0
    def __recordSection(self, stageName, **kwdatas):
        contents = '事件 = '+stageName + \
                    ',年代 = '+('' if self.evoTask.curSession is None else str(int(self.evoTask.curSession.curTime))) + \
                    ',时间 = '+time.strftime('%H:%M:%S',time.localtime(time.time()))
        self.logger.info("")
        self.logger.info(contents)

        #keys = [] if collections.isEmpty(kwdatas) else kwdatas.keys()
        keys = [] if collections.isEmpty(kwdatas) else kwdatas
        for key in keys:
            self.logger.info(key + ':' + str(kwdatas[key]))
示例#13
0
 def _initVariableValue(self):
     '''
     根据模型配置初始号变量的值
     :return:
     '''
     if not collections.isEmpty(self.variables):
         for var in self.variables:
             if var.type is not float:
                 continue
             if var.nameInfo.name in self.modelConfiguration:
                 var.range = Range(self.modelConfiguration[var.nameInfo.name])
                 var.value = 0 if var.range is None else var.range.sample()
示例#14
0
 def __init__(self,id,inds,pop):
     '''
     物种
     :param id:    int 物种id
     :param inds:  list  of int or list of Individuals 属于该种群的个体
     :param pop:   物种
     '''
     self.id = id
     self.pop = pop
     self.targetSize = 0
     self.indids = []
     if not collections.isEmpty(inds):
         self.indids = list(map(lambda ind:ind.id if isinstance(ind,Individual) else ind,inds))
     self.features = {}
     self.__doEvaulate()
示例#15
0
    def execute(self, session):
        n_clusters = session.popParam.species.size
        method = session.popParam.species.method
        iter = session.popParam.species.iter
        alg = session.popParam.species.get('alg', 'kmean')

        # 取得所有个体的特征向量
        idgenerator = networks.idGenerators.find(
            session.popParam.genomeDefinition.idGenerator)
        inds = session.pop.inds
        indVecs = np.array(
            list(map(lambda ind: self.__getIndVector(ind, session), inds)))
        if indVecs.dtype is np.dtype('O'):
            indVecs_new = []
            for sn, indVec in enumerate(indVecs):
                indVec1 = indVec.astype(np.float64)
                indVecs_new.append(indVec1)
                if np.array(indVecs_new).dtype is np.dtype('O'):
                    if len(indVec1) > len(indVecs_new[0]):
                        indVecs_new[-1] = indVec1[:len(indVecs_new[0])]
            indVecs = np.array(indVecs_new)
        indArray = whiten(indVecs)
        if alg == 'kmean':
            centroids, distortion = kmeans(obs=indArray,
                                           k_or_guess=n_clusters,
                                           iter=iter)
            labels = vq(indArray, centroids)
            species = []
            for index, center in enumerate(centroids):
                species_inds = [
                    ind for i, ind in enumerate(inds)
                    if i in np.argwhere(labels[0] == index)
                ]
                if collections.isEmpty(species_inds): continue
                specieId = idgenerator.getSpeciesid(
                    list(map(lambda ind: ind.genome, species_inds)))
                for ind in species_inds:
                    ind.speciedId = specieId
                sp = Specie(id=specieId, inds=species_inds, pop=session.pop)
                species.append(sp)
            session.pop.species = species

            return session.pop.species
        else:
            raise RuntimeError('物种分类算法名称无效(popParam.species.alg):' + alg)
示例#16
0
def neat_callback(event, monitor):
    # session结束的时候将精英个体的图形写入文件
    if event == 'session.end':
        filename = 'session' + str(monitor.evoTask.curSession.taskxh) + ".ind"
        eliest = monitor.evoTask.curSession.pop.eliest
        if collections.isEmpty(eliest): return
        optimaInd = eliest[0]
        net = optimaInd.genome

        netviewer = NetworkView()
        netviewer.drawNet(net,
                          filename=filename + str(optimaInd.id) + '.svg',
                          view=False)
        #collections.foreach(eliest,lambda ind : netviewer.drawNet(filename=filename+ind.id+'.svg',view=False))

        #evolutionView = EvolutionViewer()
        #evolutionView.drawSession(monitor,monitor.evoTask.curSession,'fitness')

    return True
示例#17
0
    def getNeuron(self, id=-1, layer=-1, xhInLayer=-1, coord=None):
        '''
        查找满足特定条件神经元(先按id,再按坐标,在按层和层内序号)
        :param id:       神经元id,有效则会优先查找
        :param layer:    所在层
        :param xhInLayer:  层内序号
        :param coord:    坐标
        :return:
        '''
        if id > 0:  #优先按照id查找
            ns = self.getNeurons()
            return collections.first(ns, lambda n: n.id == id)

        if coord is not None:  #其次按照特定坐标寻找
            ns = self.getNeurons()
            return collections.first(ns, lambda n: n.coord == coord)

        #查找特定层中某个序号的神经元
        ns = self.getNeurons(layer=layer)
        if not collections.isEmpty(ns): return None
        if xhInLayer >= len(ns): return None
        return ns[xhInLayer]
示例#18
0
    def activate(self, net, inputs):
        '''
        激活网络
        :param net:  测试网络
        :param task: 测试任务
        :return: outputs
        '''
        # 取得输入
        inputNeurons = net.getInputNeurons()

        # 重置神经元和突触状态
        collections.foreach(net.getNeurons(), lambda n: n.reset())
        collections.foreach(net.getSynapses(), lambda s: s.reset())

        # 设置输入
        for d, v in enumerate(inputs):
            if d >= len(inputNeurons): break
            model = models.nervousModels.find(
                inputNeurons[d].modelConfiguration.modelid)
            model.execute(inputNeurons[d], net, value=v)

            s = net.getOutputSynapse(inputNeurons[d].id)
            if collections.isEmpty(s): continue

            collections.foreach(s, lambda x: x.getModel().execute(x, net))

        # 反复执行
        ns = net.getNeurons()
        neuronCount = net.getNeuronCount()
        iterCount = 0
        outputNeurons = net.getOutputNeurons()
        #while not collections.all(outputNeurons,lambda n:'value' in n.states.keys()) and iterCount<=neuronCount:
        while not collections.all(
                outputNeurons,
                lambda n: 'value' in n.states) and iterCount <= neuronCount:
            iterCount += 1
            #uncomputeNeurons = collections.findall(ns,lambda n:'value' not in n.states.keys())
            uncomputeNeurons = collections.findall(
                ns, lambda n: 'value' not in n.states)
            if collections.isEmpty(uncomputeNeurons): break
            for n in uncomputeNeurons:
                model = n.getModel()
                synapses = net.getInputSynapse(n.id)
                if collections.isEmpty(synapses): continue
                #if not collections.all(synapses,lambda s:'value' in s.states.keys()):continue
                if not collections.all(synapses,
                                       lambda s: 'value' in s.states):
                    continue
                model.execute(n, net)

                synapses = net.getOutputSynapse(n.id)
                if collections.isEmpty(synapses): continue
                collections.foreach(synapses,
                                    lambda s: s.getModel().execute(s, net))

        # 将没结果的输出神经元的值设置为0
        #outputNeuronsWithNoResult = collections.findall(outputNeurons,lambda n:'value' not in n.states.keys())
        outputNeuronsWithNoResult = collections.findall(
            outputNeurons, lambda n: 'value' not in n.states)
        if not collections.isEmpty(outputNeuronsWithNoResult):
            collections.foreach(outputNeuronsWithNoResult,
                                lambda n: exec("n['value']=0"))
        # 取得结果
        outputs = list(map(lambda n: n['value'], outputNeurons))
        if len(outputs) == 1: outputs = outputs[0]
        return outputs
示例#19
0
    def execute(self, session):
        #region 第一步:规划每个物种中应有的个体数量
        # 取得物种集合,并按平均适应度排序
        species = session.pop.getSpecies()
        if collections.isEmpty(species):
            raise RuntimeError('NEAT选择操作失败:物种集合为空')
        species.sort(key=lambda s: s['fitness']['average'], reverse=True)

        # 根据物种的平均适应度在所有物种中占的比重,计算每个物种的目标个体数量
        specie_total_fitness = sum(
            list(map(lambda sp: sp['fitness']['average'], species)))

        totalSize = 0
        for i in range(len(species)):
            specie = species[i]
            # 根据物种适应度计算目标个体数量
            speicesFitness = specie['fitness']['average']
            specie.targetSize = int((speicesFitness / specie_total_fitness) *
                                    len(session.pop.inds))
            totalSize += specie.targetSize

        # 如果所有物种的目标个体数量之和仍小于种群个体数量,将不足的部分加到适应度最高的物种上(按照上面计算,不会出现大于的情况)
        if totalSize < len(session.pop.inds):
            species[0].targetSize += len(session.pop.inds) - totalSize
        totalSize = len(session.pop.inds)

        session.monitor.recordDebug(
            'neat_selection', '物种的目标个体数量',
            reduce(lambda x, y: x + "," + y,
                   map(lambda s: str(s.id) + "=" + str(s.targetSize),
                       species)))

        #endregion

        #region 第二步:遍历每个物种,如果物种中实际个体数量大于前面计算的每个物种的目标个体数量,则将适应度差的个体淘汰
        removeIndids = []
        for i in range(len(species)):
            specie = species[i]
            # 将物种中个体按照适应度由高到低排序,其中精英个体尽管排前面
            specie.indids.sort(
                key=lambda indid: session.pop[indid]['fitness'] + 0.000001
                if session.pop[indid] in session.pop.eliest else 0,
                reverse=True)
            # 实际个体数量不多于目标个体数量,不需要淘汰
            if len(specie.indids) <= specie.targetSize:
                continue

            # 删除适应度最小的个体,直到实际个体数量与目标个体数量相等(这样的删除方法,有可能会导致精英个体也被删除)
            while len(specie.indids) > specie.targetSize:
                removeIndid = specie.indids[-1]
                removeInd = session.pop[removeIndid]
                removeIndids.append(removeIndid)
                del specie.indids[-1]  # 从物种记录中删除
                session.pop.inds.remove(removeInd)  # 从种群记录中删除
        session.monitor.recordDebug(
            'neat_selection', '删除的个体',
            collections.mapreduce(
                removeIndids, reducefunc=lambda i, j: str(i) + ',' + str(j)))

        # 遍历所有物种,如果有物种个体数量为0,则将该物种删除
        species = [s for s in species if len(s.indids) > 0]
        #endregion

        #region 第三步:对每个物种,随机选择需要交叉操作的个体
        corssmeateInds = []
        for specie in species:
            if len(specie.indids) >= specie.targetSize:
                continue
            for i in range(specie.targetSize - len(specie.indids)):
                if len(specie.indids) == 1:
                    corssmeateInds.append((specie.indids[0], specie.indids[0]))
                elif len(specie.indids) == 2:
                    corssmeateInds.append((specie.indids[0], specie.indids[1]))
                else:
                    indexpair = random.sample(range(len(specie.indids)), 2)
                    corssmeateInds.append((specie.indids[indexpair[0]],
                                           specie.indids[indexpair[1]]))

        # 有错误:session.monitor.recordDebug('neat_selection', '交叉的个体',  reduce(lambda i, j: str(list(i)[0])+"-"+str(list(i)[1]) + ',' + str(list(j)[0])+"-"+str(list(j)[1]), corssmeateInds))
        # reduce(lambda i,j:str(i[0])+'-'+str(i[1])+','+str(j[0])+'-'+str(j[1]),[(0,1),(2,3)])  --->  0-1,2-3
        # reduce(lambda i,j:str(i[0])+'-'+str(i[1])+','+str(j[0])+'-'+str(j[1]),[(0,1),(2,3),(4,5)]) ---> 0--,4-5
        sdebug = ''
        for cross in corssmeateInds:
            if strs.isVaild(sdebug): sdebug += ','
            sdebug += str(cross[0]) + '-' + str(cross[1])
        session.monitor.recordDebug('neat_selection', '交叉的个体', sdebug)

        #region 第四步:对所有个体按照适应度从高到低排序,随机选择其中一部分作为变异个体
        metateinds = []
        # 计算变异个体数量
        mutateCount = int(session.runParam.mutate.propotion
                          ) if session.runParam.mutate.propotion >= 1 else int(
                              totalSize * session.runParam.mutate.propotion)
        if mutateCount <= 0:
            return True, '', (corssmeateInds, metateinds)

        # 对所有个体按照适应度从高到低排序
        session.pop.inds.sort(key=lambda x: x['fitness'] + 0.000001
                              if x in session.pop.eliest else 0,
                              reverse=True)
        # 选择候选变异个体(精英个体将被排除)
        candidateInds = collections.findall(
            session.pop.inds, lambda ind: ind not in session.pop.eliest)
        # 为每个个体计算一个选择概率(适应度越低的被选择的概率就高)
        if len(candidateInds) <= 0:
            max, avg, min, stdev = 0., 0., 0., 0.
            print('变异个体数量无效,' + str(session.pop.eliest))
            return True, '选择操作完成,其中淘汰个体数量=' + str(
                len(removeIndids)) + ',交叉个体数量=' + str(
                    len(corssmeateInds)) + ',变异个体数量=0', (corssmeateInds, [])

        else:
            max, avg, min, stdev = collections.rangefeature(
                list(map(lambda ind: ind['fitness'], candidateInds)))
        #fitnesssum = sum(list(map(lambda ind:ind['fitness'],candidateInds)))
        mutateSelProb = [
            1 - ((ind['fitness'] - min) / ((max - min) if max != min else 1))
            for index, ind in enumerate(candidateInds)
        ]
        mutateSelProb = np.array(mutateSelProb)
        p = mutateSelProb / mutateSelProb.sum()
        np.random.seed(0)
        #p = np.array(mutateSelProb)
        mutateinds = np.random.choice(candidateInds,
                                      size=mutateCount,
                                      p=p.ravel())
        session.monitor.recordDebug(
            'neat_selection', '变异的个体',
            reduce(lambda i, j: i + ',' + j,
                   map(lambda ind: str(ind.id), mutateinds)))

        return True, '选择操作完成,其中淘汰个体数量=' + str(
            len(removeIndids)) + ',交叉个体数量=' + str(
                len(corssmeateInds)) + ',变异个体数量=' + str(len(mutateinds)), (
                    corssmeateInds, list(map(lambda ind: ind.id, mutateinds)))
示例#20
0
def readText(filename, encode='utf-8', raiseError=False):
    lines = readLines(filename, None, encode, raiseError)
    if collections.isEmpty(lines): return ''
    return reduce(lambda x, y: x + '\n' + y, lines)
示例#21
0
    def putneuron(self,
                  neuron,
                  inids=None,
                  outinds=None,
                  synapseModelConfig=None):
        '''
        添加神经元,会检查神经元id,是否重复(重复会删除旧的)
        :param neuron:   Neuron 待添加神经元
        :param inids:    int or list 输入神经元id
        :param outinds:  int or list 输出神经元id
        :param synapseModelConfig: 突触计算模型
        :return:
        '''
        if neuron is None: return self

        # 检查神经元id
        if neuron.id <= 0:
            idGenerator = idGenerators.find(
                self.definition.get('idGenerator', 'default'))
            if idGenerator is None:
                raise RuntimeError(
                    "连接神经元失败(NeuralNetwork.connect(srcid,destid)):idGenerator无效"
                )
            neuron.id = idGenerator.getNeuronId(self, neuron.coord)

        # 检查神经元是否已经存在
        n = self.getNeuron(id=neuron.id)
        if n is not None:
            self.remove(n)

        # 第一次添加神经元
        layerindex = -1
        if len(self.neurons) <= 0:
            self.neurons.append([])
            layerindex = 0
        else:
            # 根据神经元所在层查找对应位置
            for i, ns in enumerate(self.neurons):
                if collections.isEmpty(ns):
                    del self.neurons[i]
                    i -= 1
                    continue
                if ns[0].layer == neuron.layer:
                    layerindex = i
                elif ns[0].layer > neuron.layer:
                    layerindex = i
                    self.neurons.insert(i, [])
                if layerindex >= 0: break
                continue

        # 添加神经元
        if layerindex < 0: self.neurons.append([neuron])
        else: self.neurons[layerindex].append(neuron)

        # 连接
        if inids is not None and synapseModelConfig is not None:
            self.connect(inids, neuron.id, neuron.birth, synapseModelConfig)
        if outinds is not None and synapseModelConfig is not None:
            self.connect(neuron.id, outinds, neuron.birth, synapseModelConfig)

        return self
示例#22
0
    def _compute_distance(self, ind1, ind2, session):
        # 距离计算,这段代码参考了https://github.com/CodeReclaimers/neat-python

        # 取得距离计算参数
        disjoint_coefficient = session.popParam.species.disjoint_coefficient
        weight_coefficient = session.popParam.species.weight_coefficient

        # 取得个体神经网络和所有神经元以及所有突触
        net1 = ind1.genome
        net2 = ind2.genome

        ns1 = net1.getNeurons()
        ns2 = net2.getNeurons()
        if collections.isEmpty(ns1): ns1 = []
        if collections.isEmpty(ns2): ns2 = []

        sps1 = net1.getSynapses()
        sps2 = net2.getSynapses()
        if collections.isEmpty(sps1): sps1 = []
        if collections.isEmpty(sps2): sps2 = []

        # 计算节点差异
        disjoint_nodes = 0
        node_distance = 0.0
        for n1 in ns1:
            if net2.getNeuron(n1.id) is None:
                disjoint_nodes += 1

        for n2 in ns2:
            n1 = net1.getNeuron(n2.id)
            if n1 is None:
                disjoint_nodes += 1
            else:
                d = abs(
                    n1.getVariableValue('bias', 0.0) -
                    n2.getVariableValue('bias', 0.0)) + abs(n1['value'] -
                                                            n2['value'])
                if n1['activation'] != n2['activation']: d += 1.0

                d = d * weight_coefficient
                node_distance += d

        max_nodes = max(len(ns1), len(ns2))
        node_distance = (node_distance +
                         disjoint_coefficient * disjoint_nodes) / max_nodes

        # 计算突触差异
        disjoint_connections = 0
        connection_distance = 0.0
        for s1 in sps1:
            if net2.getSynapse(id=s1.id) is None:
                disjoint_connections += 1
        for s2 in sps2:
            s1 = net1.getSynapse(id=s2.id)
            if s1 is None:
                disjoint_connections += 1
            else:
                d = abs(s1['weight'] - s2['weight'])
                d = d * weight_coefficient
                connection_distance += d

        max_conn = max(len(sps1), len(sps2))
        connection_distance = (connection_distance +
                               disjoint_coefficient * disjoint_connections
                               ) / max_conn if max_conn > 0 else 0.0

        # 计算总距离
        distance = node_distance + connection_distance
        return distance