depot_tools原理和实现

depot_tools原理和实现

depot_tools是Google的构建工具,所有Google系的项目构建都离不开depot_tools。
但是中文网络相关的文章基本都是简单的提一下用法,几乎没有讲解depot_tools原理实现的文章,所以这篇文章将讲解depot_tools的原理和实现。

此文章基于2022年6月的depot_tools代码。

此文章的基于Windows实现讲解,不过depot_tools是跨平台工具,基本大同小异。

部署

depot_tools本身是一个git仓库(https://chromium.googlesource.com/chromium/tools/depot_tools),其部署就是将仓库本身clone并将目录加入PATH环境变量即可。

其本身默认在每次运行时也会自动从仓库中拉取最新的代码来完成更新。

组件

depot_tools是一个工具集,其由几个主要组件组成,来完成构建过程中的代码拉取和构建。
工具主要由shell脚本和python脚本实现,在运行过程中会不断下载可执行组件。

depot_tools会使用git、python等可执行文件。但是第一次部署之后,depot_tools会使用bootstrap下载自己的git、python可执行文件,除非强制指定,否则不再使用系统安装的可执行文件。

  • bootstrap 拉取depot_tools的基础工具,如下载python、git。
  • fetch 初始化项目。
  • gclient 拉取所有项目依赖代码并执行项目hook。
  • cipd 用来下载大的二进制文件的工具。
  • vpython python虚拟环境,隔离不同版本的python环境,方便使用不同版本的python。
  • git 拉取代码的工具,depot_tools扩展了一系列git的脚本,以更加方便的使用git。

上文只列出了几个主要组件,还有很多辅助脚本由于篇幅原因不做讲解。

代理脚本

depot_tools的所有组件一般都会创建一个代理脚本,代理脚本一般是和组件同名的shell脚本,一般对组件的调用都会先调用代理脚本。然后代理脚本会对真正的组件进行更新或者初始化之类检查工作,最后默认会启动对应的组件并转发参数。

如果启动时不想更新,可以使用python update_depot_tools_toggle.py --disable关闭自动更新。python update_depot_tools_toggle.py --enable可以打开自动更新。

但是切记第一次使用的时候必须打开自动更新,因为要下载一些组件。

下文只介绍实际的组件,不再描述代理脚本的逻辑。

CIPD

CIPD全称Chrome Infrastructure Package Deployment(https://chromium.googlesource.com/infra/luci/luci-go/+/main/cipd/README.md),主要用于管理Google项目构建中用到的二进制文件(例如编译器之类的),你可以简单的认为是针对大文件的git系统。

CIPD管理的所有文件可以在(https://chrome-infra-packages.appspot.com)上查看。

bootstrap

每次运行depot_tools中的组件时,主要用于自动进行更新并拉取基础工具

首先会尝试下载或者更新cipd_client的可执行文件

然后启动bootstrap,bootstrap会读取bootstrap/manifest.txt,下载文件中指定的工具。
并创建对应的代理脚本。

vpython

vpython是Google的python虚拟化环境,主要的用于隔离不同版本的python环境,因为Google项目中使用了大量的python脚本,但是有些python脚本只能运行于特定版本的python中,所以需要同时运行不同版本的python。

git

depot_tools使用python脚本扩展了一系列git操作,来更方便的使用git。

fetch

fetch是用来初始化项目配置的工具。一般会在第一次拉取项目时用到。

fetch根据传入的参数拉取初始化项目配置。fetch所支持的项目在fetch_configs目录下,也是一系列python脚本,其中描述了项目的代码仓库和拉取方式以及默认目录。

当然你也可以自己扩展其他使用Google代码管理工具的项目配置。

目前所有的项目配置中拉取方式都是使用gclient_git,fetch初始化后会调用gclient在运行目录下生成一个.gclient文件,里面描述了项目的仓库地址等信息,之后gclient就依据此配置文件工作。

然后fetch使用默认参数调用gclient sync进行代码的拉取,当然fetch的也会把其他的命令行参数中透传给gclient。

在一些Google项目的文档、博客中你有时会看到不使用fetch,只使用gclient的方法。这就是在理解了fetch的原理后,自己完成第一步的生成gclient_git初始化过程,然后用gclient拉取代码。

gclient

gclient是进行代码拉取、工具部署、代码管理的关键组件。一旦拥有了.gclient配置文件,那么之后的工作就都可以通过gclient完成,不再需要fetch了。

因为gclient包含了很多大部分人不使用的功能,所以本文只重点讲解通常拉取代码、构建项目用到的sync命令。其他指令只简单介绍功能。

  • config 在运行目录里生成.gclient配置文件。

  • root 输出项目的根目录。

  • runhooks 执行项目配置里的hooks。当你执行sync时使用了–nohooks参数时,就可以使用该命令来手动执行hooks。

  • recurse 在项目中的每个依赖中都执行一条命令。

  • fetch 拉取所有模块中的更新。

  • diff 显示项目中每个依赖的本地修改。

  • status 显示项目中每个依赖的修改状态。

sync

此命令主要负责拉取项目代码、执行代码hook等操作,是gclient最重要的功能。

sync命令主要依赖两个文件工作,所以在这里先讲解这两个文件格式

  • .glicnet 此文件一般通过fetch自动生成,也可以手动生成,其格式就是python语法。其中记录一个solution数组,每个solution表示一个项目的基础配置。其中最重要的是三个属性:
    • name 项目名
    • url 项目仓库地址
    • deps_file 依赖配置文件
solutions = [
  {
    "name": "src",
    "url": "https://webrtc.googlesource.com/src.git",
    "deps_file": "DEPS",
    "managed": False,
    "custom_deps": {},
  },
]

  • DEPS 此文件就是上一节中的依赖配置文件,默认文件名称为DEP,也可以自定义。此文件存放在项目仓库的根目录下,其格式就是python语法。其中最重要的是4个属性

    • deps 这个属性是一个数组,包含了一系列引用项信息,引用项有两种,一种是git类型,表示一个git仓库,注意每个git类型引用项可能也会在自己的仓库根目录下含有自己的DEPS文件;一种是CIPD类型,表示一个CIPD仓库。引用项可能会含有condition属性,表示引用项生效的条件。比如有些引用项只在特性的操作系统上生效。
    • hooks 这个属性是一个包含了一系列可执行项目。可能会含有condition属性,表示执行项生效的条件,最重要的是action属性,其通常是一个命令行参数数组。表示要执行的一个命令。
    • pre_deps_hooks 此属性的性质和hooks一样,只不过执行的顺序有所不同。
    • vars 这个属性定义了一系列的属性。这些属性将用于在deps和hooks中替换对应的变量。
  1. sync命令先使用git根据.glicnet中solution的url将代码仓库以no-checkout的方式clone到一个临时目录,然后将仓库内容移动到以solution的name命名的目录中。这样下次拉取的时候只要检查目录就可以知道仓库是否拉取完成。

  2. 从仓库中检出最新的提交或者按照参数检出指定的提交。

  3. 解析deps_file属性指定的依赖配置文件。

  4. 如果配置文件中有pre_deps_hooks属性的话,先执行pre_deps_hooks中的所有生效的执行项目。

  5. 然后对deps属性中的引用项进行处理,下载生效的git引用项以及引用项仓库所依赖的引用项,。同时对过程中的CIPD类型的引用项做记录。在下载这些引用项时,gclient默认会使用多线程同时进行下载。下载方式和第1步类似,也是先下载到一个临时目录,再移动。

  6. 当完成了所git引用项的下载后,开始使用CIPD统一下载刚才记录的CIPD引用项。

  7. 当完成了所有引用项的下载后,会在根目录生成一个文件.gclient_entries,其中列举了实际完成下载的引用项。

  8. 最后开始执行hooks中所有的生效的执行项目。

这就是sync指令的整个执行流程。

下面介绍一些sync指令的参数

gclient使用optparse库来做参数解析,所以参数的输入方式可以参考optparse库的说明。

  • --nohooks 跳过执行hook阶段,hook阶段所执行的任务通常来说是和构建和编译有关。如果暂时不需要构建编译的话,可以跳过hook阶段。等需要的时候再使用runhooks命令手动执行hook。

  • -v 这个参数用来控制输出信息的详细程度。gclient使用了optparse命令行参数解析库中的次数参数来处理这个参数,也即通过重复输入这个参数来控制日志的等级。总共有四个等级,不输入参数的时候为默认的等级,如果需要最详细的日志,可以重复输入三个-v

  • --with_branch_heads 这个参数可以在拉取仓库的时候拉取分支的信息。chrome系列的代码仓库的分支使用了非git默认的分支引用命名,所以默认情况下clone是无法clone到分支信息的。当你有切换分支的需求时,你应该使用这个参数。但是它会导致你的代码仓库占用更多的磁盘空间。注意,这个参数应该在第一次使用sync指令时就使用。如果已经完成了代码的拉取,再次使用这个参数执行sync命令是没有用的。

  • --with_tags 作用类似--with_branch_heads。只不过是针对仓库的tag。

  • -r 指定仓库的版本,参数值形如name@version,name可以是solution的name或者引用项的名称。version可以是仓库的branch名或者tag名或者hash。此参数可以重复多次指定多个仓库的版本。

  • -j 一个整数参数,控制同时多少个线程来执行依赖,因为大部分引用项都是git拉取任务,耗时主要在IO层,多线程可以提高拉取的速度,但是会导致日志输出比较凌乱。

如何拉取指定版本

拉取指定版本是构建中的一个非常常用需求,但是大多数项目的构建文档中都没有明确给出步骤,给出的都是默认拉取最新版本。这里给出明确的方法,如果需要拉取指定项目的指定版本,有三种方法:

  • 使用-r参数执行sync命令,指定.gclient中solution的引用版本为要使用的版本。
  • 先使用--nohooks --with_tags --with_branch_heads拉取最新版本参数执行sync命令,然后手动checkout到指定的版本后,重新执行无参数的sync命令。
  • 修改.gclient中solution的url,在仓库地址后加上@version,version可以是仓库的branch名或者tag名或者hash。
如何管理自己的修改

当我们有时候会对某些代码做一些修改以适应自身的需求,但是通常来说chrome的项目都是由一大堆独立的仓库通过依赖关系组织在一起的。由于chrome的仓库平台并不提供个人仓库,所以只能将自己的修改提交到个人仓库。那么该如何组织自己的仓库呢?

根据上文对gclient工作原理的描述可以知道,gclient是根据DEPS中的依赖项配置来下载模块代码的。那么我们只要把需要修改的模块修改后提交到自己的仓库中。然后将DEPS中的依赖项更新为自己的仓库地址和提交号。然后将包含DEPS的主仓库也提交到自己的仓库中。

之后只要将.gclient中主仓库地址改为自己的主仓库地址后,gclient就会按照DEPS自动拉取我们修改过的依赖仓库了。

需要注意的是,git默认的所有分支引用是放在heads下面的,google为了避免拉取已经不在使用的提交,将分支的引用全部放在了branch-heads下面。这样导致一般的git指令无法拉取到分支信息。必须使用gclient的时候传入--with_branch_heads参数或者手动修改仓库的fetch引用规范(具体可以参考gclient源码中对with_branch_heads属性的处理)。

from ortools.constraint_solver import routing_enums_pb2 from ortools.constraint_solver import pywrapcp import numpy as np import datetime import math # ================== 数据结构定义 ================== class EnhancedVehicle: def __init__(self, vid, capacity, is_own, start_node, partitions): self.vid = vid # 车辆ID self.capacity = capacity # 车辆容量 self.is_own = is_own # 是否自有车 self.start_node = start_node # 出发节点 self.partitions = partitions # 允许访问分区 class EnhancedCustomer: def __init__(self, demand, time_window, partition, is_overdue): self.demand = demand # 需求重量 self.time_window = time_window # 时间窗(分钟) self.partition = partition # 所属分区 self.is_overdue = is_overdue # 是否超时订单 # ================== 数据生成器 ================== def create_enhanced_data_model(orders, vehicles, distance_matrix, time_matrix, fixed_cross_time, current_time, convertDistanceTime): """生成增强的测试数据集""" data = {} np.random.seed(42) # 基本参数 data['depot'] = 0 # 分拣中心节点 # current_time = datetime.datetime.now() data['base_time'] = current_time data['cross_time'] = fixed_cross_time # 允许跨区时间(分钟) # 生成200个客户订单(包含20%超时订单) data['customers'] = orders data['lenCustomers'] = len(data['customers']) for i in range(len(data['customers'])): if data['customers'][i].isOutTime: data['customers'][i].startTW = 0 data['customers'][i].endTW = 3600 * 48 # start = np.random.randint(0, 480) # end = start + np.random.randint(60, 240) # is_overdue = np.random.rand() < 0.2 # 20%超时 # data['customers'].append( # EnhancedCustomer( # demand=np.random.randint(1, 5), # time_window=(start, end) if not is_overdue else (0, 1440), # partition=np.random.randint(0, 10), # is_overdue=is_overdue # ) # ) # 生成15辆车(自有车10辆,外请车5辆) data['vehicles'] = vehicles data['lenVehicles'] = len(data['vehicles']) for vid in range(len(data['vehicles'])): data['vehicles'][vid].start_node = vid + 1 # 每辆车有独立出发位置 # data['vehicles'].append( # EnhancedVehicle( # vid=vid, # capacity=np.random.randint(20, 50) if vid < 10 else np.random.randint(15, 40), # is_own=(vid < 10), # start_node=start_node, # partitions=[vid % 10] + ([] if vid < 10 else [(vid % 10 + 1) % 10]) # ) # ) # 构建距离矩阵(包含分拣中心+车辆出发位置+客户位置) # node_count = 1 + len(data['vehicles']) + len(data['customers']) # 分拣中心(0) + 车辆起点(1-15) + 客户(16-215) data['distance_matrix'] = distance_matrix #np.random.randint(5, 100, (node_count, node_count)) data['time_matrix'] = time_matrix # np.fill_diagonal(data['distance_matrix'], 0) data['convertDistanceTime'] = convertDistanceTime data['customer_start_idx'] = 1 + len(data['vehicles']) #更改字段名称 for i in range(len(data['vehicles'])): data['vehicles'][i].vid = i for item in data['vehicles']: item.capacity = item.residualVolumeMax all_allowed_zones = [] for item in data['vehicles']: all_allowed_zones += item.areaIds all_allowed_zones = list(set(all_allowed_zones)) for item in data['vehicles']: # item.allowed_zones = item.areaIds if item.areaIds == []: item.allowed_zones = all_allowed_zones else: item.allowed_zones = item.areaIds item.allowed_zones = [int(item1) for item1 in item.allowed_zones] for item in data['customers']: item.zone = int(item.areaId) return data # ================== 核心约束实现 ================== def add_partition_constraints(routing, manager, data, time_dimension, fixed_cross_time): """实现动态分区访问控制""" solver = routing.solver() # 1. 创建分区状态维度 def zone_callback(from_index): from_node = manager.IndexToNode(from_index) # if from_node == data['depot']: # return -2 # 分拣中心特殊标记 if from_node < data['customer_start_idx']: return -1 # 车辆起点特殊标记 customer_idx = from_node - data['customer_start_idx'] return data['customers'][customer_idx].zone zone_callback_idx = routing.RegisterUnaryTransitCallback(zone_callback) routing.AddDimension( zone_callback_idx, 0, # slack_max 1000, # 分区容量上限 False, # start_cumul_to_zero 'Zone' ) zone_dim = routing.GetDimensionOrDie('Zone') # 2. 为每辆车添加动态分区约束 for vehicle in data['vehicles']: vehicle_id = vehicle.vid allowed_zones = set(vehicle.allowed_zones) cross_time = int(fixed_cross_time) #vehicle.cross_time # 动态分区检查回调 def zone_check_callback(from_index, to_index): nonlocal allowed_zones, cross_time to_node = manager.IndexToNode(to_index) # 允许返回分拣中心 if to_node == data['depot']: return True # 非客户节点直接允许 if to_node < data['customer_start_idx']: return True # 获取目标分区 zone = zone_callback(to_index) # 获取到达时间 arrival_time_var = time_dimension.CumulVar(to_index) min_arrival = int(solver.Min(arrival_time_var)) # 判断是否达到切换阈值 if min_arrival < cross_time: # 检查当前是否已有访问分区 start_index = routing.Start(vehicle_id) first_node_index = routing.NextVar(start_index) # 获取首个分区变量 first_zone_var = zone_dim.CumulVar(first_node_index) # 添加约束:时间阈值前必须与首个分区一致 solver.Add( solver.IfThen( zone_dim.CumulVar(to_index) != -1, zone_dim.CumulVar(to_index) == first_zone_var ) ) # 添加约束:首个分区必须在允许分区内 solver.Add( solver.IfThen( first_zone_var != -1, solver.Member(first_zone_var, list(allowed_zones)) ) ) else: # 添加约束:时间阈值后必须在允许分区内 solver.Add( solver.Member(zone_dim.CumulVar(to_index), list(allowed_zones)) ) # # # 添加约束:时间阈值前必须与首个分区一致 # # if solver.Value(zone_dim.CumulVar(to_index)) != -1: # # solver.Add(zone_dim.CumulVar(to_index) == first_zone_var) # # # # # 添加约束:首个分区必须在允许分区内 # # if solver.Value(first_zone_var) != -1: # # solver.Add(solver.Member(first_zone_var, list(allowed_zones))) # # else: # # # 添加约束:时间阈值后必须在允许分区内 # # if solver.Value(zone_dim.CumulVar(to_index)) != -1: # # solver.Add(solver.Member(zone_dim.CumulVar(to_index), list(allowed_zones))) # # return True # 注册转移回调 zone_check_idx = routing.RegisterTransitCallback(zone_check_callback) # # 3. 添加软约束确保路径可行性 # penalty = 1000000 # 大惩罚值 # routing.AddDisjunction( # [manager.NodeToIndex(i) for i in range(manager.GetNumberOfNodes())], # penalty # ) return routing # for vehicle in data['vehicles']: # vehicle_id = vehicle.vid # allowed_zones = vehicle.allowed_zones # cross_time = fixed_cross_time #vehicle.cross_time # # def zone_check(from_index, to_index): # from_node = manager.IndexToNode(from_index) # to_node = manager.IndexToNode(to_index) # # # 允许返回分拣中心 # if to_node == data['depot']: # return True # # # 客户节点处理 # if to_node >= data['customer_start_idx']: # customer = data['customers'][to_node - data['customer_start_idx']] # to_zone = customer.zone # else: # return True # 车辆起始点 # # # 获取当前时间 # time_var = time_dimension.CumulVar(to_index) # current_time = routing.solver().Min(time_var) # # # 判断时间阶段 # if current_time < cross_time: # # 检查路径中已访问的分区 # path_zones = set() # index = routing.Start(vehicle_id) # while not routing.IsEnd(index): # node = manager.IndexToNode(index) # if node >= data['customer_start_idx']: # zone = data['customers'][node - data['customer_start_idx']].zone # path_zones.add(zone) # index = routing.NextVar(index) #solution.Value(routing.NextVar(index)) # # # 允许访问的情况 # return (len(path_zones) == 0 and to_zone in allowed_zones) or \ # (len(path_zones) > 0 and to_zone in path_zones) # else: # return to_zone in allowed_zones # # next_var = routing.NextVar(routing.Start(vehicle_id)) # # 添加约束到车辆 # routing.solver().AddConstraint( # routing.solver().CheckAssignment(next_var, zone_check)) # ================== 主求解逻辑 ================== def optimized_vrp_solver(orders, vehicles, distance_matrix, time_matrix, fixed_cross_time, current_time, convertDistanceTime): data = create_enhanced_data_model(orders, vehicles, distance_matrix, time_matrix, fixed_cross_time, current_time, convertDistanceTime) # a = len(data['distance_matrix']) # data = {} # data['distance_matrix'] = [[0,1,2,3,4,5,6,7,8,9,10], # [1,0,2,3,4,5,6,7,8,9,10], # [2,1,0,3,4,5,6,7,8,9,10], # [3,1,2,0,4,5,6,7,8,9,10], # [4,1,2,3,0,5,6,7,8,9,10], # [5,1,2,3,4,0,6,7,8,9,10], # [6,1,2,3,4,5,0,7,8,9,10], # [7,1,2,3,4,5,6,0,8,9,10], # [8,1,2,3,4,5,6,7,0,9,10], # [9,1,2,3,4,5,6,7,8,0,10], # [10,1,2,3,4,5,6,7,8,9,0], # ] # data['demands'] = [0] * 6 + [item.demand for item in data['customers']] # data["vehicle_capacities"] = [item.capacity for item in data['vehicles']] # 3辆车的容量 # data["demands"] = [0, 0, 0, 0, 0, 0, 2.7, 2, 2, 1, 3] # 分拣中心起点需求为0 # data["vehicle_capacities"] = [5, 7, 10, 6, 1] # 3辆车的容量 # print(data["demands"]) # print(data["vehicle_capacities"]) # 数据准备阶段 SCALE_FACTOR = 1000 # 转换为克/毫升 # data['demands'] = [int(d * SCALE_FACTOR) for d in data['demands']] # data['demands'] = [int(d * SCALE_FACTOR) for d in data['demands']] # data['customers'] = [int(item.demand * SCALE_FACTOR) for item in data['customers']] for item in data['customers']: item.demand = int(item.demand * SCALE_FACTOR) for item in data['vehicles']: item.capacity = int(item.capacity * SCALE_FACTOR) # data['vehicles'][0].capacity = 5000 # data['vehicles'][1].capacity = 2500 # data['vehicles'][2].capacity = 3500 # data['vehicles'][3].capacity = 15000 # data['vehicles'][4].capacity = 10000 # data["vehicle_capacities"] = [int(c * SCALE_FACTOR) for c in data["vehicle_capacities"]] # # 添加安全边界 # SAFETY_FACTOR = 0.999 # 99.9%容量利用率 # data["vehicle_capacities"] = [cap * SAFETY_FACTOR for cap in data["vehicle_capacities"]] print([item.demand for item in data['customers']]) print([item.capacity for item in data['vehicles']]) # print(data["demands"]) # print(data["vehicle_capacities"]) data["num_vehicles"] = len(data['vehicles']) # 每辆车的起点(索引1,2,3),终点都是分拣中心(索引0) # data["starts"] = [1, 2, 3, 4, 5] # data["ends"] = [0, 0, 0,0,0] # # 距离矩阵(包含分拣中心所有起点) # data["distance_matrix"] = [ # [0, 5, 8, 6, 7, 1, 2, 10], # 分拣中心(索引0) # [5, 0, 6, 3, 2, 1, 2, 9], # 起点1 # [8, 6, 0, 8, 4, 1, 2, 8], # 起点2 # [6, 3, 8, 0, 5, 1, 2, 7], # 起点3 # [7, 2, 4, 5, 0, 1, 2, 3], # 客户点 # [7, 2, 4, 5, 2, 0, 2, 6], # 客户点 # [7, 2, 4, 5, 6, 1, 0, 5], # 客户点 # [7, 2, 4, 5, 6, 1, 2, 0], # 客户点 # ] # data["demands"] = [0, 0, 0, 0, 3,4,5,6] # 分拣中心起点需求为0 # data["vehicle_capacities"] = [25, 20, 10] # 3辆车的容量 # data["num_vehicles"] = 3 # # 每辆车的起点(索引1,2,3),终点都是分拣中心(索引0) # data["starts"] = [1, 2, 3] # data["ends"] = [0, 0, 0] manager = pywrapcp.RoutingIndexManager( len(data['distance_matrix']), data["num_vehicles"], [v.start_node for v in data['vehicles']], # 各车起始位置 data["starts"], # [data['depot']] * len(data['vehicles']) #data["ends"] # ) # 统一返回分拣中心 routing = pywrapcp.RoutingModel(manager) # ========== 注册核心回调函数 ========== def distance_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) return data["distance_matrix"][from_node][to_node] transit_callback_idx = routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_idx) # ========== 时间维度约束 ========== service_time = 10 # 每个客户服务时间10分钟 def time_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) travel_time = data['time_matrix'][from_node][to_node] # travel_time = distance_callback(from_index, to_index) * data['convertDistanceTime'] * 60 # 假设速度1单位/分钟 travel_time = travel_time + (data['customers'][from_node - data['customer_start_idx']].servTime if from_node >= data[ 'customer_start_idx'] else 0) return int(travel_time) time_callback_idx = routing.RegisterTransitCallback(time_callback) routing.AddDimension( time_callback_idx, slack_max=0, # 不允许等待 capacity=24 * 3600, # 最大时间限制(24小时)24 * 3600, # 车辆最大工作时间 fix_start_cumul_to_zero=False,# 起始时间为当前时间 name='Time' ) time_dimension = routing.GetDimensionOrDie('Time') # 设置车辆起始时间(当前系统时间) for vehicle in data['vehicles']: vehicle_id = vehicle.vid start_index = routing.Start(vehicle_id) time_dimension.CumulVar(start_index).SetMin(current_time) time_dimension.CumulVar(start_index).SetMax(current_time) # 设置客户时间窗 for customer_idx in range(len(data['customers'])): index = manager.NodeToIndex(customer_idx + data['customer_start_idx']) # 客户节点从16开始 print(index) if not data['customers'][customer_idx].isOutTime: time_dimension.CumulVar(index).SetRange( data['customers'][customer_idx].startTW, data['customers'][customer_idx].endTW ) # ========== 容量约束 ========== def demand_callback(from_index): node = manager.IndexToNode(from_index) if node < data['customer_start_idx']: return 0 # 非客户节点无需求 return data['customers'][node - data['customer_start_idx']].demand # def demand_callback(from_index): # from_node = manager.IndexToNode(from_index) # # print('from_index', from_index) # # print('node',node) # return data['demands'][from_node] demand_callback_idx = routing.RegisterUnaryTransitCallback(demand_callback) routing.AddDimensionWithVehicleCapacity( demand_callback_idx, 0, # null capacity slack [v.capacity for v in data['vehicles']], #data["vehicle_capacities"], # True, 'Capacity' ) capacity_dimension = routing.GetDimensionOrDie('Capacity') # capacity_dimension.SetCumulVarSoftUpperBoundToZero() # 严格约束 # ========== 高级约束 ========== #1. 分区动态约束 routing = add_partition_constraints(routing, manager, data, time_dimension, fixed_cross_time) # 2. 自有车优先(设置不同固定成本) for vid in range(len(data['vehicles'])): routing.SetFixedCostOfVehicle(100 if data['vehicles'][vid].isHaveTask == 0 else 10000, vid) # # 3. 超时订单优先(设置不同惩罚值) # penalty = 1000000 # 未服务的惩罚 # for customer_idx in range(len(data['customers'])): # penalty = 1000000 if data['customers'][customer_idx].isOutTime else 1000 # routing.AddDisjunction( # [manager.NodeToIndex(customer_idx + data['customer_start_idx'])], # penalty # ) # ========== 求解参数配置 ========== search_params = pywrapcp.DefaultRoutingSearchParameters() search_params.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC ) search_params.local_search_metaheuristic = ( routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH ) search_params.time_limit.seconds = 60 # 60秒求解时间 # search_params.log_search = True # ========== 执行求解 ========== solution = routing.SolveWithParameters(search_params) # ========== 结果解析 ========== if solution: """打印解决方案""" total_distance = 0 for vehicle_id in range(data["num_vehicles"]): print('vehicle_id', vehicle_id) index = routing.Start(vehicle_id) node_index = manager.IndexToNode(index) print('node_index', node_index) plan_output = f"车辆 {vehicle_id} 路线:\n" route_distance = 0 route_load = 0 while not routing.IsEnd(index): node_index = manager.IndexToNode(index) if node_index == 0: node_type = "中心" elif node_index >= data['customer_start_idx']: node_type = f"客户{node_index - data['customer_start_idx'] + 1}" else: node_type = f"车辆{node_index}起始点" # 获取时间信息 time_var = time_dimension.CumulVar(index) arrival_time = solution.Min(time_var) if node_index < data['customer_start_idx']: departure_time = arrival_time else: departure_time = arrival_time + data['customers'][node_index-data['customer_start_idx']].servTime # 格式化为可读时间 arr_time_str = arrival_time #min_to_time(arrival_time).strftime("%H:%M") dep_time_str = departure_time #min_to_time(departure_time).strftime("%H:%M") # 添加到输出 plan_output += ( f"节点 {node_index}({node_type}) | " f"到达: {arrival_time}s ({arr_time_str}) | " f"离开: {departure_time}s ({dep_time_str})\n" ) next_node_index = manager.IndexToNode(solution.Value(routing.NextVar(index))) if node_index >= data['customer_start_idx']: route_load += data['customers'][node_index-data['customer_start_idx']].demand #data["demands"][node_index] plan_output += f" {node_index} ->" previous_index = index index = solution.Value(routing.NextVar(index)) route_distance += routing.GetArcCostForVehicle( previous_index, index, vehicle_id ) # 输出终点(仓库) node_index = manager.IndexToNode(index) time_var = time_dimension.CumulVar(index) arrival_time = solution.Min(time_var) arr_time_str = arrival_time #min_to_time(arrival_time).strftime("%H:%M") plan_output += ( f"节点 {node_index}(中心) | " f"到达: {arrival_time}s ({arr_time_str})\n" ) plan_output += f" {manager.IndexToNode(index)}\n" plan_output += f"行驶距离: {route_distance}\t载重量: {route_load}\n" print(plan_output) total_distance += route_distance print(f"总行驶距离: {total_distance}") else: print("No solution found!") # if __name__ == '__main__': # optimized_vrp_solver(orders, vehicles, distance_matrix, time_matrix, fixed_cross_time, current_time) # optimized_vrp_solver()
06-06
def add_partition_constraints(routing, manager, data, time_dimension, fixed_cross_time): """实现动态分区访问控制""" solver = routing.solver() # 1. 创建分区状态维度 def zone_callback(from_index): from_node = manager.IndexToNode(from_index) # if from_node == data['depot']: # return -2 # 分拣中心特殊标记 if from_node < data['customer_start_idx']: return -1 # 车辆起点特殊标记 customer_idx = from_node - data['customer_start_idx'] return data['customers'][customer_idx].zone zone_callback_idx = routing.RegisterUnaryTransitCallback(zone_callback) routing.AddDimension( zone_callback_idx, 0, # slack_max 1000, # 分区容量上限 False, # start_cumul_to_zero 'Zone' ) zone_dim = routing.GetDimensionOrDie('Zone') # 2. 为每辆车添加动态分区约束 for vehicle in data['vehicles']: vehicle_id = vehicle.vid allowed_zones = set(vehicle.allowed_zones) cross_time = fixed_cross_time #vehicle.cross_time # 动态分区检查回调 def zone_check_callback(from_index, to_index): nonlocal allowed_zones, cross_time to_node = manager.IndexToNode(to_index) # 允许返回分拣中心 if to_node == data['depot']: return True # 非客户节点直接允许 if to_node < data['customer_start_idx']: return True # 获取目标分区 zone = zone_callback(to_index) # 获取到达时间 arrival_time_var = time_dimension.CumulVar(to_index) min_arrival = solver.Min(arrival_time_var) # 判断是否达到切换阈值 if min_arrival < cross_time: # 检查当前是否已有访问分区 start_index = routing.Start(vehicle_id) first_node_index = routing.NextVar(start_index) # 获取首个分区变量 first_zone_var = zone_dim.CumulVar(first_node_index) # 添加约束:时间阈值前必须与首个分区一致 solver.Add( solver.IfThen( zone_dim.CumulVar(to_index) != -1, zone_dim.CumulVar(to_index) == first_zone_var ) ) # 添加约束:首个分区必须在允许分区内 solver.Add( solver.IfThen( first_zone_var != -1, solver.Member(first_zone_var, list(allowed_zones)) ) ) else: # 添加约束:时间阈值后必须在允许分区内 solver.Add( solver.Member(zone_dim.CumulVar(to_index), list(allowed_zones)) ) return True # 注册转移回调 zone_check_idx = routing.RegisterTransitCallback(zone_check_callback) # 3. 添加软约束确保路径可行性 penalty = 1000000 # 大惩罚值 routing.AddDisjunction( [manager.NodeToIndex(i) for i in range(manager.GetNumberOfNodes())], penalty ) 报错WARNING: All log messages before absl::InitializeLog() is called are written to STDERR F0000 00:00:1749109138.770297 395312 routing.cc:1859] Check failed: kUnassigned != indices[i] (-1 vs. -1)
06-06
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值