feat: 初始化Easy Patch插件及依赖文件
- 添加Blender插件核心文件:__init__.py、ui.py、property.py、preference.py - 添加插件工具模块:g.py、loop.py、generate_loop.py、const.py、op.py - 添加翻译工具:utils/trans.py - 添加PuLP线性规划库及其依赖文件,包括CBC求解器二进制文件 - 添加.gitignore和VSCode配置文件
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"python-envs.defaultEnvManager": "ms-python.python:conda",
|
||||||
|
"python-envs.defaultPackageManager": "ms-python.python:conda",
|
||||||
|
"python-envs.pythonProjects": []
|
||||||
|
}
|
||||||
45
__init__.py
Normal file
45
__init__.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
bl_info = {
|
||||||
|
"name": "Easy Patch",
|
||||||
|
"author": "oimoyu",
|
||||||
|
"version": (2, 1, 0),
|
||||||
|
"blender": (3, 2, 0),
|
||||||
|
"location": "View3D > Sidebar > Easy Patch",
|
||||||
|
"description": "Easy Patch",
|
||||||
|
"category": "Object",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from .ui import classes as ui_classes
|
||||||
|
from .op import classes as op_classes
|
||||||
|
from .preference import classes as preference_classes
|
||||||
|
from .property import property_classes, EasyPatchProperty
|
||||||
|
from .utils.trans import t
|
||||||
|
from . import const
|
||||||
|
|
||||||
|
|
||||||
|
classes = ui_classes + op_classes + property_classes + preference_classes
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
# # delete when release
|
||||||
|
bpy.app.debug = False
|
||||||
|
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.Scene.easy_patch_properties = bpy.props.PointerProperty(
|
||||||
|
type=EasyPatchProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
# for i in bpy.context.preferences.addons:
|
||||||
|
# print(i)
|
||||||
|
|
||||||
|
# print(bpy.context.preferences.addons["blender-easy-patch-2"].preferences)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
||||||
|
|
||||||
|
del bpy.types.Scene.easy_patch_properties
|
||||||
535
boundary.py
Normal file
535
boundary.py
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import mathutils
|
||||||
|
import bmesh
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from . import const
|
||||||
|
from .utils import functions
|
||||||
|
from . import g
|
||||||
|
|
||||||
|
|
||||||
|
class Boundary:
|
||||||
|
def __init__(self, start_vertex, end_vertex, path):
|
||||||
|
self.index = -1
|
||||||
|
self.verts = []
|
||||||
|
self.path = path
|
||||||
|
self.start_vertex = start_vertex
|
||||||
|
self.end_vertex = end_vertex
|
||||||
|
|
||||||
|
self.temp_length = 3 # for uncreated boundary
|
||||||
|
|
||||||
|
self.mid_co = mathutils.Vector((0, 0, 0))
|
||||||
|
self.solid_path = []
|
||||||
|
self.is_editable = True
|
||||||
|
self.is_hover = False
|
||||||
|
|
||||||
|
self.is_solid_start = (
|
||||||
|
not not start_vertex
|
||||||
|
and start_vertex not in self.get_boundaries_verts_except_self()
|
||||||
|
)
|
||||||
|
self.is_solid_end = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_temp(cls, start_vertex):
|
||||||
|
instance = cls(
|
||||||
|
start_vertex,
|
||||||
|
None,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
if self.edges:
|
||||||
|
return len(self.edges)
|
||||||
|
else:
|
||||||
|
return self.temp_length
|
||||||
|
|
||||||
|
@length.setter
|
||||||
|
def length(self, length):
|
||||||
|
# 固化边
|
||||||
|
if not self.is_editable:
|
||||||
|
return
|
||||||
|
if length < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: 下面两个暂不用考虑,两个boundary为同一个loop并且boundary都为1了,则冲突了
|
||||||
|
|
||||||
|
# 建成和非建成改的长度不一样
|
||||||
|
if self.edges:
|
||||||
|
# TODO:特殊情况,两个boundary且都为1的时候冲突了
|
||||||
|
# if (
|
||||||
|
# length == 1
|
||||||
|
# and len(self.op_instance.patch.boundaries) == 2
|
||||||
|
# and any(
|
||||||
|
# [
|
||||||
|
# boundary.length == 1
|
||||||
|
# for boundary in self.op_instance.patch.boundaries
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
# ):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# 特殊情况,已建成的边,如果头尾都吸附到了某个edge的两个顶点上(并且不是吸附路径),如果这个时候改变分段为1,则会报错创建已存在的边
|
||||||
|
if length == 1:
|
||||||
|
start_vertex_related_edges = self.start_vertex.link_edges
|
||||||
|
for start_vertex_related_edge in start_vertex_related_edges:
|
||||||
|
if self.end_vertex in start_vertex_related_edge.verts:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.change_length(length)
|
||||||
|
else:
|
||||||
|
# # TODO:特殊情况,两个边且都为1的时候
|
||||||
|
# if (
|
||||||
|
# length == 1
|
||||||
|
# and len(self.op_instance.patch.boundaries) == 1
|
||||||
|
# and self.op_instance.patch.boundaries[0].length == 1
|
||||||
|
# ):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# 特殊情况,未建成的边,如果头尾都吸附到了某个edge的两个顶点上(并且不是吸附路径),如果这个时候改变分段为1,则会报错创建已存在的边,强制约束不能小于1
|
||||||
|
if length <= 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.temp_length = length
|
||||||
|
|
||||||
|
def get_actual_verts_co_and_path(self):
|
||||||
|
# 第一个是考虑了temp_path,第二个是真实的path
|
||||||
|
if self.solid_path:
|
||||||
|
path = self.solid_path
|
||||||
|
vertex_co_along_path = path
|
||||||
|
else:
|
||||||
|
path = self.path
|
||||||
|
vertex_co_along_path, _ = self.vertices_co_along_path()
|
||||||
|
return vertex_co_along_path, path
|
||||||
|
|
||||||
|
def change_length(self, length):
|
||||||
|
self.remove_mesh()
|
||||||
|
self.create_mesh(length)
|
||||||
|
|
||||||
|
# self.op_instance.patch.update_after_boundary_change()
|
||||||
|
functions.on_change_boundary_length(self)
|
||||||
|
|
||||||
|
def append_path_from_2d(
|
||||||
|
self,
|
||||||
|
context,
|
||||||
|
co_2d,
|
||||||
|
is_snap_surface=True,
|
||||||
|
is_snap_opposite=False,
|
||||||
|
):
|
||||||
|
# 獲取base_co
|
||||||
|
if len(self.path) != 0:
|
||||||
|
base_co = self.path[-1]
|
||||||
|
else:
|
||||||
|
if self.start_vertex:
|
||||||
|
# vertex.co获取的是局部坐标
|
||||||
|
start_vertex = g.obj.matrix_world @ self.start_vertex.co
|
||||||
|
base_co = start_vertex
|
||||||
|
self.path.append(start_vertex)
|
||||||
|
else:
|
||||||
|
base_co = (0, 0, 0)
|
||||||
|
|
||||||
|
region = context.region
|
||||||
|
rv3d = context.region_data
|
||||||
|
|
||||||
|
# 阈值
|
||||||
|
if self.path:
|
||||||
|
path_last_co_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, self.path[-1]
|
||||||
|
) # 视图外会返回None
|
||||||
|
if not path_last_co_2d:
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
path_last_co_2d - mathutils.Vector(co_2d)
|
||||||
|
).length < const.draw_path_threshold:
|
||||||
|
# print("太小不记录")
|
||||||
|
return
|
||||||
|
|
||||||
|
co_3d = []
|
||||||
|
last_co = mathutils.Vector(base_co)
|
||||||
|
|
||||||
|
# Get the ray origin and direction
|
||||||
|
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, co_2d)
|
||||||
|
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, co_2d)
|
||||||
|
|
||||||
|
hit = None
|
||||||
|
if is_snap_surface:
|
||||||
|
snap_bvh_trees = g.snap_bvh_trees
|
||||||
|
for snap_bvh_tree in snap_bvh_trees:
|
||||||
|
location, normal, index, distance = snap_bvh_tree.ray_cast(
|
||||||
|
ray_origin, ray_direction
|
||||||
|
)
|
||||||
|
if location:
|
||||||
|
hit = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if hit:
|
||||||
|
if (
|
||||||
|
is_snap_opposite or normal.dot(ray_direction.normalized()) < 0
|
||||||
|
): # 点乘大于0,反向
|
||||||
|
co_3d = location
|
||||||
|
|
||||||
|
if not co_3d:
|
||||||
|
# Project base point
|
||||||
|
plane_center = last_co
|
||||||
|
plane_normal = ray_direction
|
||||||
|
|
||||||
|
intersection_point = mathutils.geometry.intersect_line_plane(
|
||||||
|
ray_origin, ray_origin + ray_direction, plane_center, plane_normal
|
||||||
|
)
|
||||||
|
if intersection_point:
|
||||||
|
co_3d = intersection_point
|
||||||
|
|
||||||
|
self.path.append(co_3d)
|
||||||
|
return
|
||||||
|
|
||||||
|
def create_mesh(self, length):
|
||||||
|
if self.solid_path:
|
||||||
|
self.is_editable = False
|
||||||
|
# self.edges 和self.verts 已在update_solid中定义,并且不用创建实体
|
||||||
|
|
||||||
|
else:
|
||||||
|
vertices_co_along_path_3d, self.verts_correspond_path_idxs = (
|
||||||
|
self.vertices_co_along_path(length)
|
||||||
|
)
|
||||||
|
|
||||||
|
# create vert
|
||||||
|
vertices_along_path = []
|
||||||
|
for i in range(len(vertices_co_along_path_3d)):
|
||||||
|
coord = vertices_co_along_path_3d[i]
|
||||||
|
if i == 0:
|
||||||
|
# 开头
|
||||||
|
if self.start_vertex:
|
||||||
|
vertices_along_path.append(self.start_vertex)
|
||||||
|
else:
|
||||||
|
vertex = functions.create_temp_vertex(coord)
|
||||||
|
vertices_along_path.append(vertex)
|
||||||
|
self.start_vertex = vertex
|
||||||
|
|
||||||
|
elif i == len(vertices_co_along_path_3d) - 1:
|
||||||
|
# 中间
|
||||||
|
if self.end_vertex:
|
||||||
|
vertices_along_path.append(self.end_vertex)
|
||||||
|
else:
|
||||||
|
vertex = functions.create_temp_vertex(coord)
|
||||||
|
vertices_along_path.append(vertex)
|
||||||
|
self.end_vertex = vertex
|
||||||
|
else:
|
||||||
|
# 结尾
|
||||||
|
vertex = functions.create_temp_vertex(coord)
|
||||||
|
vertices_along_path.append(vertex)
|
||||||
|
|
||||||
|
# create edge
|
||||||
|
boundary_edges = []
|
||||||
|
for i in range(len(vertices_along_path) - 1):
|
||||||
|
edge = functions.create_temp_edge(
|
||||||
|
vertices_along_path[i], vertices_along_path[i + 1]
|
||||||
|
)
|
||||||
|
boundary_edges.append(edge)
|
||||||
|
bmesh.update_edit_mesh(g.obj_me)
|
||||||
|
|
||||||
|
self.edges = boundary_edges # ordered edges from start vert
|
||||||
|
self.verts = vertices_along_path
|
||||||
|
|
||||||
|
self.update_mid_co()
|
||||||
|
|
||||||
|
@functions.set_block_before_done
|
||||||
|
def done_from_temp(self, end_vertex):
|
||||||
|
if (
|
||||||
|
len(self.path) < 3
|
||||||
|
): # path代表鼠标路径,即使吸附上了solid边,这个也是鼠标路径
|
||||||
|
functions.log("too short, cancel")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.end_vertex = end_vertex
|
||||||
|
self.is_solid_end = (
|
||||||
|
not not end_vertex
|
||||||
|
and end_vertex not in self.get_boundaries_verts_except_self()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.start_vertex
|
||||||
|
and self.end_vertex
|
||||||
|
and self.start_vertex == self.end_vertex
|
||||||
|
):
|
||||||
|
functions.log("start and end same, cancel")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 开始处理切分↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
|
||||||
|
# 处理切分
|
||||||
|
# boundary被切分,被切分的boundary的相关的loop要先删pattern及其网格,每切分一个boundary,boundary会被删除1条,新建3条,然后重建loop,然后相关的loop(即新建的boundary,被切分成2个的boundary)要重建网格
|
||||||
|
|
||||||
|
# 截断了另外的boundary,需要把那些boundary拆分并重建,两个可能:1.开始或结束点在boundary上 2.开启了自动截断模式(笔记,自动截断的情况,获取截断点的坐标,分割path,注意要先创建截断点vert(这里单独写个函数),把他赋予到对应boundary的start和end,再执行create_mesh)
|
||||||
|
mid_verts_to_boundary = (
|
||||||
|
{}
|
||||||
|
) # 建立中间点(即除了开始和结束)到boundary的映射。考虑不是每次都建立,不然滚轮的时候会频繁创建(开销很小,不考虑)
|
||||||
|
for b in g.boundaries:
|
||||||
|
if b == self: # 排除自身
|
||||||
|
continue
|
||||||
|
for v in b.verts:
|
||||||
|
if v == b.start_vertex or v == b.end_vertex:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# # 理论上来说中间点和boundary是一一对应的,不可能有重复的情况
|
||||||
|
# if v in mid_verts_to_boundary:
|
||||||
|
# raise "mid vert duplicated"
|
||||||
|
|
||||||
|
mid_verts_to_boundary[v] = b
|
||||||
|
|
||||||
|
# TODO: 暂时无法处理两个切分点都在同一个boundary的中间点上,返回None
|
||||||
|
# TODO: 暂时无法处理两个boundary的情况
|
||||||
|
# 下面这个约束同时过滤了上面这两个,实际上这两个issue要同时解决才能应用
|
||||||
|
for b in g.boundaries:
|
||||||
|
if b == self: # 跳过自身(没必要,因为这个都没创建)
|
||||||
|
continue
|
||||||
|
if self.start_vertex in b.verts and self.end_vertex in b.verts:
|
||||||
|
functions.log("2 boundary case, cancel")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 被切割的boundary要先删pattern的mesh,等切割完了,再全部recreate
|
||||||
|
to_be_seperate_boundary_by_end = None
|
||||||
|
to_be_seperate_boundary_by_end_related_loops = []
|
||||||
|
if self.end_vertex in mid_verts_to_boundary:
|
||||||
|
to_be_seperate_boundary_by_end = mid_verts_to_boundary[self.end_vertex]
|
||||||
|
for loop in functions.get_loops_from_boundary(
|
||||||
|
to_be_seperate_boundary_by_end
|
||||||
|
):
|
||||||
|
# 删pattern
|
||||||
|
loop.remove_pattern()
|
||||||
|
to_be_seperate_boundary_by_end_related_loops.append(loop)
|
||||||
|
|
||||||
|
to_be_seperate_boundary_by_start = None
|
||||||
|
to_be_seperate_boundary_by_start_related_loops = []
|
||||||
|
if self.start_vertex in mid_verts_to_boundary:
|
||||||
|
to_be_seperate_boundary_by_start = mid_verts_to_boundary[self.start_vertex]
|
||||||
|
for loop in functions.get_loops_from_boundary(
|
||||||
|
to_be_seperate_boundary_by_start
|
||||||
|
):
|
||||||
|
# 删pattern
|
||||||
|
loop.remove_pattern()
|
||||||
|
to_be_seperate_boundary_by_start_related_loops.append(loop)
|
||||||
|
|
||||||
|
# 从全局变量删loop
|
||||||
|
to_be_seperate_boundary_related_loops = set(
|
||||||
|
(
|
||||||
|
to_be_seperate_boundary_by_end_related_loops
|
||||||
|
+ to_be_seperate_boundary_by_start_related_loops
|
||||||
|
)
|
||||||
|
) # 取并集,因为有可能会有同一个loop的情况
|
||||||
|
for (
|
||||||
|
to_be_seperate_boundary_related_loop
|
||||||
|
) in to_be_seperate_boundary_related_loops:
|
||||||
|
g.loops.remove(to_be_seperate_boundary_related_loop)
|
||||||
|
|
||||||
|
# 从全局变量删boundary
|
||||||
|
if to_be_seperate_boundary_by_end:
|
||||||
|
g.boundaries.remove(to_be_seperate_boundary_by_end)
|
||||||
|
if to_be_seperate_boundary_by_start:
|
||||||
|
g.boundaries.remove(to_be_seperate_boundary_by_start)
|
||||||
|
|
||||||
|
# 切分
|
||||||
|
if to_be_seperate_boundary_by_end:
|
||||||
|
need_to_be_cut_boundary = mid_verts_to_boundary[self.end_vertex]
|
||||||
|
seperate_boundary_1, seperate_boundary_2 = seperate_boundary_from_vert(
|
||||||
|
need_to_be_cut_boundary, self.end_vertex
|
||||||
|
)
|
||||||
|
# 切分为两个的boundary加进全局变量
|
||||||
|
for i in [seperate_boundary_1, seperate_boundary_2]:
|
||||||
|
# 重建boundary网格
|
||||||
|
i.create_mesh(3)
|
||||||
|
# 附加到全局变量
|
||||||
|
g.boundaries.add(i)
|
||||||
|
if to_be_seperate_boundary_by_start:
|
||||||
|
need_to_be_cut_boundary = mid_verts_to_boundary[self.start_vertex]
|
||||||
|
seperate_boundary_1, seperate_boundary_2 = seperate_boundary_from_vert(
|
||||||
|
need_to_be_cut_boundary, self.start_vertex
|
||||||
|
)
|
||||||
|
# 切分为两个的boundary加进全局变量
|
||||||
|
for i in [seperate_boundary_1, seperate_boundary_2]:
|
||||||
|
# 重建boundary网格
|
||||||
|
i.create_mesh(3)
|
||||||
|
# 附加到全局变量
|
||||||
|
g.boundaries.add(i)
|
||||||
|
|
||||||
|
# 重建loop和pattern在外部进行了,因为是删loop而不是改loop所以可以自动识别到
|
||||||
|
# 处理切分完毕↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
|
||||||
|
|
||||||
|
# 如果开始结束都在某个loop的控制点中,则要把那个loop删掉,如:正方形,对角切
|
||||||
|
for loop in g.loops:
|
||||||
|
controll_verts = loop.controll_verts
|
||||||
|
if (
|
||||||
|
self.start_vertex in controll_verts
|
||||||
|
and self.end_vertex in controll_verts
|
||||||
|
):
|
||||||
|
g.loops.remove(loop)
|
||||||
|
loop.remove_pattern()
|
||||||
|
|
||||||
|
# 创建网格前,处理吸附到中轴线
|
||||||
|
if self.is_editable and g.is_drawing_along_mirror:
|
||||||
|
functions.process_path_to_mirror_axis(self.path)
|
||||||
|
|
||||||
|
self.create_mesh(self.length)
|
||||||
|
g.boundaries.add(self)
|
||||||
|
|
||||||
|
g.is_redraw_boundary = True
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def remove_mesh(
|
||||||
|
self, exclude_verts=[], is_force_remove_edges=False, is_remove_start_end=False
|
||||||
|
):
|
||||||
|
if is_force_remove_edges:
|
||||||
|
functions.remove_bm_edges(
|
||||||
|
g.obj_bm, self.edges
|
||||||
|
) # using bmesh.ops.delete edges will also delete the island vert
|
||||||
|
|
||||||
|
if len(self.edges) == 1:
|
||||||
|
# 特殊情况,
|
||||||
|
functions.remove_bm_edges(
|
||||||
|
g.obj_bm, [self.edges[0]]
|
||||||
|
) # using bmesh.ops.delete edges will also delete the island vert
|
||||||
|
|
||||||
|
if len(self.edges) > 1:
|
||||||
|
if is_remove_start_end:
|
||||||
|
# 删除网格,包括开始和结束顶点,用于用户在modal的删除
|
||||||
|
boundaries_verts_except_self = self.get_boundaries_verts_except_self()
|
||||||
|
to_be_delete_verts = []
|
||||||
|
remaining_verts = []
|
||||||
|
for vert in self.verts_generator():
|
||||||
|
if vert in exclude_verts:
|
||||||
|
continue # 排除 exclude_verts 中的顶点
|
||||||
|
|
||||||
|
if self.is_solid_start and vert == self.start_vertex:
|
||||||
|
remaining_verts.append(
|
||||||
|
vert
|
||||||
|
) # 如果是 solid_start, 跳过start_vertex
|
||||||
|
continue
|
||||||
|
if self.is_solid_end and vert == self.end_vertex:
|
||||||
|
remaining_verts.append(vert) # 如果是 solid_end, 跳过end_vertex
|
||||||
|
continue
|
||||||
|
# 并且这个vert不能出现在其他boundary中
|
||||||
|
if vert in boundaries_verts_except_self:
|
||||||
|
remaining_verts.append(vert)
|
||||||
|
continue
|
||||||
|
|
||||||
|
to_be_delete_verts.append(vert)
|
||||||
|
functions.delete_bm_verts(g.obj_bm, to_be_delete_verts)
|
||||||
|
|
||||||
|
# 即使经过上面的条件,还是可能漏,如:三角形,外部画一根截断一个边,先删外部的那一根,然后删被截断的下面那根,然后删上面那根,中间会留一个顶点,因为一开始就把他当成solid_vert看了,但是随着boundary被删除,他就不是了
|
||||||
|
to_be_delete_verts = []
|
||||||
|
for vert in remaining_verts:
|
||||||
|
if len(vert.link_edges) == 0:
|
||||||
|
to_be_delete_verts.append(vert)
|
||||||
|
functions.delete_bm_verts(g.obj_bm, to_be_delete_verts)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 删除网格,除了开始和结束顶点,用于改变长度时的删除网格
|
||||||
|
verts = [
|
||||||
|
vert
|
||||||
|
for vert in self.verts_generator()
|
||||||
|
if vert != self.start_vertex
|
||||||
|
and vert != self.end_vertex
|
||||||
|
and vert not in exclude_verts
|
||||||
|
]
|
||||||
|
functions.delete_bm_verts(g.obj_bm, verts)
|
||||||
|
|
||||||
|
self.edges = []
|
||||||
|
bmesh.update_edit_mesh(g.obj_me)
|
||||||
|
|
||||||
|
def get_boundaries_verts_except_self(self):
|
||||||
|
boundaries_verts_except_self = set()
|
||||||
|
for b in g.boundaries:
|
||||||
|
if b == self:
|
||||||
|
continue
|
||||||
|
boundaries_verts_except_self.update(b.verts)
|
||||||
|
return boundaries_verts_except_self
|
||||||
|
|
||||||
|
def verts_generator(self):
|
||||||
|
# # find first edge
|
||||||
|
# start_vertex = self.start_vertex
|
||||||
|
# yield start_vertex
|
||||||
|
# for edge in self.edges:
|
||||||
|
# end_vertex = edge.other_vert(start_vertex)
|
||||||
|
# yield end_vertex
|
||||||
|
# start_vertex = end_vertex
|
||||||
|
|
||||||
|
for i in self.verts: # 搞不懂为什么一开始写成上面这样
|
||||||
|
yield i
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vertices_co(self):
|
||||||
|
return [temp.co for temp in list(self.verts_generator())]
|
||||||
|
|
||||||
|
def vertices_co_along_path(self, edge_length=None):
|
||||||
|
if edge_length is None:
|
||||||
|
edge_length = self.temp_length
|
||||||
|
return functions.uniform_vertices_co_along_path(self.path, edge_length - 1)
|
||||||
|
|
||||||
|
def update_mid_co(self):
|
||||||
|
midpoint_of_path = functions.get_midpoint_of_path(g.obj_bm, self.edges)
|
||||||
|
|
||||||
|
self.mid_co = g.obj.matrix_world @ midpoint_of_path
|
||||||
|
return
|
||||||
|
|
||||||
|
@functions.set_block_before_done
|
||||||
|
def delete(self):
|
||||||
|
# boundary删除,loop可能会被删除,loop被删pattern也会被删
|
||||||
|
related_loops = functions.get_loops_from_boundary(self)
|
||||||
|
|
||||||
|
# 删除网格
|
||||||
|
for related_loop in related_loops:
|
||||||
|
related_loop.remove_pattern()
|
||||||
|
|
||||||
|
if not self.solid_path:
|
||||||
|
self.remove_mesh(is_remove_start_end=True)
|
||||||
|
|
||||||
|
# 从全局变量中删除loop和boundary
|
||||||
|
g.boundaries.remove(self)
|
||||||
|
for related_loop in related_loops:
|
||||||
|
g.loops.remove(related_loop)
|
||||||
|
|
||||||
|
g.is_redraw_boundary = True
|
||||||
|
g.is_redraw_pattern = True
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def seperate_from_path_idxs(path_idxs):
|
||||||
|
# 从多个
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def seperate_boundary_from_vert(boundary, seperate_vert):
|
||||||
|
# 从boundary已有的顶点,拆分boundary
|
||||||
|
# 如果path不够长,则只有头尾直线
|
||||||
|
|
||||||
|
# # 首先要删除boundaries列表(外部删了)
|
||||||
|
# g.boundaries.remove(boundary)
|
||||||
|
|
||||||
|
seperate_vert_co_world = g.obj.matrix_world @ seperate_vert.co
|
||||||
|
|
||||||
|
# 删除boundary网格(不包括开始和结束点和vert)
|
||||||
|
boundary.remove_mesh(exclude_verts=[seperate_vert], is_force_remove_edges=True)
|
||||||
|
|
||||||
|
# 拆分
|
||||||
|
ori_start_vert = boundary.start_vertex
|
||||||
|
ori_end_vert = boundary.end_vertex
|
||||||
|
|
||||||
|
# 通过verts_correspond_path_idxs,来获取已有顶点到path索引的映射,因为path是不均匀的,所以要在计算均匀顶点的时候顺便计算
|
||||||
|
vert_idx_in_verts = boundary.verts.index(seperate_vert)
|
||||||
|
vert_idx_in_path = boundary.verts_correspond_path_idxs[
|
||||||
|
vert_idx_in_verts
|
||||||
|
] # 如果path 10个,则范围是0~9,tou是0,尾是9,中间是0~8
|
||||||
|
if len(boundary.path) < vert_idx_in_path + 2: # 理论不会发生
|
||||||
|
raise f"vert_idx_in_path exceed path length: {len(boundary.path)} {vert_idx_in_path}"
|
||||||
|
path_1 = boundary.path[0 : vert_idx_in_path + 1] + [seperate_vert_co_world]
|
||||||
|
path_2 = [seperate_vert_co_world] + boundary.path[
|
||||||
|
vert_idx_in_path + 1 : len(boundary.path)
|
||||||
|
]
|
||||||
|
|
||||||
|
boundary_1 = Boundary(ori_start_vert, seperate_vert, path_1)
|
||||||
|
boundary_2 = Boundary(seperate_vert, ori_end_vert, path_2)
|
||||||
|
|
||||||
|
return boundary_1, boundary_2
|
||||||
8
const.py
Normal file
8
const.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
addon_prefix = "easy_patch"
|
||||||
|
|
||||||
|
draw_path_threshold = 2 # 绘制路径的最小间隔
|
||||||
|
hover_threshold = 20 # 触发鼠标悬浮的最小间隔
|
||||||
|
|
||||||
|
mouse_move_threshold = 2 # px
|
||||||
|
|
||||||
|
snap_vertex_threshold = 30
|
||||||
2
debug.log
Normal file
2
debug.log
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[0303/180138.538:INFO:gin\isolate_holder.cc:165] SetPartitionAllocOomCallback and RegisterIsolateHolder
|
||||||
|
[0303/180155.896:INFO:gin\isolate_holder.cc:165] SetPartitionAllocOomCallback and RegisterIsolateHolder
|
||||||
785
draw.py
Normal file
785
draw.py
Normal file
@@ -0,0 +1,785 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import bpy
|
||||||
|
import blf
|
||||||
|
import gpu
|
||||||
|
import bmesh
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
from . import g
|
||||||
|
from gpu_extras.batch import batch_for_shader
|
||||||
|
from gpu_extras.presets import draw_circle_2d
|
||||||
|
from .utils import functions
|
||||||
|
import random
|
||||||
|
from .utils.trans import t
|
||||||
|
import mathutils
|
||||||
|
import itertools
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
|
from .utils import functions
|
||||||
|
from . import const
|
||||||
|
|
||||||
|
if bpy.app.version[0] == 3 or bpy.app.version[0] == 4:
|
||||||
|
import bgl
|
||||||
|
|
||||||
|
def draw_desc(self, context):
|
||||||
|
prefs = functions.get_preferences()
|
||||||
|
|
||||||
|
if prefs.desc_position_y == 100 or prefs.desc_position_x == 100:
|
||||||
|
return
|
||||||
|
|
||||||
|
font_id = 0
|
||||||
|
|
||||||
|
region = context.region
|
||||||
|
width, height = region.width, region.height
|
||||||
|
|
||||||
|
blf.color(font_id, 1, 1, 1, 0.6)
|
||||||
|
|
||||||
|
font_size = self.get_font_size()
|
||||||
|
self.set_font_size(font_size)
|
||||||
|
# base_x, base_y = width / 15, height * 7 / 8
|
||||||
|
base_x = width * prefs.desc_position_x / 100
|
||||||
|
base_y = height * (100 - prefs.desc_position_y) / 100
|
||||||
|
|
||||||
|
def position_generator(base_x, base_y, line_height):
|
||||||
|
while True:
|
||||||
|
blf.position(font_id, base_x, base_y, 0)
|
||||||
|
yield
|
||||||
|
base_y -= line_height
|
||||||
|
|
||||||
|
pos_gen = position_generator(base_x, base_y, font_size)
|
||||||
|
|
||||||
|
if g.mode == g.Mode.NORMAL:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('current_status')}: {t('NORMAL')}")
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('click_drag_to_draw_line')}")
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('hold_ctrl')} - {t('snap_vert')}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id, f"{t('hold_ctrl')}+{t('hold_shift')} - {t('snap_exist_path')}"
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"M {t('key')} - {t('draw_on_axis_of_symmetry')}{'('+t('persist')+')' if g.is_drawing_along_mirror_persist else ''}: {t('enable') if g.is_drawing_along_mirror else t('disable')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"B {t('key')} - {t('skip_no_solution_pattern')}: {t('enable') if g.is_auto_search_pattern else t('disable')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"S {t('key')} - {t('snap_surface')}: {t('enable') if g.is_snap_surface else t('disable')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if g.mode == g.Mode.NORMAL:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"V {t('key')} - {t('snap_opposite')}: {t('enable') if g.is_snap_opposite else t('disable')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if g.hovering_boundary:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"X {t('key')} - {t('delete_highlight_edge')}")
|
||||||
|
|
||||||
|
if g.hovering_loop:
|
||||||
|
loop = g.hovering_loop
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"Ctrl+{t('scroll')} - {t('change_segment')}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"Shift+{t('scroll')} - {t('change_padding')}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"T {t('key')} - {t('switch_pattern')}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"R {t('key')} - {t('rotate_pattern')}")
|
||||||
|
|
||||||
|
if g.hovering_loop.pattern:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"Shift + 1,2,3 - {t('smooth_mesh')}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"i {t('key')} - {t('faces_normal_toward_viewport')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if g.hovering_loop.pattern:
|
||||||
|
pattern = loop.pattern
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('solver_solution')}: {[int(i) for i in pattern.solution]}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('current_para')}:")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('pattern')}: {pattern.name}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('rotation')}: {pattern.rotation}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.color(font_id, 1, 0.5, 0.5, 0.6)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('solver_solution')}: {loop.solver_msg}",
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
if (
|
||||||
|
any([i for i in loop.solver_constraint_list if i is not None])
|
||||||
|
or loop.solver_constraint_rotation is not None
|
||||||
|
or loop.solver_constraint_pattern
|
||||||
|
):
|
||||||
|
blf.color(font_id, 1, 0.5, 0.5, 0.6)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('solver_constraint')}:",
|
||||||
|
)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('pattern')}: {loop.solver_constraint_pattern.name}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('rotation')}: {loop.solver_constraint_rotation}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"{t('var_constraint')}: {[i if i is not None else '' for i in loop.solver_constraint_list]}",
|
||||||
|
)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"C {t('key')} {t('clear_constraint')}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"Shift + C {t('only_clear_var')}",
|
||||||
|
)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(
|
||||||
|
font_id,
|
||||||
|
f"Ctrl + C {t('only_clear_rotation')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif g.mode == g.Mode.DRAWING:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('current_status')}: {t('DRAWING')}")
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('right_click')}: {t('cancel_this_draw')}")
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('left_click_release')}: {t('apply_this_draw')}")
|
||||||
|
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('scroll')} - {t('change_segment')}")
|
||||||
|
|
||||||
|
elif g.mode == g.Mode.MERGE_BOUNDARY:
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('current_status')}: {t('MERGE_BOUNDARY')}")
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
|
||||||
|
blf.color(font_id, 1, 1, 1, 0.6)
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"{t('enter')} - {t('apply_mesh')}")
|
||||||
|
next(pos_gen)
|
||||||
|
blf.draw(font_id, f"ESC - {t('quit')}")
|
||||||
|
|
||||||
|
|
||||||
|
def draw_snap_vertex(self, context):
|
||||||
|
if (
|
||||||
|
not g.drawing_verts_snap_vertex
|
||||||
|
or g.drawing_verts_snap_vertex not in g.obj_bm.verts
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
location_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
context.region,
|
||||||
|
context.region_data,
|
||||||
|
g.obj.matrix_world @ g.drawing_verts_snap_vertex.co,
|
||||||
|
)
|
||||||
|
if not location_2d:
|
||||||
|
return
|
||||||
|
|
||||||
|
if location_2d:
|
||||||
|
font_size = self.get_font_size() // 2
|
||||||
|
self.set_line_width(1)
|
||||||
|
draw_circle_2d(location_2d, (1, 1, 1, 1), font_size)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_hovering_loop_3d(self):
|
||||||
|
if g.mode != g.Mode.NORMAL:
|
||||||
|
return
|
||||||
|
|
||||||
|
hovering_loop = g.hovering_loop
|
||||||
|
if not hovering_loop:
|
||||||
|
return
|
||||||
|
|
||||||
|
verts = hovering_loop.verts
|
||||||
|
mid_co = sum((g.obj.matrix_world @ v.co for v in verts), mathutils.Vector()) / len(
|
||||||
|
verts
|
||||||
|
)
|
||||||
|
|
||||||
|
coords = [mid_co] + [g.obj.matrix_world @ v.co for v in verts]
|
||||||
|
indices = []
|
||||||
|
|
||||||
|
for i in range(len(verts)):
|
||||||
|
if i == len(verts) - 1:
|
||||||
|
indices.append((0, i + 1, 1))
|
||||||
|
else:
|
||||||
|
indices.append((0, i + 1, i + 2))
|
||||||
|
|
||||||
|
shader = self.shader_3d_uniform_color
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
batch = batch_for_shader(shader, "TRIS", {"pos": coords}, indices=indices)
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", (0, 0.6, 0, 0.3))
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_hovering_boundary(self):
|
||||||
|
if g.mode != g.Mode.NORMAL:
|
||||||
|
return
|
||||||
|
|
||||||
|
hovering_boundary = g.hovering_boundary
|
||||||
|
if not hovering_boundary:
|
||||||
|
return
|
||||||
|
|
||||||
|
path = [g.obj.matrix_world @ v.co for v in hovering_boundary.verts]
|
||||||
|
line_co_pairs = []
|
||||||
|
for i in range(len(path) - 1):
|
||||||
|
line_co_pairs.append((path[i], path[i + 1]))
|
||||||
|
draw_lines_3d(self, line_co_pairs, (1, 1, 1, 1), 5)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_loop(self):
|
||||||
|
for loop in g.loops:
|
||||||
|
for idx, (boundary, is_forward) in enumerate(loop.b_and_d_list):
|
||||||
|
|
||||||
|
region = bpy.context.region
|
||||||
|
rv3d = bpy.context.region_data
|
||||||
|
coord_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, boundary.mid_co
|
||||||
|
)
|
||||||
|
if coord_2d is not None:
|
||||||
|
text = str(idx)
|
||||||
|
if not is_forward:
|
||||||
|
text += " R"
|
||||||
|
|
||||||
|
# 绘制索引
|
||||||
|
blf.size(0, 50)
|
||||||
|
blf.position(0, coord_2d[0], coord_2d[1], 0)
|
||||||
|
blf.color(0, 0, 1, 0, 1)
|
||||||
|
blf.draw(0, text)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_temp_patch_boundary(self):
|
||||||
|
boundary = g.drawing_path_boundary
|
||||||
|
if not boundary:
|
||||||
|
return
|
||||||
|
|
||||||
|
# draw path
|
||||||
|
vertex_co_along_path, path = boundary.get_actual_verts_co_and_path()
|
||||||
|
|
||||||
|
shader = self.shader_3d_uniform_color
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
self.set_line_width(5)
|
||||||
|
batch = batch_for_shader(shader, "LINE_STRIP", {"pos": path})
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", (0.0, 0.0, 1, 0.5))
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
# draw vertex
|
||||||
|
# vertex_co_along_path = patch_boundary.vertices_co_along_path()
|
||||||
|
for vertex_co in vertex_co_along_path:
|
||||||
|
# TODO: vulkun 用这个shader的时候,gpu.state.point_size_set 失效了,等待修复或别的方案。因为新shader POINT_UNIFORM_COLOR,只能画2d顶点
|
||||||
|
batch = batch_for_shader(shader, "POINTS", {"pos": [vertex_co]})
|
||||||
|
shader.uniform_float("color", (1, 1, 1, 1))
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_boundaries_3d(self):
|
||||||
|
if not g.boundaries:
|
||||||
|
return
|
||||||
|
|
||||||
|
# edges
|
||||||
|
if g.is_redraw_boundary or not g.last_boundary_data:
|
||||||
|
draw_boundary_data_list = []
|
||||||
|
for boundary in g.boundaries:
|
||||||
|
line_color = functions.hsv_to_rgb(
|
||||||
|
functions.stable_random_from_obj(boundary), 1, 1, 1
|
||||||
|
)
|
||||||
|
boundaries_verts_co = boundary.vertices_co
|
||||||
|
boundaries_verts_co = [g.obj.matrix_world @ i for i in boundaries_verts_co]
|
||||||
|
|
||||||
|
line_co_pairs = []
|
||||||
|
vert_coords = boundaries_verts_co
|
||||||
|
for i in range(len(boundaries_verts_co) - 1):
|
||||||
|
line_co_pair = (boundaries_verts_co[i], boundaries_verts_co[i + 1])
|
||||||
|
|
||||||
|
line_co_pairs.append(line_co_pair)
|
||||||
|
|
||||||
|
draw_boundary_data_list.append((line_co_pairs, line_color, vert_coords))
|
||||||
|
|
||||||
|
draw_lines_3d(self, line_co_pairs, line_color, line_width=2.5)
|
||||||
|
draw_points_3d(self, vert_coords, (1, 1, 1, 1), point_size=5)
|
||||||
|
|
||||||
|
g.last_boundary_data = draw_boundary_data_list
|
||||||
|
g.is_redraw_boundary = False
|
||||||
|
else:
|
||||||
|
for boundary_data in g.last_boundary_data:
|
||||||
|
line_co_pairs, line_color, vert_coords = boundary_data
|
||||||
|
draw_lines_3d(self, line_co_pairs, line_color, line_width=2.5)
|
||||||
|
draw_points_3d(self, vert_coords, (1, 1, 1, 1), point_size=5)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_boundaries_text_2d(self):
|
||||||
|
font_id = 0
|
||||||
|
font_size = self.get_font_size()
|
||||||
|
|
||||||
|
context = bpy.context
|
||||||
|
rv3d = context.region_data
|
||||||
|
region = context.region
|
||||||
|
|
||||||
|
# 绘制边数
|
||||||
|
for boundary in g.boundaries:
|
||||||
|
coord_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, boundary.mid_co)
|
||||||
|
if coord_2d is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
x, y = coord_2d
|
||||||
|
blf.position(font_id, x, y, 0)
|
||||||
|
blf.color(font_id, 1, 1, 1, 1)
|
||||||
|
self.set_font_size(font_size)
|
||||||
|
blf.draw(font_id, f"{boundary.length}")
|
||||||
|
|
||||||
|
# 绘制建议文本
|
||||||
|
for loop in g.loops:
|
||||||
|
for i, boundary in enumerate(loop.boundaries):
|
||||||
|
boundary_mid_co_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, boundary.mid_co
|
||||||
|
)
|
||||||
|
loop_mid_co_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, loop.mid_co
|
||||||
|
)
|
||||||
|
|
||||||
|
offset = mathutils.Vector((0, 0))
|
||||||
|
if (
|
||||||
|
boundary_mid_co_2d.x <= loop_mid_co_2d.x
|
||||||
|
and boundary_mid_co_2d.y <= loop_mid_co_2d.y
|
||||||
|
):
|
||||||
|
result = mathutils.Vector((1, 1)) # a在b的左下角
|
||||||
|
elif (
|
||||||
|
boundary_mid_co_2d.x >= loop_mid_co_2d.x
|
||||||
|
and boundary_mid_co_2d.y <= loop_mid_co_2d.y
|
||||||
|
):
|
||||||
|
result = mathutils.Vector((-3, 1)) # a在b的右下角
|
||||||
|
elif (
|
||||||
|
boundary_mid_co_2d.x <= loop_mid_co_2d.x
|
||||||
|
and boundary_mid_co_2d.y >= loop_mid_co_2d.y
|
||||||
|
):
|
||||||
|
result = mathutils.Vector((1, -1)) # a在b的左上角
|
||||||
|
elif (
|
||||||
|
boundary_mid_co_2d.x >= loop_mid_co_2d.x
|
||||||
|
and boundary_mid_co_2d.y >= loop_mid_co_2d.y
|
||||||
|
):
|
||||||
|
result = mathutils.Vector((-3, -1)) # a在b的右上角
|
||||||
|
|
||||||
|
offset_value = 20
|
||||||
|
offset = result * offset_value
|
||||||
|
|
||||||
|
# 建议文本
|
||||||
|
suggest_text = None
|
||||||
|
if loop.suggest_solution:
|
||||||
|
pos_value = loop.suggest_solution[i]
|
||||||
|
neg_value = loop.suggest_solution[i + len(loop.boundaries)]
|
||||||
|
if pos_value > 0:
|
||||||
|
suggest_text = f"+{int(pos_value)}"
|
||||||
|
if neg_value > 0:
|
||||||
|
suggest_text = f"{-int(neg_value)}"
|
||||||
|
|
||||||
|
suggest_text_co_3d = boundary.mid_co
|
||||||
|
suggest_text_co_2d = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, suggest_text_co_3d
|
||||||
|
)
|
||||||
|
if not suggest_text_co_2d:
|
||||||
|
continue
|
||||||
|
suggest_text_co_2d = suggest_text_co_2d + offset
|
||||||
|
|
||||||
|
constraint_text = None
|
||||||
|
constraint_value = loop.solver_constraint_list[i]
|
||||||
|
if loop.solver_constraint_list[i] is not None:
|
||||||
|
constraint_text = f"{t('padding')}:{int(constraint_value)}"
|
||||||
|
draw_patch_boundary_text(
|
||||||
|
self,
|
||||||
|
suggest_text_co_2d,
|
||||||
|
suggest_text=suggest_text,
|
||||||
|
constraint_text=constraint_text,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_patch_pattern(self):
|
||||||
|
font_id = 0
|
||||||
|
|
||||||
|
if g.is_redraw_pattern:
|
||||||
|
for loop in g.loops:
|
||||||
|
pattern = loop.pattern
|
||||||
|
if not loop.pattern:
|
||||||
|
continue
|
||||||
|
|
||||||
|
color = (1, 1, 1, 0.5)
|
||||||
|
verts = pattern.interior_verts
|
||||||
|
edges = pattern.interior_edges
|
||||||
|
if verts:
|
||||||
|
pass
|
||||||
|
# 减轻压力
|
||||||
|
# draw_verts_2d(self, verts, color, 5)
|
||||||
|
|
||||||
|
line_co_pairs = []
|
||||||
|
for edge in edges:
|
||||||
|
line_co_pair = (
|
||||||
|
g.obj.matrix_world @ edge.verts[0].co,
|
||||||
|
g.obj.matrix_world @ edge.verts[1].co,
|
||||||
|
)
|
||||||
|
line_co_pairs.append(line_co_pair)
|
||||||
|
|
||||||
|
if line_co_pairs:
|
||||||
|
draw_lines_3d(self, line_co_pairs, color, line_width=2)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_lines_3d(self, line_co_pairs, color, line_width=2):
|
||||||
|
if not line_co_pairs:
|
||||||
|
return
|
||||||
|
|
||||||
|
line_co_pair_flatten = list(itertools.chain(*line_co_pairs))
|
||||||
|
|
||||||
|
shader = self.shader_3d_uniform_color
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
self.set_line_width(line_width)
|
||||||
|
batch = batch_for_shader(shader, "LINES", {"pos": line_co_pair_flatten})
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", color)
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_points_3d(self, vert_coords, color, point_size=3):
|
||||||
|
if not vert_coords:
|
||||||
|
return
|
||||||
|
|
||||||
|
if bpy.app.version < (4, 5, 0):
|
||||||
|
shader = self.shader_3d_uniform_color
|
||||||
|
gpu.state.blend_set("ALPHA")
|
||||||
|
self.set_point_size(point_size)
|
||||||
|
batch = batch_for_shader(shader, "POINTS", {"pos": vert_coords})
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", color)
|
||||||
|
batch.draw(shader)
|
||||||
|
else:
|
||||||
|
shader = self.shader_point_uniform_color
|
||||||
|
batch = batch_for_shader(shader, "POINTS", {"pos": vert_coords})
|
||||||
|
shader.uniform_float("color", color)
|
||||||
|
gpu.state.point_size_set(point_size)
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_edges_2d(self, edges, color):
|
||||||
|
if not edges:
|
||||||
|
return
|
||||||
|
|
||||||
|
edges_co = [
|
||||||
|
tuple(g.obj.matrix_world @ vert.co for vert in edge.verts) for edge in edges
|
||||||
|
]
|
||||||
|
context = bpy.context
|
||||||
|
region = context.region
|
||||||
|
rv3d = context.region_data
|
||||||
|
|
||||||
|
coords_3d = edges_co
|
||||||
|
coords_2d = [
|
||||||
|
functions.coords_3d_to_2d(region, rv3d, sublist) for sublist in coords_3d
|
||||||
|
]
|
||||||
|
if None in coords_2d:
|
||||||
|
return
|
||||||
|
|
||||||
|
coords_2d = list(itertools.chain.from_iterable(coords_2d))
|
||||||
|
|
||||||
|
shader = self.shader_2d_uniform_color
|
||||||
|
batch = batch_for_shader(shader, "LINES", {"pos": coords_2d})
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", color)
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_path_2d(self, path, color, line_width=1):
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
|
||||||
|
edges_co = [(path[i], path[i + 1]) for i in range(len(path) - 1)]
|
||||||
|
|
||||||
|
context = bpy.context
|
||||||
|
region = context.region
|
||||||
|
rv3d = context.region_data
|
||||||
|
|
||||||
|
coords_3d = edges_co
|
||||||
|
coords_2d = [
|
||||||
|
[
|
||||||
|
(view3d_utils.location_3d_to_region_2d(region, rv3d, item) or (None, None))
|
||||||
|
for item in sublist
|
||||||
|
if view3d_utils.location_3d_to_region_2d(region, rv3d, item) is not None
|
||||||
|
]
|
||||||
|
for sublist in coords_3d
|
||||||
|
]
|
||||||
|
if None in coords_2d:
|
||||||
|
return
|
||||||
|
|
||||||
|
coords_2d = list(itertools.chain.from_iterable(coords_2d))
|
||||||
|
|
||||||
|
self.set_line_width(line_width)
|
||||||
|
shader = self.shader_2d_uniform_color
|
||||||
|
batch = batch_for_shader(shader, "LINES", {"pos": coords_2d})
|
||||||
|
shader.bind()
|
||||||
|
shader.uniform_float("color", color)
|
||||||
|
batch.draw(shader)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_patch_boundary_text(self, coord_2d, suggest_text, constraint_text):
|
||||||
|
|
||||||
|
font_id = 0
|
||||||
|
font_size = self.get_font_size()
|
||||||
|
|
||||||
|
# text
|
||||||
|
if coord_2d is None:
|
||||||
|
return
|
||||||
|
x, y = coord_2d
|
||||||
|
|
||||||
|
self.set_font_size(font_size * (2 / 3))
|
||||||
|
offset = font_size
|
||||||
|
if suggest_text is not None:
|
||||||
|
blf.position(font_id, x, y - offset, 0)
|
||||||
|
blf.color(font_id, 0, 1, 0, 1)
|
||||||
|
blf.draw(font_id, str(suggest_text))
|
||||||
|
offset += font_size
|
||||||
|
|
||||||
|
if constraint_text is not None:
|
||||||
|
blf.position(font_id, x, y, 0)
|
||||||
|
blf.color(font_id, 1, 0, 0, 1)
|
||||||
|
blf.draw(font_id, str(constraint_text))
|
||||||
|
|
||||||
|
self.set_font_size(font_size)
|
||||||
|
|
||||||
|
|
||||||
|
class Draw:
|
||||||
|
def __init__(self):
|
||||||
|
self.title_handler = None
|
||||||
|
self.temp_handler = None
|
||||||
|
self.snap_vertex_handler = None
|
||||||
|
|
||||||
|
self._draw_2d_handler = None
|
||||||
|
self._draw_3d_handler = None
|
||||||
|
|
||||||
|
def get_font_size(self):
|
||||||
|
region = bpy.context.region
|
||||||
|
width, height = region.width, region.height
|
||||||
|
font_size = max(10, min(width, height) // 50)
|
||||||
|
return font_size
|
||||||
|
|
||||||
|
def draw_2d(self, context):
|
||||||
|
if repr(self.op_instance).endswith("invalid>"):
|
||||||
|
self.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
if g.is_changing_mesh:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not g.is_running:
|
||||||
|
self.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
start_time = time.time()
|
||||||
|
# draw_hovering_boundary_2d(self)
|
||||||
|
draw_hovering_boundary_2d_time = time.time() - start_time
|
||||||
|
|
||||||
|
draw_desc(self, context)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
draw_snap_vertex(self, context)
|
||||||
|
draw_snap_vertex_time = time.time() - start_time
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
# draw_boundaries(self)
|
||||||
|
draw_boundaries_time = time.time() - start_time
|
||||||
|
|
||||||
|
draw_boundaries_text_2d(self)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
# draw_patch_pattern(self)
|
||||||
|
draw_patch_pattern_time = time.time() - start_time
|
||||||
|
|
||||||
|
# last_echo_time = g.last_echo_time if g.last_echo_time else 0
|
||||||
|
# if time.time() - last_echo_time > 0.1:
|
||||||
|
# print(
|
||||||
|
# f"{draw_hovering_boundary_2d_time:.3f}",
|
||||||
|
# f"{draw_snap_vertex_time:.3f}",
|
||||||
|
# f"{draw_boundaries_time:.3f}",
|
||||||
|
# f"{draw_patch_pattern_time:.3f}",
|
||||||
|
# )
|
||||||
|
# g.last_echo_time = time.time()
|
||||||
|
except Exception as e:
|
||||||
|
self.clear()
|
||||||
|
self.process_exception(e)
|
||||||
|
|
||||||
|
# draw_loop(self) # debug
|
||||||
|
|
||||||
|
def draw_3d(self, context):
|
||||||
|
if repr(self.op_instance).endswith("invalid>"):
|
||||||
|
self.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
if g.is_changing_mesh:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not g.is_running:
|
||||||
|
self.clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
draw_hovering_boundary(self)
|
||||||
|
|
||||||
|
draw_hovering_loop_3d(self)
|
||||||
|
draw_temp_patch_boundary(self)
|
||||||
|
|
||||||
|
draw_patch_pattern(self)
|
||||||
|
draw_boundaries_3d(self)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.clear()
|
||||||
|
self.process_exception(e)
|
||||||
|
# raise e
|
||||||
|
|
||||||
|
def process_exception(self, e):
|
||||||
|
error_info = traceback.format_exc()
|
||||||
|
functions.log(error_info)
|
||||||
|
|
||||||
|
# tab退出时,bm删除,这里执行循环很快,还来不及清空,会抛出ReferenceError,这个情况不向ui抛出错误
|
||||||
|
if isinstance(e, ReferenceError):
|
||||||
|
error_info = traceback.format_exc()
|
||||||
|
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
g.running_exception = e
|
||||||
|
# raise e
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_line_width(self, line_width):
|
||||||
|
if bpy.app.version[0] == 3:
|
||||||
|
# 3.x
|
||||||
|
bgl.glLineWidth(line_width)
|
||||||
|
else:
|
||||||
|
# 4.x 5.x
|
||||||
|
gpu.state.line_width_set(line_width)
|
||||||
|
|
||||||
|
def set_point_size(self, point_size):
|
||||||
|
if bpy.app.version[0] == 3:
|
||||||
|
# 3.x
|
||||||
|
bgl.glPointSize(point_size)
|
||||||
|
else:
|
||||||
|
# 4.x 5.x
|
||||||
|
gpu.state.point_size_set(point_size)
|
||||||
|
|
||||||
|
def set_font_size(self, font_size):
|
||||||
|
font_id = 0
|
||||||
|
if bpy.app.version[0] == 3:
|
||||||
|
# # 3.x got 3
|
||||||
|
blf.size(font_id, font_size, 72)
|
||||||
|
else:
|
||||||
|
# 4.0 got 2
|
||||||
|
blf.size(font_id, font_size)
|
||||||
|
|
||||||
|
def start(self, op_instance):
|
||||||
|
functions.log("start draw")
|
||||||
|
|
||||||
|
self.op_instance = op_instance
|
||||||
|
|
||||||
|
if bpy.app.version[0] == 3:
|
||||||
|
if bpy.app.version[0] >= 6:
|
||||||
|
# 3.6
|
||||||
|
self.shader_3d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||||
|
self.shader_2d_uniform_color = gpu.shader.from_builtin(
|
||||||
|
"2D_UNIFORM_COLOR"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 3.3
|
||||||
|
self.shader_3d_uniform_color = gpu.shader.from_builtin(
|
||||||
|
"3D_UNIFORM_COLOR"
|
||||||
|
)
|
||||||
|
self.shader_2d_uniform_color = gpu.shader.from_builtin(
|
||||||
|
"2D_UNIFORM_COLOR"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 4.0 5.x
|
||||||
|
self.shader_3d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||||
|
self.shader_2d_uniform_color = gpu.shader.from_builtin("UNIFORM_COLOR")
|
||||||
|
|
||||||
|
if bpy.app.version >= (4, 5, 0):
|
||||||
|
self.shader_point_uniform_color = gpu.shader.from_builtin("POINT_UNIFORM_COLOR")
|
||||||
|
|
||||||
|
if not self._draw_2d_handler:
|
||||||
|
self._draw_2d_handler = bpy.types.SpaceView3D.draw_handler_add(
|
||||||
|
self.draw_2d,
|
||||||
|
(bpy.context,),
|
||||||
|
"WINDOW",
|
||||||
|
"POST_PIXEL",
|
||||||
|
)
|
||||||
|
if not self._draw_3d_handler:
|
||||||
|
self._draw_3d_handler = bpy.types.SpaceView3D.draw_handler_add(
|
||||||
|
self.draw_3d,
|
||||||
|
(bpy.context,),
|
||||||
|
"WINDOW",
|
||||||
|
"POST_VIEW",
|
||||||
|
)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
functions.log("darw cleared")
|
||||||
|
if self._draw_2d_handler:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._draw_2d_handler, "WINDOW")
|
||||||
|
self._draw_2d_handler = None
|
||||||
|
|
||||||
|
if self._draw_3d_handler:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(self._draw_3d_handler, "WINDOW")
|
||||||
|
self._draw_3d_handler = None
|
||||||
|
|
||||||
|
|
||||||
|
draw = Draw()
|
||||||
118
g.py
Normal file
118
g.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
def clear_all_variables():
|
||||||
|
global_vars = globals()
|
||||||
|
|
||||||
|
for var in list(global_vars):
|
||||||
|
if not var.startswith("__") and not callable(global_vars[var]):
|
||||||
|
global_vars[var] = None
|
||||||
|
|
||||||
|
init_variables()
|
||||||
|
|
||||||
|
|
||||||
|
def init_variables():
|
||||||
|
global_vars = globals()
|
||||||
|
|
||||||
|
global boundaries
|
||||||
|
boundaries = set()
|
||||||
|
|
||||||
|
global patches
|
||||||
|
patches = set()
|
||||||
|
|
||||||
|
global loops
|
||||||
|
loops = []
|
||||||
|
|
||||||
|
global temp_bm_verts
|
||||||
|
temp_bm_verts = set()
|
||||||
|
|
||||||
|
global temp_bm_edges
|
||||||
|
temp_bm_edges = set()
|
||||||
|
|
||||||
|
global snap_bvh_trees
|
||||||
|
snap_bvh_trees = []
|
||||||
|
|
||||||
|
global last_update_hovering_timestamp
|
||||||
|
last_update_hovering_timestamp = 0
|
||||||
|
|
||||||
|
global last_update_snap_timestamp
|
||||||
|
last_update_snap_timestamp = 0
|
||||||
|
|
||||||
|
global mode
|
||||||
|
mode = Mode.NORMAL
|
||||||
|
|
||||||
|
global is_snap_surface
|
||||||
|
is_snap_surface = True
|
||||||
|
|
||||||
|
global is_auto_search_pattern
|
||||||
|
is_auto_search_pattern = True
|
||||||
|
|
||||||
|
global is_snap_opposite
|
||||||
|
is_snap_opposite = True
|
||||||
|
|
||||||
|
|
||||||
|
is_running = False
|
||||||
|
running_exception = None # 存放外部发生的错误,让他可以raise到用户界面
|
||||||
|
|
||||||
|
# 绘制曲线中
|
||||||
|
drawing_verts_snap_vertex = None
|
||||||
|
drawing_path_boundary = None
|
||||||
|
|
||||||
|
|
||||||
|
patches = set() # 建成的补丁
|
||||||
|
boundaries = set() # 建成的边
|
||||||
|
loops = (
|
||||||
|
[]
|
||||||
|
) # boundary环路列表,元素是loop,loop是元组的列表,元组第一个元素是boundary,第二个元素是方向
|
||||||
|
|
||||||
|
# 记录插件创建的点和边,防止误删搞坏用户的网格,使用set,提高in语句的性能
|
||||||
|
# TODO: 網格更新的時候沒有及時更新,但是暂时没问题,不考虑这个,只在最后获取顶点数的时候有问题
|
||||||
|
temp_bm_verts = set()
|
||||||
|
temp_bm_edges = set()
|
||||||
|
|
||||||
|
# 全局设置
|
||||||
|
is_snap_surface = True
|
||||||
|
is_snap_opposite = True
|
||||||
|
is_auto_search_pattern = True
|
||||||
|
|
||||||
|
snap_bvh_trees = [] # 要吸附的bvh_tree
|
||||||
|
obj = None # 编辑物体
|
||||||
|
obj_bm = None # 编辑物体的bm
|
||||||
|
obj_me = None # 编辑物体的data即mesh对象
|
||||||
|
obj_bvh_tree = None # 编辑物体的data即mesh对象
|
||||||
|
|
||||||
|
hovering_loop = None
|
||||||
|
hovering_boundary = None
|
||||||
|
|
||||||
|
temp_merge_boundary_1 = None # 临时变量,用于临时储存合并boundary
|
||||||
|
|
||||||
|
last_update_hovering_timestamp = 0
|
||||||
|
last_update_snap_timestamp = 0
|
||||||
|
|
||||||
|
last_mouse_co_2d = None
|
||||||
|
|
||||||
|
last_echo_time = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Mode(Enum):
|
||||||
|
NORMAL = 1
|
||||||
|
DRAWING = 2
|
||||||
|
MERGE_BOUNDARY = 3
|
||||||
|
|
||||||
|
|
||||||
|
mode = Mode.NORMAL
|
||||||
|
|
||||||
|
mirror_path = []
|
||||||
|
|
||||||
|
is_drawing_along_mirror = False
|
||||||
|
is_drawing_along_mirror_persist = False
|
||||||
|
|
||||||
|
is_changing_mesh = False
|
||||||
|
|
||||||
|
|
||||||
|
is_redraw_boundary = False
|
||||||
|
last_boundary_data = None
|
||||||
|
is_redraw_pattern = False
|
||||||
|
last_pattern_data = None
|
||||||
|
is_redraw_hovering = False
|
||||||
|
last_hovering_data = None
|
||||||
194
generate_loop.py
Normal file
194
generate_loop.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
from . import g
|
||||||
|
from .loop import Loop
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# 因为要引用Loop patch,所以单独放了
|
||||||
|
|
||||||
|
|
||||||
|
def regenerate_loops(is_force_refresh=False):
|
||||||
|
boundaries = g.boundaries # 假设 'g' 有一个 'boundaries' 属性,它包含边界对象
|
||||||
|
graph = {}
|
||||||
|
|
||||||
|
# 构建无向图
|
||||||
|
for boundary in boundaries:
|
||||||
|
graph.setdefault(boundary.start_vertex, []).append(boundary.end_vertex)
|
||||||
|
graph.setdefault(boundary.end_vertex, []).append(boundary.start_vertex)
|
||||||
|
|
||||||
|
# 方向字典
|
||||||
|
boundary_direction = {
|
||||||
|
(boundary.start_vertex, boundary.end_vertex): (boundary, True)
|
||||||
|
for boundary in boundaries
|
||||||
|
}
|
||||||
|
boundary_direction.update(
|
||||||
|
{
|
||||||
|
(boundary.end_vertex, boundary.start_vertex): (boundary, False)
|
||||||
|
for boundary in boundaries
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def dfs(node, start, path, visited, current_loops):
|
||||||
|
max_length = 6
|
||||||
|
if len(path) > max_length:
|
||||||
|
return
|
||||||
|
|
||||||
|
visited.add(node)
|
||||||
|
path.append(node)
|
||||||
|
|
||||||
|
for neighbor in graph[node]:
|
||||||
|
# 2条边在拓扑结构上是一摸一样的,没办法通过拓扑办法识别最小圈
|
||||||
|
if neighbor == start and len(path) > 2:
|
||||||
|
# 找到一个环路,创建对应的环路(每条边是一个元组,包含boundary和方向)
|
||||||
|
loop = Loop()
|
||||||
|
for i in range(len(path) - 1):
|
||||||
|
start_vertex = path[i]
|
||||||
|
end_vertex = path[i + 1]
|
||||||
|
if (start_vertex, end_vertex) in boundary_direction:
|
||||||
|
boundary, direction = boundary_direction[
|
||||||
|
(start_vertex, end_vertex)
|
||||||
|
]
|
||||||
|
loop.add_edge(boundary, direction)
|
||||||
|
else:
|
||||||
|
boundary, direction = boundary_direction[
|
||||||
|
(end_vertex, start_vertex)
|
||||||
|
]
|
||||||
|
loop.add_edge(boundary, direction)
|
||||||
|
|
||||||
|
# 处理闭合边(path[-1] 到 path[0])
|
||||||
|
start_vertex = path[-1]
|
||||||
|
end_vertex = path[0]
|
||||||
|
if (start_vertex, end_vertex) in boundary_direction:
|
||||||
|
boundary, direction = boundary_direction[(start_vertex, end_vertex)]
|
||||||
|
loop.add_edge(boundary, direction)
|
||||||
|
else:
|
||||||
|
boundary, direction = boundary_direction[(end_vertex, start_vertex)]
|
||||||
|
loop.add_edge(boundary, direction)
|
||||||
|
|
||||||
|
current_loops.append(loop)
|
||||||
|
elif neighbor not in visited:
|
||||||
|
dfs(neighbor, start, path, visited, current_loops)
|
||||||
|
|
||||||
|
path.pop()
|
||||||
|
visited.remove(node)
|
||||||
|
|
||||||
|
loops = [] # 所有环路
|
||||||
|
for node in graph:
|
||||||
|
current_loops = [] # 用来存储当前节点发现的环路
|
||||||
|
dfs(node, node, [], set(), current_loops)
|
||||||
|
loops.extend(current_loops) # 将当前节点的环路加入到结果中
|
||||||
|
|
||||||
|
# 去重环路
|
||||||
|
unique_loops = set(loops) # 使用 set 去重环路
|
||||||
|
|
||||||
|
# 删除长度超过6的
|
||||||
|
unique_loops = {loop for loop in unique_loops if len(loop.boundaries) <= 6}
|
||||||
|
|
||||||
|
# 如果一个环路,他有弦,则删掉
|
||||||
|
to_be_delete_loops = set()
|
||||||
|
|
||||||
|
# 获取连续1 2 3条boundary的映射,并且要标记属于哪个loop的
|
||||||
|
start_v_and_end_v_to_loops_of_length = [
|
||||||
|
defaultdict(set) for _ in range(3)
|
||||||
|
] # 0是长度为1的,1为长度为2的,2为长度为3的,元素为字典,key是两个顶点,value是相关的loop
|
||||||
|
for loop in unique_loops:
|
||||||
|
verts = loop.controll_verts
|
||||||
|
for vert_idx, vert in enumerate(verts):
|
||||||
|
for offset in range(1, 4): # 1~3
|
||||||
|
vert_1 = verts[vert_idx]
|
||||||
|
vert_2 = verts[(vert_idx + offset) % len(verts)]
|
||||||
|
|
||||||
|
sorted_start_v_and_end_v_tuple = tuple(
|
||||||
|
sorted([vert_1, vert_2], key=lambda v: v.index)
|
||||||
|
)
|
||||||
|
start_v_and_end_v_to_loops_of_length[offset - 1][
|
||||||
|
sorted_start_v_and_end_v_tuple
|
||||||
|
].add(loop)
|
||||||
|
|
||||||
|
# # debug形状
|
||||||
|
# print(len(start_v_and_end_v_to_loops_of_length))
|
||||||
|
# for i, start_v_and_end_v_to_loops in enumerate(
|
||||||
|
# start_v_and_end_v_to_loops_of_length
|
||||||
|
# ):
|
||||||
|
# print(f"长度{i+1}")
|
||||||
|
# for (vert_1, vert_2), loops in start_v_and_end_v_to_loops.items():
|
||||||
|
# print(len(loops))
|
||||||
|
|
||||||
|
for loop in unique_loops:
|
||||||
|
if loop.length <= 3: # 小于3不检查弦
|
||||||
|
continue
|
||||||
|
|
||||||
|
offset_range = loop.length - 3
|
||||||
|
|
||||||
|
loop_controll_verts = loop.controll_verts
|
||||||
|
|
||||||
|
for offset in range(1, offset_range + 1):
|
||||||
|
# 隔一个顶点就检查1条的,隔两个顶点就检查1和2条的,隔三个就检查123条的
|
||||||
|
# offset就是boundary的条数
|
||||||
|
|
||||||
|
# 遍历两两不相邻的顶点
|
||||||
|
for i in range(len(loop_controll_verts)):
|
||||||
|
for j in range(len(loop_controll_verts)):
|
||||||
|
# 确保 j 不等于 i,并且 j 不是 i 的相邻边界
|
||||||
|
if (
|
||||||
|
i != j
|
||||||
|
and (j != (i + 1) % len(loop_controll_verts))
|
||||||
|
and (j != (i - 1) % len(loop_controll_verts))
|
||||||
|
):
|
||||||
|
vert_1 = loop_controll_verts[i]
|
||||||
|
vert_2 = loop_controll_verts[j]
|
||||||
|
sorted_start_v_and_end_v_tuple = tuple(
|
||||||
|
sorted([vert_1, vert_2], key=lambda v: v.index)
|
||||||
|
)
|
||||||
|
# 隔一个顶点就检查1条的,2和3跳过
|
||||||
|
# 如果 i 和 j 隔了 1 个顶点,跳过 offset 为 1 和 2
|
||||||
|
distance = min(
|
||||||
|
abs(i - j), len(loop_controll_verts) - abs(i - j)
|
||||||
|
) # 0 1距离1
|
||||||
|
|
||||||
|
# print(loop.length, distance, offset)
|
||||||
|
|
||||||
|
if distance == 2 and (offset == 2 or offset == 3):
|
||||||
|
continue # 跳过 offset 为 1 或 2 的情况
|
||||||
|
|
||||||
|
# 如果 i 和 j 隔了 2 个顶点,跳过 offset 为 2
|
||||||
|
if distance == 3 and offset == 2:
|
||||||
|
continue # 跳过 offset 为 2 的情况
|
||||||
|
|
||||||
|
related_loops = start_v_and_end_v_to_loops_of_length[
|
||||||
|
offset - 1
|
||||||
|
].get(sorted_start_v_and_end_v_tuple)
|
||||||
|
if not related_loops:
|
||||||
|
continue
|
||||||
|
# 排除自身的影响,并且用-号而不用remove()这样搞不会导致start_v_and_end_v_to_loops_of_length被修改
|
||||||
|
remaining_loops = related_loops - {loop}
|
||||||
|
if remaining_loops:
|
||||||
|
to_be_delete_loops.add(loop)
|
||||||
|
# TODO:一旦被认为是弦,则对该loop的循环就要退出了,但是层数太多了
|
||||||
|
break
|
||||||
|
|
||||||
|
# # 从所有的loops中删除外层loop
|
||||||
|
unique_loops.difference_update(to_be_delete_loops)
|
||||||
|
|
||||||
|
# 防止重建网格
|
||||||
|
updated_loops = []
|
||||||
|
if not is_force_refresh:
|
||||||
|
loop_tobe_assign = set()
|
||||||
|
for loop in unique_loops:
|
||||||
|
if loop in g.loops:
|
||||||
|
continue
|
||||||
|
g.loops.append(loop)
|
||||||
|
updated_loops.append(loop)
|
||||||
|
else:
|
||||||
|
g.loops = unique_loops
|
||||||
|
updated_loops = unique_loops
|
||||||
|
|
||||||
|
# print(len(g.loops))
|
||||||
|
# for loop in g.loops:
|
||||||
|
# print("---")
|
||||||
|
# print(loop.shape)
|
||||||
|
# for b, d in loop.b_and_d_list:
|
||||||
|
# print(b.start_vertex.index, b.end_vertex.index, d)
|
||||||
|
|
||||||
|
for updated_loop in updated_loops:
|
||||||
|
updated_loop.recreate_pattern()
|
||||||
|
|
||||||
|
return updated_loops
|
||||||
318
loop.py
Normal file
318
loop.py
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import bmesh
|
||||||
|
import mathutils
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from . import pattern
|
||||||
|
from .pattern import pattern_list
|
||||||
|
from . import g
|
||||||
|
|
||||||
|
|
||||||
|
class Loop:
|
||||||
|
def __init__(self):
|
||||||
|
self.b_and_d_list = [] # 存储边界和方向的元组 (boundary, direction)
|
||||||
|
|
||||||
|
self.bvh_tree = None
|
||||||
|
self.pattern = None # create要调用到,没声明报错
|
||||||
|
self.mid_co = mathutils.Vector((0, 0, 0))
|
||||||
|
|
||||||
|
self.solver_msg = ""
|
||||||
|
self.suggest_solution = []
|
||||||
|
self.solver_constraint_list = [
|
||||||
|
None for _ in range(8)
|
||||||
|
] # 丑陋储存法,用None作占位符,现存的pattern最多有八个约束(6个padding 2个x y 附加边流)
|
||||||
|
self.solver_constraint_rotation = None
|
||||||
|
self.solver_constraint_pattern = None
|
||||||
|
|
||||||
|
def add_edge(self, boundary, direction):
|
||||||
|
self.b_and_d_list.append((boundary, direction))
|
||||||
|
self.update_mid_co()
|
||||||
|
|
||||||
|
def update_mid_co(self):
|
||||||
|
self.mid_co = sum(
|
||||||
|
[b.mid_co for b in self.boundaries], mathutils.Vector()
|
||||||
|
) / len(self.boundaries)
|
||||||
|
|
||||||
|
def normalize(self):
|
||||||
|
"""标准化环路,将每条边的顶点索引进行排序,忽略边的方向"""
|
||||||
|
return sorted(
|
||||||
|
[
|
||||||
|
(id(boundary), boundary) # Sort by the unique id of the boundary object
|
||||||
|
for boundary, _ in self.b_and_d_list
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""重载 '==' 运算符,用来进行环路set的去重"""
|
||||||
|
return self.normalize() == other.normalize()
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
"""重载 '__hash__' 使其能够作为字典的键进行去重"""
|
||||||
|
return hash(tuple(self.normalize()))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Loop({self.b_and_d_list})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def length(self):
|
||||||
|
return len(self.b_and_d_list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self):
|
||||||
|
# 这个是指边数
|
||||||
|
return [len(b.verts) - 1 for b, _ in self.b_and_d_list]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def boundaries(self):
|
||||||
|
return [b for b, _ in self.b_and_d_list]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def boundary_verts_walk(self):
|
||||||
|
# 有重复,用于createmesh的时候walk,即每个boundary的头尾都是重叠的
|
||||||
|
verts = []
|
||||||
|
for b, d in self.b_and_d_list:
|
||||||
|
if d:
|
||||||
|
boundary_verts = b.verts
|
||||||
|
else:
|
||||||
|
boundary_verts = b.verts[::-1]
|
||||||
|
for v in boundary_verts:
|
||||||
|
verts.append(v)
|
||||||
|
|
||||||
|
return verts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def verts(self):
|
||||||
|
# 无重复顶点
|
||||||
|
verts = []
|
||||||
|
seen = set() # 用来跟踪已访问的顶点
|
||||||
|
|
||||||
|
for b, d in self.b_and_d_list:
|
||||||
|
if d:
|
||||||
|
boundary_verts = b.verts # 如果方向为真,按顺序获取顶点
|
||||||
|
else:
|
||||||
|
boundary_verts = b.verts[::-1] # 如果方向为假,反转顶点顺序
|
||||||
|
|
||||||
|
# 遍历当前边界的顶点
|
||||||
|
for v in boundary_verts:
|
||||||
|
if v not in seen: # 如果顶点未被访问过
|
||||||
|
verts.append(v)
|
||||||
|
seen.add(v) # 标记该顶点为已访问
|
||||||
|
|
||||||
|
return verts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def controll_verts(self):
|
||||||
|
# 即start和end的列表,无重复顶点
|
||||||
|
controll_verts = []
|
||||||
|
|
||||||
|
for idx, (b, d) in enumerate(self.b_and_d_list):
|
||||||
|
if d:
|
||||||
|
vert_1 = b.start_vertex
|
||||||
|
vert_2 = b.end_vertex
|
||||||
|
else:
|
||||||
|
vert_1 = b.end_vertex
|
||||||
|
vert_2 = b.start_vertex
|
||||||
|
|
||||||
|
controll_verts.append(vert_1)
|
||||||
|
|
||||||
|
return controll_verts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def edges(self):
|
||||||
|
edges = []
|
||||||
|
for b, d in self.b_and_d_list:
|
||||||
|
if d:
|
||||||
|
boundary_edges = b.edges
|
||||||
|
else:
|
||||||
|
boundary_edges = b.edges[::-1]
|
||||||
|
|
||||||
|
for e in boundary_edges:
|
||||||
|
edges.append(e)
|
||||||
|
|
||||||
|
return edges
|
||||||
|
|
||||||
|
# @property
|
||||||
|
# def controll_verts(self):
|
||||||
|
# # 控制点,即每个boundary的start end
|
||||||
|
# return set(v for b in self.boundaries for v in b.verts)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_solution(self):
|
||||||
|
if self.pattern and self.pattern.solution:
|
||||||
|
return self.pattern.solution
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_bvh_tree(self):
|
||||||
|
temp_bm = bmesh.new()
|
||||||
|
|
||||||
|
temp_bm_verts = []
|
||||||
|
for b in self.boundaries:
|
||||||
|
for vert in b.verts:
|
||||||
|
vert = temp_bm.verts.new(vert.co)
|
||||||
|
temp_bm_verts.append(vert)
|
||||||
|
|
||||||
|
# 确保新创建的顶点的索引是正确的
|
||||||
|
temp_bm.verts.index_update()
|
||||||
|
temp_bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
temp_bm.faces.new(temp_bm_verts) # ngon
|
||||||
|
|
||||||
|
bvh_tree = mathutils.bvhtree.BVHTree.FromBMesh(temp_bm)
|
||||||
|
self.bvh_tree = bvh_tree
|
||||||
|
|
||||||
|
temp_bm.free()
|
||||||
|
|
||||||
|
def recreate_pattern(self):
|
||||||
|
# 尝试创建pattern
|
||||||
|
|
||||||
|
self.remove_pattern()
|
||||||
|
try:
|
||||||
|
pattern_instance = pattern.recreate_pattern(self)
|
||||||
|
except Exception as e:
|
||||||
|
pattern_instance = None
|
||||||
|
error_info = traceback.format_exc()
|
||||||
|
g.running_exception = e
|
||||||
|
|
||||||
|
bmesh.update_edit_mesh(g.obj_me)
|
||||||
|
|
||||||
|
self.pattern = pattern_instance
|
||||||
|
|
||||||
|
g.is_redraw_pattern = True
|
||||||
|
return self.pattern
|
||||||
|
|
||||||
|
def remove_pattern(self):
|
||||||
|
if self.pattern:
|
||||||
|
self.pattern.remove_mesh()
|
||||||
|
self.pattern = None
|
||||||
|
|
||||||
|
def get_pattern_constraint(self):
|
||||||
|
# 获取当前pattern,如果有约束则返回约束,无约束则返回当前pattern实例,如无解则返回None
|
||||||
|
if self.solver_constraint_pattern:
|
||||||
|
return self.solver_constraint_pattern
|
||||||
|
elif self.pattern:
|
||||||
|
# 不应返回pattern实例,后面还要copy,有些属性无法复制
|
||||||
|
for i in pattern.pattern_list:
|
||||||
|
if i.name == self.pattern.name:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
def change_solver_constraint_rotation(self):
|
||||||
|
current_pattern = self.get_pattern_constraint()
|
||||||
|
if not current_pattern:
|
||||||
|
return
|
||||||
|
|
||||||
|
rotation = self.solver_constraint_rotation
|
||||||
|
if rotation is None:
|
||||||
|
if self.pattern:
|
||||||
|
new_rotation = self.pattern.rotation + 1
|
||||||
|
else:
|
||||||
|
new_rotation = 0
|
||||||
|
else:
|
||||||
|
new_rotation = rotation + 1
|
||||||
|
new_rotation = new_rotation % len(self.boundaries)
|
||||||
|
|
||||||
|
self.solver_constraint_rotation = new_rotation
|
||||||
|
self.solver_constraint_pattern = current_pattern
|
||||||
|
self.recreate_pattern()
|
||||||
|
|
||||||
|
def next_solver_constraint_pattern(self, is_auto_search=True):
|
||||||
|
current_pattern = self.get_pattern_constraint()
|
||||||
|
|
||||||
|
# 初始化约束,设置pattern约束
|
||||||
|
boundary_num = len(self.boundaries)
|
||||||
|
pattern_list_current = [
|
||||||
|
i for i in pattern_list if boundary_num == i.boundary_num
|
||||||
|
]
|
||||||
|
self.init_solver_constraint()
|
||||||
|
if current_pattern is None:
|
||||||
|
loop_start_idx = 0
|
||||||
|
else:
|
||||||
|
loop_start_idx = next(
|
||||||
|
i
|
||||||
|
for i, obj in enumerate(pattern_list_current)
|
||||||
|
if obj.name == current_pattern.name
|
||||||
|
)
|
||||||
|
|
||||||
|
for pattern in (
|
||||||
|
pattern_list_current[loop_start_idx + 1 :]
|
||||||
|
+ pattern_list_current[:loop_start_idx]
|
||||||
|
): # 不跳过第一个,会导致无法切换到下一个
|
||||||
|
self.solver_constraint_pattern = pattern
|
||||||
|
result = self.recreate_pattern()
|
||||||
|
if not is_auto_search:
|
||||||
|
# 不是自动模式,则拿到第一个就溜
|
||||||
|
return
|
||||||
|
|
||||||
|
if result is not None:
|
||||||
|
return
|
||||||
|
# 找不到,回到原处
|
||||||
|
self.solver_constraint_pattern = current_pattern
|
||||||
|
self.recreate_pattern()
|
||||||
|
|
||||||
|
def init_solver_constraint(
|
||||||
|
self, is_init_list=True, is_init_rotation=True, is_init_pattern=True
|
||||||
|
):
|
||||||
|
# 另外两个约束不能单独保留
|
||||||
|
if is_init_pattern:
|
||||||
|
is_init_list = True
|
||||||
|
is_init_rotation = True
|
||||||
|
|
||||||
|
if is_init_list:
|
||||||
|
self.solver_constraint_list = [None for _ in range(len(self.shape) + 2)]
|
||||||
|
if is_init_rotation:
|
||||||
|
self.solver_constraint_rotation = None
|
||||||
|
|
||||||
|
if is_init_pattern:
|
||||||
|
self.solver_constraint_pattern = None
|
||||||
|
|
||||||
|
self.recreate_pattern()
|
||||||
|
|
||||||
|
def change_solver_constraint_list(self, key, change):
|
||||||
|
current_pattern = self.get_pattern_constraint()
|
||||||
|
|
||||||
|
if not current_pattern:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.solver_constraint_pattern and not self.pattern:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(key, int):
|
||||||
|
solution_idx = key
|
||||||
|
else:
|
||||||
|
edge_var_count = len(current_pattern.additional_edge_flow_info)
|
||||||
|
if key == "x" and edge_var_count >= 1:
|
||||||
|
solution_idx = len(self.shape) - 1 + 1
|
||||||
|
elif key == "y" and edge_var_count >= 2:
|
||||||
|
solution_idx = len(self.shape) - 1 + 2
|
||||||
|
elif key == "z" and edge_var_count >= 3:
|
||||||
|
solution_idx = len(self.shape) - 1 + 3
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 是否有约束,没有再找solution
|
||||||
|
solver_constraint_list = self.solver_constraint_list
|
||||||
|
old_value = solver_constraint_list[solution_idx]
|
||||||
|
|
||||||
|
if old_value is None:
|
||||||
|
current_solution = self.current_solution
|
||||||
|
if current_solution:
|
||||||
|
old_value = current_solution[solution_idx]
|
||||||
|
if old_value is None:
|
||||||
|
old_value = 0
|
||||||
|
|
||||||
|
new_value = old_value + change
|
||||||
|
if new_value < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
upper = max(self.shape)
|
||||||
|
if new_value > upper:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.solver_constraint_list[solution_idx] = new_value
|
||||||
|
self.solver_constraint_pattern = current_pattern
|
||||||
|
self.recreate_pattern()
|
||||||
|
|
||||||
|
def get_boundary_index(self, boundary_input):
|
||||||
|
for idx, b in enumerate(self.boundaries):
|
||||||
|
if b == boundary_input:
|
||||||
|
return idx
|
||||||
708
op.py
Normal file
708
op.py
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import bpy
|
||||||
|
import bmesh
|
||||||
|
from bpy_extras import view3d_utils
|
||||||
|
import time
|
||||||
|
import mathutils
|
||||||
|
|
||||||
|
|
||||||
|
from . import const, g
|
||||||
|
from .utils import functions
|
||||||
|
from .draw import draw
|
||||||
|
from . import boundary
|
||||||
|
from . import generate_loop
|
||||||
|
from .utils.trans import t
|
||||||
|
|
||||||
|
|
||||||
|
class EasyPatchModalOperator(bpy.types.Operator):
|
||||||
|
bl_idname = f"{const.addon_prefix}.start"
|
||||||
|
bl_label = "Start Patching"
|
||||||
|
bl_options = {"REGISTER", "UNDO"} # 不加UNDO撤回的话,会导致用户前一个步骤也撤回
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
try:
|
||||||
|
if context.area:
|
||||||
|
context.area.tag_redraw()
|
||||||
|
|
||||||
|
result = self.process_undo(context, event)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# start_timestamp = time.time()
|
||||||
|
# process drag
|
||||||
|
result = self.process_drawing_verts(context, event)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
# drawing_verts_consume_time = time.time() - start_timestamp
|
||||||
|
|
||||||
|
# process stop
|
||||||
|
result = self.process_stop(context, event)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# start_timestamp = time.time()
|
||||||
|
result = self.process_running(context, event)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
# running_consume_time = time.time() - start_timestamp
|
||||||
|
|
||||||
|
# start_timestamp = time.time()
|
||||||
|
result = self.process_loop(context, event)
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
# process_loop_consume_time = time.time() - start_timestamp
|
||||||
|
|
||||||
|
# start_timestamp = time.time()
|
||||||
|
update_hover(context, event)
|
||||||
|
# update_hover_consume_time = time.time() - start_timestamp
|
||||||
|
|
||||||
|
# last_echo_time = g.last_echo_time if g.last_echo_time else 0
|
||||||
|
# if time.time() - last_echo_time > 0.1:
|
||||||
|
# print(
|
||||||
|
# drawing_verts_consume_time,
|
||||||
|
# running_consume_time,
|
||||||
|
# process_loop_consume_time,
|
||||||
|
# update_hover_consume_time,
|
||||||
|
# )
|
||||||
|
# g.last_echo_time = time.time()
|
||||||
|
|
||||||
|
#
|
||||||
|
g.last_mouse_co_2d = mathutils.Vector(
|
||||||
|
(event.mouse_region_x, event.mouse_region_y)
|
||||||
|
)
|
||||||
|
return self.process_ui_block(context, event)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
functions.safe_execute(self.clear, context)
|
||||||
|
functions.log(f"An expection raise when modal, exit modal. Error: {str(e)}")
|
||||||
|
self.report(
|
||||||
|
{"WARNING"},
|
||||||
|
f"An expection raise when modal, exit modal. Error: {str(e)}",
|
||||||
|
)
|
||||||
|
EasyPatchModalOperator.set_stop_flag()
|
||||||
|
raise e
|
||||||
|
|
||||||
|
return {"PASS_THROUGH"}
|
||||||
|
# return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if not g.is_running:
|
||||||
|
self.start(context)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
else:
|
||||||
|
EasyPatchModalOperator.stop()
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
def start(self, context):
|
||||||
|
functions.log("modal start")
|
||||||
|
|
||||||
|
g.clear_all_variables()
|
||||||
|
|
||||||
|
draw.start(self)
|
||||||
|
g.is_running = True
|
||||||
|
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
|
||||||
|
self.context = context
|
||||||
|
self.region = context.region
|
||||||
|
self.rv3d = context.region_data
|
||||||
|
|
||||||
|
obj = functions.get_edit_obj()
|
||||||
|
g.obj = obj
|
||||||
|
g.obj_me = obj.data
|
||||||
|
g.obj_bm = bmesh.from_edit_mesh(obj.data)
|
||||||
|
|
||||||
|
g.mode = g.Mode.NORMAL
|
||||||
|
g.drawing_verts_snap_vertex = None
|
||||||
|
g.drawing_path_boundary = None
|
||||||
|
|
||||||
|
g.snap_bvh_trees = functions.get_snap_bvh_trees()
|
||||||
|
g.obj_bvh_tree = functions.get_obj_bvh_tree()
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
functions.log("modal stop")
|
||||||
|
EasyPatchModalOperator.set_stop_flag()
|
||||||
|
|
||||||
|
def clear(self, context, remove_mesh_flag=True):
|
||||||
|
functions.log("modal clear")
|
||||||
|
|
||||||
|
# 用户tab离开编辑模式,会导致bm被删除,所以会报很多错
|
||||||
|
|
||||||
|
if remove_mesh_flag:
|
||||||
|
functions.safe_execute(self.remove_mesh)
|
||||||
|
|
||||||
|
functions.safe_execute(context.window_manager.event_timer_remove, self._timer)
|
||||||
|
functions.safe_execute(EasyPatchModalOperator.set_stop_flag)
|
||||||
|
functions.safe_execute(draw.clear)
|
||||||
|
|
||||||
|
if context.object.mode == "EDIT":
|
||||||
|
bmesh.update_edit_mesh(g.obj_me)
|
||||||
|
|
||||||
|
g.clear_all_variables()
|
||||||
|
|
||||||
|
def set_stop_flag():
|
||||||
|
g.is_running = False
|
||||||
|
|
||||||
|
# 删除插件生成的网格
|
||||||
|
def remove_mesh(self):
|
||||||
|
if g.temp_bm_edges:
|
||||||
|
functions.remove_bm_edges(g.obj_bm, g.temp_bm_edges)
|
||||||
|
g.temp_bm_edges = set()
|
||||||
|
|
||||||
|
if g.temp_bm_verts:
|
||||||
|
functions.delete_bm_verts(g.obj_bm, g.temp_bm_verts)
|
||||||
|
g.temp_bm_verts = set()
|
||||||
|
|
||||||
|
for loop in g.loops:
|
||||||
|
if loop.pattern:
|
||||||
|
loop.pattern.remove_mesh()
|
||||||
|
|
||||||
|
try:
|
||||||
|
bmesh.update_edit_mesh(g.obj_me)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def process_stop(self, context, event):
|
||||||
|
if (
|
||||||
|
not g.is_running
|
||||||
|
or event.type == "ESC"
|
||||||
|
or context.object.mode != "EDIT"
|
||||||
|
or g.obj_bm is None
|
||||||
|
):
|
||||||
|
self.clear(context)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
if event.type in ["RET", "NUMPAD_ENTER"]:
|
||||||
|
count_vertex = functions.get_created_verts_num()
|
||||||
|
|
||||||
|
self.clear(context, remove_mesh_flag=False)
|
||||||
|
self.report({"INFO"}, f"{count_vertex} {t('verts')} {t('created')}")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
if g.running_exception:
|
||||||
|
raise g.running_exception
|
||||||
|
|
||||||
|
def process_drawing_verts(self, context, event):
|
||||||
|
# TODO: update snap和hovering和append_from_2d加一个移动阈值才更新,记录上一个鼠标位置,超过阈值才进行,modal末尾更新,共用一个值
|
||||||
|
# TODO: loop加上一个屏蔽pattern生成的状态
|
||||||
|
functions.update_snap_vertex(event, is_snap_opposite=g.is_snap_opposite)
|
||||||
|
|
||||||
|
if g.mode == g.Mode.DRAWING:
|
||||||
|
# # update and clear snap vertex when ctrl in running
|
||||||
|
functions.update_solid_path(context, event)
|
||||||
|
|
||||||
|
# append路径
|
||||||
|
g.drawing_path_boundary.append_path_from_2d(
|
||||||
|
context,
|
||||||
|
(event.mouse_region_x, event.mouse_region_y),
|
||||||
|
is_snap_surface=g.is_snap_surface,
|
||||||
|
is_snap_opposite=g.is_snap_opposite,
|
||||||
|
)
|
||||||
|
|
||||||
|
# drawing中改变点数
|
||||||
|
if event.type == "WHEELUPMOUSE":
|
||||||
|
g.drawing_path_boundary.length += 1
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
if event.type == "WHEELDOWNMOUSE":
|
||||||
|
g.drawing_path_boundary.length -= 1
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# stop drag
|
||||||
|
if event.type == "LEFTMOUSE" and event.value == "RELEASE":
|
||||||
|
# # 第六条强制闭合
|
||||||
|
# if len(self.patch.boundaries) == 5:
|
||||||
|
# g.drawing_verts_snap_vertex = self.patch.start_vertex
|
||||||
|
|
||||||
|
snap_vert = (
|
||||||
|
g.drawing_verts_snap_vertex
|
||||||
|
) # 确保一致性,先获取,防止中间修改
|
||||||
|
functions.log("stop drag vertex:" + str(snap_vert))
|
||||||
|
|
||||||
|
done_boundary = g.drawing_path_boundary.done_from_temp(snap_vert)
|
||||||
|
|
||||||
|
g.drawing_verts_snap_vertex = None
|
||||||
|
g.mode = g.Mode.NORMAL
|
||||||
|
|
||||||
|
# clear drag things
|
||||||
|
g.drawing_path_boundary = None
|
||||||
|
|
||||||
|
if not done_boundary:
|
||||||
|
return
|
||||||
|
|
||||||
|
if done_boundary.path[-1] != done_boundary.end_vertex.co:
|
||||||
|
# 不知道为什么,end_vertex是吸附的时候需要手动补
|
||||||
|
done_boundary.path.append(
|
||||||
|
g.obj.matrix_world @ done_boundary.end_vertex.co
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_loop.regenerate_loops()
|
||||||
|
|
||||||
|
if not g.is_drawing_along_mirror_persist:
|
||||||
|
g.is_drawing_along_mirror = False # 每次画完都重置这个变量
|
||||||
|
|
||||||
|
# 删除temp boundary
|
||||||
|
if event.type == "RIGHTMOUSE" and event.value == "PRESS":
|
||||||
|
g.drawing_path_boundary = None
|
||||||
|
g.mode = g.Mode.NORMAL
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# if event.value != "NOTHING":
|
||||||
|
# print(event.type, event.value)
|
||||||
|
|
||||||
|
# TODO: 以CLICK_DRAG,用户可能会以左键单击误触发,造成多余的线条,多一个ctrl+enter,以保留多余线条
|
||||||
|
# start drag
|
||||||
|
if event.type == "LEFTMOUSE" and event.value == "CLICK_DRAG":
|
||||||
|
functions.log("start drag vertex:" + str(g.drawing_verts_snap_vertex))
|
||||||
|
g.mode = g.Mode.DRAWING
|
||||||
|
|
||||||
|
# create temp boundary
|
||||||
|
if g.drawing_verts_snap_vertex:
|
||||||
|
g.drawing_path_boundary = boundary.Boundary.create_temp(
|
||||||
|
g.drawing_verts_snap_vertex
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
g.drawing_path_boundary = boundary.Boundary.create_temp(None)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"RUNNING_MODAL"
|
||||||
|
} # block drag ui operation, if not running_modal, release will not trigger
|
||||||
|
|
||||||
|
def process_undo(self, context, event):
|
||||||
|
if event.type == "Z" and event.value == "PRESS" and event.ctrl:
|
||||||
|
|
||||||
|
self.report({"WARNING"}, t("you_cant_undo_here"))
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def process_ui_block(self, context, event):
|
||||||
|
# I键内插,会导致未知问题
|
||||||
|
if event.type in ["X", "I"]:
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
return {"PASS_THROUGH"}
|
||||||
|
|
||||||
|
def process_loop(self, context, event):
|
||||||
|
if not g.hovering_loop:
|
||||||
|
return
|
||||||
|
|
||||||
|
loop = g.hovering_loop
|
||||||
|
|
||||||
|
# R
|
||||||
|
if event.type == "R" and event.value == "PRESS":
|
||||||
|
loop.change_solver_constraint_rotation()
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# T
|
||||||
|
if event.type == "T" and event.value == "PRESS":
|
||||||
|
loop.next_solver_constraint_pattern(is_auto_search=g.is_auto_search_pattern)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# invert normal
|
||||||
|
if event.type == "I" and event.value == "PRESS" and loop.pattern:
|
||||||
|
functions.faces_normal_toward_viewport(g.obj, g.obj_bm, loop.pattern.faces)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# 改变padding约束
|
||||||
|
if (
|
||||||
|
event.shift
|
||||||
|
and (event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE")
|
||||||
|
and g.hovering_boundary
|
||||||
|
):
|
||||||
|
boundary_index = loop.get_boundary_index(g.hovering_boundary)
|
||||||
|
if event.type == "WHEELUPMOUSE":
|
||||||
|
loop.change_solver_constraint_list(boundary_index, 1)
|
||||||
|
if event.type == "WHEELDOWNMOUSE":
|
||||||
|
loop.change_solver_constraint_list(boundary_index, -1)
|
||||||
|
|
||||||
|
# X附加边流
|
||||||
|
if event.type == "X" and event.value == "PRESS" and event.shift:
|
||||||
|
loop.change_solver_constraint_list("x", 1)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
if event.type == "X" and event.value == "PRESS" and event.ctrl:
|
||||||
|
loop.change_solver_constraint_list("x", -1)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# Y附加边流
|
||||||
|
if event.type == "Y" and event.value == "PRESS" and event.shift:
|
||||||
|
loop.change_solver_constraint_list("y", 1)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
if event.type == "Y" and event.value == "PRESS" and event.ctrl:
|
||||||
|
loop.change_solver_constraint_list("y", -1)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# C 清除约束
|
||||||
|
if event.type == "C" and event.value == "PRESS":
|
||||||
|
if event.shift:
|
||||||
|
loop.init_solver_constraint(
|
||||||
|
is_init_list=True, is_init_rotation=False, is_init_pattern=False
|
||||||
|
)
|
||||||
|
elif event.ctrl:
|
||||||
|
loop.init_solver_constraint(
|
||||||
|
is_init_list=False, is_init_rotation=True, is_init_pattern=False
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
loop.init_solver_constraint()
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
def process_running(self, context, event):
|
||||||
|
if g.mode != g.Mode.NORMAL:
|
||||||
|
return
|
||||||
|
# 边缘建成后,改变长度
|
||||||
|
if event.ctrl and (
|
||||||
|
event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE"
|
||||||
|
):
|
||||||
|
# boundary = functions.get_nearest_boundary_from_mouse(event)
|
||||||
|
boundary = g.hovering_boundary
|
||||||
|
if boundary:
|
||||||
|
if event.type == "WHEELUPMOUSE":
|
||||||
|
boundary.length += 1
|
||||||
|
if event.type == "WHEELDOWNMOUSE":
|
||||||
|
boundary.length -= 1
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# S
|
||||||
|
if event.type == "S" and event.value == "PRESS":
|
||||||
|
g.is_snap_surface = not g.is_snap_surface # 切换吸附状态
|
||||||
|
|
||||||
|
if g.hovering_loop and g.hovering_loop.pattern:
|
||||||
|
if g.is_snap_surface:
|
||||||
|
# 吸附曲面
|
||||||
|
functions.snap_verts_to_surface_along_normal(
|
||||||
|
g.obj,
|
||||||
|
g.obj_bm,
|
||||||
|
g.hovering_loop.pattern.interior_verts,
|
||||||
|
g.snap_bvh_trees,
|
||||||
|
allow_opposite_direction=g.is_snap_opposite,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
functions.smooth_verts_by_normal(
|
||||||
|
g.obj, g.obj_bm, g.hovering_loop.pattern.interior_verts
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
if event.type == "M" and event.value == "PRESS" and not event.ctrl:
|
||||||
|
g.is_drawing_along_mirror = (
|
||||||
|
not g.is_drawing_along_mirror
|
||||||
|
) # 切换镜像绘制状态
|
||||||
|
|
||||||
|
if not g.is_drawing_along_mirror:
|
||||||
|
g.is_drawing_along_mirror_persist = False
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# 删除boundary
|
||||||
|
if (
|
||||||
|
event.type == "X"
|
||||||
|
and event.value == "PRESS"
|
||||||
|
and not event.shift
|
||||||
|
and not event.ctrl
|
||||||
|
and g.hovering_boundary
|
||||||
|
):
|
||||||
|
g.hovering_boundary.delete()
|
||||||
|
generate_loop.regenerate_loops()
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
if event.type == "M" and event.value == "PRESS" and event.ctrl:
|
||||||
|
g.is_drawing_along_mirror_persist = (
|
||||||
|
not g.is_drawing_along_mirror_persist
|
||||||
|
) # 切换镜像绘制状态
|
||||||
|
|
||||||
|
g.is_drawing_along_mirror = g.is_drawing_along_mirror_persist
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
# smooth
|
||||||
|
if event.shift:
|
||||||
|
tobe_smooth_loops = []
|
||||||
|
if event.ctrl:
|
||||||
|
tobe_smooth_loops = g.loops
|
||||||
|
else:
|
||||||
|
if g.hovering_loop:
|
||||||
|
tobe_smooth_loops = [g.hovering_loop]
|
||||||
|
|
||||||
|
for tobe_smooth_loop in tobe_smooth_loops:
|
||||||
|
if (
|
||||||
|
not tobe_smooth_loop.pattern
|
||||||
|
or not tobe_smooth_loop.pattern.interior_verts
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
to_be_smooth_verts = tobe_smooth_loop.pattern.interior_verts
|
||||||
|
|
||||||
|
if event.type in ["ONE", "NUMPAD_1"]:
|
||||||
|
functions.smooth_verts_by_avg(g.obj, g.obj_bm, to_be_smooth_verts)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
elif event.type in ["TWO", "NUMPAD_2"]:
|
||||||
|
functions.smooth_verts_by_normal(
|
||||||
|
g.obj,
|
||||||
|
g.obj_bm,
|
||||||
|
to_be_smooth_verts,
|
||||||
|
smooth_type=0,
|
||||||
|
)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
elif event.type in ["THREE", "NUMPAD_3"]:
|
||||||
|
functions.smooth_verts_by_normal(
|
||||||
|
g.obj,
|
||||||
|
g.obj_bm,
|
||||||
|
to_be_smooth_verts,
|
||||||
|
smooth_type=1,
|
||||||
|
)
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
# self.is_snap_surface = False
|
||||||
|
|
||||||
|
# v snap oppose,目前只针对path绘制的时候应用
|
||||||
|
if event.type == "V" and event.value == "PRESS" and g.is_running:
|
||||||
|
if g.is_snap_opposite:
|
||||||
|
g.is_snap_opposite = False
|
||||||
|
else:
|
||||||
|
g.is_snap_opposite = True
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
if event.type == "B" and event.value == "PRESS":
|
||||||
|
g.is_auto_search_pattern = not g.is_auto_search_pattern
|
||||||
|
return {"RUNNING_MODAL"}
|
||||||
|
|
||||||
|
|
||||||
|
class EasyPatchSnapSelectedToSurfaceOperator(bpy.types.Operator):
|
||||||
|
bl_idname = f"{const.addon_prefix}.snap_selected_to_surface"
|
||||||
|
bl_label = "Snap Selected Vertices to Surface"
|
||||||
|
# bl_description = t("toggle_driver")
|
||||||
|
bl_options = {"REGISTER", "UNDO"} # 不加UNDO撤回的话,会导致用户前一个步骤也撤回
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
obj = context.object
|
||||||
|
|
||||||
|
# 检查是否在编辑模式
|
||||||
|
if obj.mode != "EDIT":
|
||||||
|
self.report({"WARNING"}, "Please run this in edit mode")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# 获取依赖图
|
||||||
|
depsgraph = context.evaluated_depsgraph_get()
|
||||||
|
|
||||||
|
# 获取网格数据
|
||||||
|
bm = bmesh.from_edit_mesh(obj.data)
|
||||||
|
bm.verts.ensure_lookup_table()
|
||||||
|
|
||||||
|
# 获取所有选中的顶点
|
||||||
|
selected_vertices = [v for v in bm.verts if v.select]
|
||||||
|
if not selected_vertices:
|
||||||
|
self.report({"WARNING"}, "No Vertices selected")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# 获取所有可见的网格对象(排除当前对象)
|
||||||
|
visible_objects = [
|
||||||
|
o
|
||||||
|
for o in context.visible_objects
|
||||||
|
if o.type == "MESH" and o != obj and o.visible_get()
|
||||||
|
]
|
||||||
|
|
||||||
|
if not visible_objects:
|
||||||
|
self.report({"WARNING"}, "no_visible_target_mesh_to_snap")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# 构建所有可见目标对象的 BVH 树(一次性操作)
|
||||||
|
bvh_trees = functions.get_snap_bvh_trees()
|
||||||
|
|
||||||
|
functions.snap_verts_to_surface_along_normal(
|
||||||
|
obj,
|
||||||
|
bm,
|
||||||
|
selected_vertices,
|
||||||
|
bvh_trees,
|
||||||
|
allow_opposite_direction=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 将顶点更改应用到网格上,loop_triangles提高效率
|
||||||
|
bmesh.update_edit_mesh(obj.data, loop_triangles=True)
|
||||||
|
|
||||||
|
# self.report({"INFO"}, f"Successfully snap {snap_count} vertices")
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_nearest_boundary_and_loop_by_co_2d(context, co_2d):
|
||||||
|
nearest_boundary = None
|
||||||
|
nearest_loop = None
|
||||||
|
min_distance = float("inf")
|
||||||
|
|
||||||
|
# 获取当前视图区域和区域的3D视图
|
||||||
|
region = context.region
|
||||||
|
rv3d = context.space_data.region_3d
|
||||||
|
|
||||||
|
# # mid_co方案
|
||||||
|
# # 遍历boundary列表
|
||||||
|
# for boundary in g.boundaries:
|
||||||
|
# # 获取boundary的mid_co三维坐标
|
||||||
|
# mid_co = boundary.mid_co
|
||||||
|
|
||||||
|
# # 将三维坐标投影到2D屏幕坐标
|
||||||
|
# screen_coord = view3d_utils.location_3d_to_region_2d(region, rv3d, mid_co)
|
||||||
|
|
||||||
|
# # 如果投影成功
|
||||||
|
# if screen_coord:
|
||||||
|
# # 计算鼠标位置与投影点的距离
|
||||||
|
# dist = (screen_coord[0] - co_2d[0]) ** 2 + (screen_coord[1] - co_2d[1]) ** 2
|
||||||
|
# if dist < min_distance:
|
||||||
|
# min_distance = dist
|
||||||
|
# nearest_boundary = boundary
|
||||||
|
|
||||||
|
# TODO: 只有一个edge的用mid_co,不然重叠了捕获不到
|
||||||
|
# 遍历boundary列表
|
||||||
|
for boundary in g.boundaries:
|
||||||
|
coords = [v.co for v in boundary.verts] + [boundary.mid_co]
|
||||||
|
# 遍历boundary的每一个顶点
|
||||||
|
for vert_co in coords:
|
||||||
|
# 将三维坐标投影到2D屏幕坐标
|
||||||
|
screen_coord = view3d_utils.location_3d_to_region_2d(region, rv3d, vert_co)
|
||||||
|
|
||||||
|
# 如果投影成功
|
||||||
|
if screen_coord:
|
||||||
|
# 计算鼠标位置与投影点的距离
|
||||||
|
dist = (screen_coord[0] - co_2d[0]) ** 2 + (
|
||||||
|
screen_coord[1] - co_2d[1]
|
||||||
|
) ** 2
|
||||||
|
if dist < min_distance:
|
||||||
|
min_distance = dist
|
||||||
|
nearest_boundary = boundary
|
||||||
|
|
||||||
|
if nearest_boundary:
|
||||||
|
# 获取与nearest_boundary相关的loop
|
||||||
|
connected_loops = functions.get_loops_from_boundary(nearest_boundary)
|
||||||
|
# print(nearest_boundary, connected_loops)
|
||||||
|
|
||||||
|
if len(connected_loops) == 0:
|
||||||
|
nearest_loop = None
|
||||||
|
elif len(connected_loops) == 1:
|
||||||
|
nearest_loop = connected_loops[0]
|
||||||
|
else:
|
||||||
|
# 找到距离鼠标位置最近的loop
|
||||||
|
min_loop_distance = float("inf")
|
||||||
|
|
||||||
|
for loop in connected_loops:
|
||||||
|
# 获取loop的mid_co三维坐标
|
||||||
|
loop_mid_co = loop.mid_co
|
||||||
|
|
||||||
|
# 将loop的三维坐标投影到2D屏幕坐标
|
||||||
|
loop_screen_coord = view3d_utils.location_3d_to_region_2d(
|
||||||
|
region, rv3d, loop_mid_co
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果loop投影成功
|
||||||
|
if loop_screen_coord:
|
||||||
|
# 计算鼠标位置与loop的投影点的距离
|
||||||
|
loop_dist = (loop_screen_coord[0] - co_2d[0]) ** 2 + (
|
||||||
|
loop_screen_coord[1] - co_2d[1]
|
||||||
|
) ** 2
|
||||||
|
if loop_dist < min_loop_distance:
|
||||||
|
min_loop_distance = loop_dist
|
||||||
|
nearest_loop = loop
|
||||||
|
|
||||||
|
return nearest_boundary, nearest_loop
|
||||||
|
|
||||||
|
|
||||||
|
def get_nearest_boundary_and_loop_by_co_3d(co_3d):
|
||||||
|
nearest_boundary = None
|
||||||
|
nearest_loop = None
|
||||||
|
min_distance = float("inf")
|
||||||
|
|
||||||
|
# # mid_co方案
|
||||||
|
# # 遍历boundary列表
|
||||||
|
# for boundary in g.boundaries:
|
||||||
|
# mid_co = boundary.mid_co
|
||||||
|
|
||||||
|
# # 计算给定的3D坐标与boundary的mid_co之间的欧几里得距离
|
||||||
|
# distance = (mid_co - co_3d).length
|
||||||
|
|
||||||
|
# # 判断是否是最近的boundary
|
||||||
|
# if distance < min_distance:
|
||||||
|
# min_distance = distance
|
||||||
|
# nearest_boundary = boundary
|
||||||
|
|
||||||
|
# verts方案
|
||||||
|
for boundary in g.boundaries:
|
||||||
|
coords = [v.co for v in boundary.verts] + [boundary.mid_co]
|
||||||
|
# 遍历boundary的每一个顶点
|
||||||
|
for vert_co in coords:
|
||||||
|
|
||||||
|
# 计算给定的3D坐标与顶点坐标之间的欧几里得距离
|
||||||
|
distance = (vert_co - co_3d).length
|
||||||
|
|
||||||
|
# 判断是否是最近的boundary
|
||||||
|
if distance < min_distance:
|
||||||
|
min_distance = distance
|
||||||
|
nearest_boundary = boundary
|
||||||
|
|
||||||
|
if nearest_boundary:
|
||||||
|
connected_loops = functions.get_loops_from_boundary(nearest_boundary)
|
||||||
|
if len(connected_loops) == 0:
|
||||||
|
nearest_loop = None
|
||||||
|
elif len(connected_loops) == 1:
|
||||||
|
nearest_loop = connected_loops[0]
|
||||||
|
else:
|
||||||
|
# 多个loop的情况,计算每个loop的mid_co和目标co_3d之间的3D距离
|
||||||
|
min_loop_distance = float("inf")
|
||||||
|
|
||||||
|
for loop in connected_loops:
|
||||||
|
loop_mid_co = loop.mid_co
|
||||||
|
|
||||||
|
# 计算给定的3D坐标与loop的mid_co之间的欧几里得距离
|
||||||
|
loop_distance = (loop_mid_co - co_3d).length
|
||||||
|
|
||||||
|
# 判断是否是最近的loop
|
||||||
|
if loop_distance < min_loop_distance:
|
||||||
|
min_loop_distance = loop_distance
|
||||||
|
nearest_loop = loop
|
||||||
|
|
||||||
|
return nearest_boundary, nearest_loop
|
||||||
|
|
||||||
|
|
||||||
|
def update_hover(context, event):
|
||||||
|
if g.is_changing_mesh:
|
||||||
|
g.hovering_boundary = None
|
||||||
|
g.hovering_loop = None
|
||||||
|
|
||||||
|
g.is_redraw_hovering = True
|
||||||
|
return
|
||||||
|
|
||||||
|
current_timestamp = time.time()
|
||||||
|
if current_timestamp - g.last_update_hovering_timestamp < (1 / 10):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 两个方案,1.boundary的mid_co投影到2d,获取最近的,然后loop挑出b相邻的,再根据mid_co取2d最近的 2.ray_cast,没结果则用方案一,有结果,取距离该点最近的boundary的mid_co,然后再相邻取loop
|
||||||
|
|
||||||
|
region = context.region
|
||||||
|
rv3d = context.region_data
|
||||||
|
mouse_co_2d = (event.mouse_region_x, event.mouse_region_y)
|
||||||
|
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, mouse_co_2d)
|
||||||
|
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, mouse_co_2d)
|
||||||
|
|
||||||
|
closest_co = None
|
||||||
|
distance_min = float("inf")
|
||||||
|
for bvh_tree in g.snap_bvh_trees:
|
||||||
|
ray_cast_co, _, _, distance = bvh_tree.ray_cast(ray_origin, ray_direction)
|
||||||
|
if distance and distance < distance_min:
|
||||||
|
distance_min = distance
|
||||||
|
closest_co = ray_cast_co
|
||||||
|
|
||||||
|
hovering_boundary = None
|
||||||
|
hovering_loop = None
|
||||||
|
if closest_co:
|
||||||
|
hovering_boundary, hovering_loop = get_nearest_boundary_and_loop_by_co_3d(
|
||||||
|
closest_co
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# boundary
|
||||||
|
hovering_boundary, hovering_loop = get_nearest_boundary_and_loop_by_co_2d(
|
||||||
|
context, mouse_co_2d
|
||||||
|
)
|
||||||
|
|
||||||
|
g.hovering_boundary = hovering_boundary
|
||||||
|
g.hovering_loop = hovering_loop
|
||||||
|
|
||||||
|
g.last_update_hovering_timestamp = current_timestamp
|
||||||
|
|
||||||
|
g.is_redraw_hovering = True
|
||||||
|
|
||||||
|
|
||||||
|
classes = (EasyPatchModalOperator, EasyPatchSnapSelectedToSurfaceOperator)
|
||||||
1662
pattern.py
Normal file
1662
pattern.py
Normal file
File diff suppressed because it is too large
Load Diff
23
preference.py
Normal file
23
preference.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import bpy
|
||||||
|
from .utils.trans import t
|
||||||
|
from .utils import functions
|
||||||
|
|
||||||
|
|
||||||
|
class EasyPatchPrefs(bpy.types.AddonPreferences):
|
||||||
|
bl_idname = functions.get_package_name() # 設置爲package的名字否則不識別
|
||||||
|
|
||||||
|
desc_position_x: bpy.props.IntProperty(name=t("font_size"), default=5, min=0, max=100) # type: ignore
|
||||||
|
desc_position_y: bpy.props.IntProperty(name=t("font_size"), default=15, min=0, max=100) # type: ignore
|
||||||
|
|
||||||
|
# TODO:经典模式开关,注意要在g建立一个变量确保一致性
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.label(text=t("hotkey_display_position"))
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
row.prop(self, "desc_position_x", text="x", expand=True)
|
||||||
|
row.prop(self, "desc_position_y", text="y", expand=True)
|
||||||
|
|
||||||
|
|
||||||
|
classes = (EasyPatchPrefs,)
|
||||||
20
property.py
Normal file
20
property.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import bpy
|
||||||
|
from .utils.trans import t
|
||||||
|
|
||||||
|
|
||||||
|
def update_snap_obj(self, context):
|
||||||
|
if self.snap_obj == context.active_object:
|
||||||
|
self.snap_obj = None
|
||||||
|
|
||||||
|
|
||||||
|
class EasyPatchProperty(bpy.types.PropertyGroup):
|
||||||
|
snap_obj: bpy.props.PointerProperty(
|
||||||
|
type=bpy.types.Object,
|
||||||
|
description=t(
|
||||||
|
"snap_obj_setting_desc",
|
||||||
|
),
|
||||||
|
update=update_snap_obj,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
property_classes = (EasyPatchProperty,)
|
||||||
49
ui.py
Normal file
49
ui.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from . import const, g
|
||||||
|
import bpy
|
||||||
|
from . import op
|
||||||
|
from .utils.trans import t
|
||||||
|
|
||||||
|
|
||||||
|
class EasyPatchMainPanel(bpy.types.Panel):
|
||||||
|
bl_label = "Easy Patch"
|
||||||
|
bl_idname = "VIEW3D_PT_OIMOYU_EASY_Patch_MAIN_PANEL"
|
||||||
|
bl_space_type = "VIEW_3D"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_context = "mesh_edit"
|
||||||
|
bl_options = {"DEFAULT_CLOSED"}
|
||||||
|
|
||||||
|
bl_category = "Edit"
|
||||||
|
bl_order = 1
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
|
||||||
|
if g.is_running:
|
||||||
|
layout.row().operator(
|
||||||
|
op.EasyPatchModalOperator.bl_idname,
|
||||||
|
text=t("stop") + " Easy Patch",
|
||||||
|
icon="PAUSE",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
layout.prop(
|
||||||
|
context.scene.easy_patch_properties,
|
||||||
|
"snap_obj",
|
||||||
|
text="",
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.row().operator(
|
||||||
|
op.EasyPatchModalOperator.bl_idname,
|
||||||
|
text=t("start") + " Easy Patch",
|
||||||
|
icon="PLAY",
|
||||||
|
)
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
|
||||||
|
layout.row().operator(
|
||||||
|
op.EasyPatchSnapSelectedToSurfaceOperator.bl_idname,
|
||||||
|
text=t("snap_selected_to_surface"),
|
||||||
|
icon="MOD_SHRINKWRAP",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
classes = (EasyPatchMainPanel,)
|
||||||
1145
utils/functions.py
Normal file
1145
utils/functions.py
Normal file
File diff suppressed because it is too large
Load Diff
41
utils/pulp/__init__.py
Normal file
41
utils/pulp/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.20
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id: __init__.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Module file that imports all of the pulp functions
|
||||||
|
|
||||||
|
Copyright 2007- Stuart Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
"""
|
||||||
|
from .constants import VERSION
|
||||||
|
|
||||||
|
from .pulp import *
|
||||||
|
from .apis import *
|
||||||
|
from .utilities import *
|
||||||
|
from .constants import *
|
||||||
|
from .tests import pulpTestAll
|
||||||
|
|
||||||
|
__doc__ = pulp.__doc__
|
||||||
|
__version__ = VERSION
|
||||||
165
utils/pulp/apis/__init__.py
Normal file
165
utils/pulp/apis/__init__.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
from .coin_api import *
|
||||||
|
from .cplex_api import *
|
||||||
|
from .gurobi_api import *
|
||||||
|
from .glpk_api import *
|
||||||
|
from .choco_api import *
|
||||||
|
from .mipcl_api import *
|
||||||
|
from .mosek_api import *
|
||||||
|
from .scip_api import *
|
||||||
|
from .xpress_api import *
|
||||||
|
from .highs_api import *
|
||||||
|
from .copt_api import *
|
||||||
|
from .core import *
|
||||||
|
|
||||||
|
_all_solvers = [
|
||||||
|
GLPK_CMD,
|
||||||
|
PYGLPK,
|
||||||
|
CPLEX_CMD,
|
||||||
|
CPLEX_PY,
|
||||||
|
GUROBI,
|
||||||
|
GUROBI_CMD,
|
||||||
|
MOSEK,
|
||||||
|
XPRESS,
|
||||||
|
XPRESS_CMD,
|
||||||
|
XPRESS_PY,
|
||||||
|
PULP_CBC_CMD,
|
||||||
|
COIN_CMD,
|
||||||
|
COINMP_DLL,
|
||||||
|
CHOCO_CMD,
|
||||||
|
MIPCL_CMD,
|
||||||
|
SCIP_CMD,
|
||||||
|
FSCIP_CMD,
|
||||||
|
SCIP_PY,
|
||||||
|
HiGHS,
|
||||||
|
HiGHS_CMD,
|
||||||
|
COPT,
|
||||||
|
COPT_DLL,
|
||||||
|
COPT_CMD,
|
||||||
|
]
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Default solver selection
|
||||||
|
if PULP_CBC_CMD().available():
|
||||||
|
LpSolverDefault = PULP_CBC_CMD()
|
||||||
|
elif GLPK_CMD().available():
|
||||||
|
LpSolverDefault = GLPK_CMD()
|
||||||
|
elif COIN_CMD().available():
|
||||||
|
LpSolverDefault = COIN_CMD()
|
||||||
|
else:
|
||||||
|
LpSolverDefault = None
|
||||||
|
|
||||||
|
|
||||||
|
def setConfigInformation(**keywords):
|
||||||
|
"""
|
||||||
|
set the data in the configuration file
|
||||||
|
at the moment will only edit things in [locations]
|
||||||
|
the keyword value pairs come from the keywords dictionary
|
||||||
|
"""
|
||||||
|
# TODO: extend if we ever add another section in the config file
|
||||||
|
# read the old configuration
|
||||||
|
config = Parser()
|
||||||
|
config.read(config_filename)
|
||||||
|
# set the new keys
|
||||||
|
for key, val in keywords.items():
|
||||||
|
config.set("locations", key, val)
|
||||||
|
# write the new configuration
|
||||||
|
fp = open(config_filename, "w")
|
||||||
|
config.write(fp)
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
|
||||||
|
def configSolvers():
|
||||||
|
"""
|
||||||
|
Configure the path the the solvers on the command line
|
||||||
|
|
||||||
|
Designed to configure the file locations of the solvers from the
|
||||||
|
command line after installation
|
||||||
|
"""
|
||||||
|
configlist = [
|
||||||
|
(cplex_dll_path, "cplexpath", "CPLEX: "),
|
||||||
|
(coinMP_path, "coinmppath", "CoinMP dll (windows only): "),
|
||||||
|
]
|
||||||
|
print(
|
||||||
|
"Please type the full path including filename and extension \n"
|
||||||
|
+ "for each solver available"
|
||||||
|
)
|
||||||
|
configdict = {}
|
||||||
|
for default, key, msg in configlist:
|
||||||
|
value = input(msg + "[" + str(default) + "]")
|
||||||
|
if value:
|
||||||
|
configdict[key] = value
|
||||||
|
setConfigInformation(**configdict)
|
||||||
|
|
||||||
|
|
||||||
|
def getSolver(solver, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Instantiates a solver from its name
|
||||||
|
|
||||||
|
:param str solver: solver name to create
|
||||||
|
:param args: additional arguments to the solver
|
||||||
|
:param kwargs: additional keyword arguments to the solver
|
||||||
|
:return: solver of type :py:class:`LpSolver`
|
||||||
|
"""
|
||||||
|
mapping = {k.name: k for k in _all_solvers}
|
||||||
|
try:
|
||||||
|
return mapping[solver](*args, **kwargs)
|
||||||
|
except KeyError:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"The solver {} does not exist in PuLP.\nPossible options are: \n{}".format(
|
||||||
|
solver, mapping.keys()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def getSolverFromDict(data):
|
||||||
|
"""
|
||||||
|
Instantiates a solver from a dictionary with its data
|
||||||
|
|
||||||
|
:param dict data: a dictionary with, at least an "solver" key with the name
|
||||||
|
of the solver to create
|
||||||
|
:return: a solver of type :py:class:`LpSolver`
|
||||||
|
:raises PulpSolverError: if the dictionary does not have the "solver" key
|
||||||
|
:rtype: LpSolver
|
||||||
|
"""
|
||||||
|
solver = data.pop("solver", None)
|
||||||
|
if solver is None:
|
||||||
|
raise PulpSolverError("The json file has no solver attribute.")
|
||||||
|
return getSolver(solver, **data)
|
||||||
|
|
||||||
|
|
||||||
|
def getSolverFromJson(filename):
|
||||||
|
"""
|
||||||
|
Instantiates a solver from a json file with its data
|
||||||
|
|
||||||
|
:param str filename: name of the json file to read
|
||||||
|
:return: a solver of type :py:class:`LpSolver`
|
||||||
|
:rtype: LpSolver
|
||||||
|
"""
|
||||||
|
with open(filename) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return getSolverFromDict(data)
|
||||||
|
|
||||||
|
|
||||||
|
def listSolvers(onlyAvailable=False):
|
||||||
|
"""
|
||||||
|
List the names of all the existing solvers in PuLP
|
||||||
|
|
||||||
|
:param bool onlyAvailable: if True, only show the available solvers
|
||||||
|
:return: list of solver names
|
||||||
|
:rtype: list
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for s in _all_solvers:
|
||||||
|
solver = s()
|
||||||
|
if (not onlyAvailable) or solver.available():
|
||||||
|
result.append(solver.name)
|
||||||
|
del solver
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# DEPRECATED aliases:
|
||||||
|
get_solver = getSolver
|
||||||
|
get_solver_from_json = getSolverFromJson
|
||||||
|
get_solver_from_dict = getSolverFromDict
|
||||||
|
list_solvers = listSolvers
|
||||||
158
utils/pulp/apis/choco_api.py
Normal file
158
utils/pulp/apis/choco_api.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, subprocess, PulpSolverError
|
||||||
|
import os
|
||||||
|
from .. import constants
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class CHOCO_CMD(LpSolver_CMD):
|
||||||
|
"""The CHOCO_CMD solver"""
|
||||||
|
|
||||||
|
name = "CHOCO_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
keepFiles=False,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("choco-parsers-with-dependencies.jar")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
java_path = self.executableExtension("java")
|
||||||
|
return self.executable(self.path) and self.executable(java_path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
java_path = self.executableExtension("java")
|
||||||
|
if not self.executable(java_path):
|
||||||
|
raise PulpSolverError(
|
||||||
|
"PuLP: java needs to be installed and accesible in order to use CHOCO_CMD"
|
||||||
|
)
|
||||||
|
if not os.path.exists(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpMps, tmpLp, tmpSol = self.create_tmp_files(lp.name, "mps", "lp", "sol")
|
||||||
|
# just to report duplicated variables:
|
||||||
|
lp.checkDuplicateVars()
|
||||||
|
|
||||||
|
lp.writeMPS(tmpMps, mpsSense=lp.sense)
|
||||||
|
try:
|
||||||
|
os.remove(tmpSol)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
cmd = java_path + ' -cp "' + self.path + '" org.chocosolver.parser.mps.ChocoMPS'
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
cmd += f" -tl {self.timeLimit}" * 1000
|
||||||
|
cmd += " " + " ".join([f"{key} {value}" for key, value in self.options])
|
||||||
|
cmd += f" {tmpMps}"
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
cmd += " -max"
|
||||||
|
if lp.isMIP():
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn("CHOCO_CMD cannot solve the relaxation of a problem")
|
||||||
|
# we always get the output to a file.
|
||||||
|
# if not, we cannot read it afterwards
|
||||||
|
# (we thus ignore the self.msg parameter)
|
||||||
|
pipe = open(tmpSol, "w")
|
||||||
|
|
||||||
|
return_code = subprocess.call(cmd, stdout=pipe, stderr=pipe, shell=True)
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
status = constants.LpStatusNotSolved
|
||||||
|
status_sol = constants.LpSolutionNoSolutionFound
|
||||||
|
values = None
|
||||||
|
else:
|
||||||
|
status, values, status_sol = self.readsol(tmpSol)
|
||||||
|
self.delete_tmp_files(tmpMps, tmpLp, tmpSol)
|
||||||
|
|
||||||
|
lp.assignStatus(status, status_sol)
|
||||||
|
if status not in [constants.LpStatusInfeasible, constants.LpStatusNotSolved]:
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(filename):
|
||||||
|
"""Read a Choco solution file"""
|
||||||
|
# TODO: figure out the unbounded status in choco solver
|
||||||
|
chocoStatus = {
|
||||||
|
"OPTIMUM FOUND": constants.LpStatusOptimal,
|
||||||
|
"SATISFIABLE": constants.LpStatusOptimal,
|
||||||
|
"UNSATISFIABLE": constants.LpStatusInfeasible,
|
||||||
|
"UNKNOWN": constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
chocoSolStatus = {
|
||||||
|
"OPTIMUM FOUND": constants.LpSolutionOptimal,
|
||||||
|
"SATISFIABLE": constants.LpSolutionIntegerFeasible,
|
||||||
|
"UNSATISFIABLE": constants.LpSolutionInfeasible,
|
||||||
|
"UNKNOWN": constants.LpSolutionNoSolutionFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
status = constants.LpStatusNotSolved
|
||||||
|
sol_status = constants.LpSolutionNoSolutionFound
|
||||||
|
values = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
content = f.readlines()
|
||||||
|
content = [l.strip() for l in content if l[:2] not in ["o ", "c "]]
|
||||||
|
if not len(content):
|
||||||
|
return status, values, sol_status
|
||||||
|
if content[0][:2] == "s ":
|
||||||
|
status_str = content[0][2:]
|
||||||
|
status = chocoStatus[status_str]
|
||||||
|
sol_status = chocoSolStatus[status_str]
|
||||||
|
for line in content[1:]:
|
||||||
|
name, value = line.split()
|
||||||
|
values[name] = float(value)
|
||||||
|
|
||||||
|
return status, values, sol_status
|
||||||
873
utils/pulp/apis/coin_api.py
Normal file
873
utils/pulp/apis/coin_api.py
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
|
||||||
|
from .core import cbc_path, pulp_cbc_path, coinMP_path, devnull, operating_system
|
||||||
|
import os
|
||||||
|
from .. import constants
|
||||||
|
from tempfile import mktemp
|
||||||
|
import ctypes
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class COIN_CMD(LpSolver_CMD):
|
||||||
|
"""The COIN CLP/CBC LP solver
|
||||||
|
now only uses cbc
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "COIN_CMD"
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension(cbc_path)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
fracGap=None,
|
||||||
|
maxSeconds=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
presolve=None,
|
||||||
|
cuts=None,
|
||||||
|
strong=None,
|
||||||
|
options=None,
|
||||||
|
warmStart=False,
|
||||||
|
keepFiles=False,
|
||||||
|
path=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
timeMode="elapsed",
|
||||||
|
mip_start=False,
|
||||||
|
maxNodes=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool warmStart: if True, the solver will use the current value of variables as a start
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param bool presolve: if True, adds presolve on
|
||||||
|
:param bool cuts: if True, adds gomory on knapsack on probing on
|
||||||
|
:param bool strong: if True, adds strong
|
||||||
|
:param float fracGap: deprecated for gapRel
|
||||||
|
:param float maxSeconds: deprecated for timeLimit
|
||||||
|
:param str timeMode: "elapsed": count wall-time to timeLimit; "cpu": count cpu-time
|
||||||
|
:param bool mip_start: deprecated for warmStart
|
||||||
|
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if fracGap is not None:
|
||||||
|
warnings.warn("Parameter fracGap is being depreciated for gapRel")
|
||||||
|
if gapRel is not None:
|
||||||
|
warnings.warn("Parameter gapRel and fracGap passed, using gapRel")
|
||||||
|
else:
|
||||||
|
gapRel = fracGap
|
||||||
|
if maxSeconds is not None:
|
||||||
|
warnings.warn("Parameter maxSeconds is being depreciated for timeLimit")
|
||||||
|
if timeLimit is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter timeLimit and maxSeconds passed, using timeLimit"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
timeLimit = maxSeconds
|
||||||
|
if mip_start:
|
||||||
|
warnings.warn("Parameter mip_start is being depreciated for warmStart")
|
||||||
|
if warmStart:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter mipStart and mip_start passed, using warmStart"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warmStart = mip_start
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
presolve=presolve,
|
||||||
|
cuts=cuts,
|
||||||
|
strong=strong,
|
||||||
|
options=options,
|
||||||
|
warmStart=warmStart,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
threads=threads,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
logPath=logPath,
|
||||||
|
timeMode=timeMode,
|
||||||
|
maxNodes=maxNodes,
|
||||||
|
)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""Make a copy of self"""
|
||||||
|
aCopy = LpSolver_CMD.copy(self)
|
||||||
|
aCopy.optionsDict = self.optionsDict
|
||||||
|
return aCopy
|
||||||
|
|
||||||
|
def actualSolve(self, lp, **kwargs):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
return self.solve_CBC(lp, **kwargs)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def solve_CBC(self, lp, use_mps=True):
|
||||||
|
"""Solve a MIP problem using CBC"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError(
|
||||||
|
f"Pulp: cannot execute {self.path} cwd: {os.getcwd()}"
|
||||||
|
)
|
||||||
|
tmpLp, tmpMps, tmpSol, tmpMst = self.create_tmp_files(
|
||||||
|
lp.name, "lp", "mps", "sol", "mst"
|
||||||
|
)
|
||||||
|
if use_mps:
|
||||||
|
vs, variablesNames, constraintsNames, objectiveName = lp.writeMPS(
|
||||||
|
tmpMps, rename=1
|
||||||
|
)
|
||||||
|
cmds = " " + tmpMps + " "
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
cmds += "-max "
|
||||||
|
else:
|
||||||
|
vs = lp.writeLP(tmpLp)
|
||||||
|
# In the Lp we do not create new variable or constraint names:
|
||||||
|
variablesNames = {v.name: v.name for v in vs}
|
||||||
|
constraintsNames = {c: c for c in lp.constraints}
|
||||||
|
cmds = " " + tmpLp + " "
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
self.writesol(tmpMst, lp, vs, variablesNames, constraintsNames)
|
||||||
|
cmds += f"-mips {tmpMst} "
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
cmds += f"-sec {self.timeLimit} "
|
||||||
|
options = self.options + self.getOptions()
|
||||||
|
for option in options:
|
||||||
|
cmds += "-" + option + " "
|
||||||
|
if self.mip:
|
||||||
|
cmds += "-branch "
|
||||||
|
else:
|
||||||
|
cmds += "-initialSolve "
|
||||||
|
cmds += "-printingOptions all "
|
||||||
|
cmds += "-solution " + tmpSol + " "
|
||||||
|
if self.msg:
|
||||||
|
pipe = None
|
||||||
|
else:
|
||||||
|
pipe = open(os.devnull, "w")
|
||||||
|
logPath = self.optionsDict.get("logPath")
|
||||||
|
if logPath:
|
||||||
|
if self.msg:
|
||||||
|
warnings.warn(
|
||||||
|
"`logPath` argument replaces `msg=1`. The output will be redirected to the log file."
|
||||||
|
)
|
||||||
|
pipe = open(self.optionsDict["logPath"], "w")
|
||||||
|
log.debug(self.path + cmds)
|
||||||
|
args = []
|
||||||
|
args.append(self.path)
|
||||||
|
args.extend(cmds[1:].split())
|
||||||
|
if not self.msg and operating_system == "win":
|
||||||
|
# Prevent flashing windows if used from a GUI application
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
cbc = subprocess.Popen(
|
||||||
|
args, stdout=pipe, stderr=pipe, stdin=devnull, startupinfo=startupinfo
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cbc = subprocess.Popen(args, stdout=pipe, stderr=pipe, stdin=devnull)
|
||||||
|
if cbc.wait() != 0:
|
||||||
|
if pipe:
|
||||||
|
pipe.close()
|
||||||
|
raise PulpSolverError(
|
||||||
|
"Pulp: Error while trying to execute, use msg=True for more details"
|
||||||
|
+ self.path
|
||||||
|
)
|
||||||
|
if pipe:
|
||||||
|
pipe.close()
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
raise PulpSolverError("Pulp: Error while executing " + self.path)
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
values,
|
||||||
|
reducedCosts,
|
||||||
|
shadowPrices,
|
||||||
|
slacks,
|
||||||
|
sol_status,
|
||||||
|
) = self.readsol_MPS(tmpSol, lp, vs, variablesNames, constraintsNames)
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
lp.assignVarsDj(reducedCosts)
|
||||||
|
lp.assignConsPi(shadowPrices)
|
||||||
|
lp.assignConsSlack(slacks, activity=True)
|
||||||
|
lp.assignStatus(status, sol_status)
|
||||||
|
self.delete_tmp_files(tmpMps, tmpLp, tmpSol, tmpMst)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def getOptions(self):
|
||||||
|
params_eq = dict(
|
||||||
|
gapRel="ratio {}",
|
||||||
|
gapAbs="allow {}",
|
||||||
|
threads="threads {}",
|
||||||
|
presolve="presolve on",
|
||||||
|
strong="strong {}",
|
||||||
|
cuts="gomory on knapsack on probing on",
|
||||||
|
timeMode="timeMode {}",
|
||||||
|
maxNodes="maxNodes {}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
v.format(self.optionsDict[k])
|
||||||
|
for k, v in params_eq.items()
|
||||||
|
if self.optionsDict.get(k) is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
def readsol_MPS(
|
||||||
|
self, filename, lp, vs, variablesNames, constraintsNames, objectiveName=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Read a CBC solution file generated from an mps or lp file (possible different names)
|
||||||
|
"""
|
||||||
|
values = {v.name: 0 for v in vs}
|
||||||
|
|
||||||
|
reverseVn = {v: k for k, v in variablesNames.items()}
|
||||||
|
reverseCn = {v: k for k, v in constraintsNames.items()}
|
||||||
|
|
||||||
|
reducedCosts = {}
|
||||||
|
shadowPrices = {}
|
||||||
|
slacks = {}
|
||||||
|
status, sol_status = self.get_status(filename)
|
||||||
|
with open(filename) as f:
|
||||||
|
for l in f:
|
||||||
|
if len(l) <= 2:
|
||||||
|
break
|
||||||
|
l = l.split()
|
||||||
|
# incase the solution is infeasible
|
||||||
|
if l[0] == "**":
|
||||||
|
l = l[1:]
|
||||||
|
vn = l[1]
|
||||||
|
val = l[2]
|
||||||
|
dj = l[3]
|
||||||
|
if vn in reverseVn:
|
||||||
|
values[reverseVn[vn]] = float(val)
|
||||||
|
reducedCosts[reverseVn[vn]] = float(dj)
|
||||||
|
if vn in reverseCn:
|
||||||
|
slacks[reverseCn[vn]] = float(val)
|
||||||
|
shadowPrices[reverseCn[vn]] = float(dj)
|
||||||
|
return status, values, reducedCosts, shadowPrices, slacks, sol_status
|
||||||
|
|
||||||
|
def writesol(self, filename, lp, vs, variablesNames, constraintsNames):
|
||||||
|
"""
|
||||||
|
Writes a CBC solution file generated from an mps / lp file (possible different names)
|
||||||
|
returns True on success
|
||||||
|
"""
|
||||||
|
values = {v.name: v.value() if v.value() is not None else 0 for v in vs}
|
||||||
|
value_lines = []
|
||||||
|
value_lines += [
|
||||||
|
(i, v, values[k], 0) for i, (k, v) in enumerate(variablesNames.items())
|
||||||
|
]
|
||||||
|
lines = ["Stopped on time - objective value 0\n"]
|
||||||
|
lines += ["{:>7} {} {:>15} {:>23}\n".format(*tup) for tup in value_lines]
|
||||||
|
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def readsol_LP(self, filename, lp, vs):
|
||||||
|
"""
|
||||||
|
Read a CBC solution file generated from an lp (good names)
|
||||||
|
returns status, values, reducedCosts, shadowPrices, slacks, sol_status
|
||||||
|
"""
|
||||||
|
variablesNames = {v.name: v.name for v in vs}
|
||||||
|
constraintsNames = {c: c for c in lp.constraints}
|
||||||
|
return self.readsol_MPS(filename, lp, vs, variablesNames, constraintsNames)
|
||||||
|
|
||||||
|
def get_status(self, filename):
|
||||||
|
cbcStatus = {
|
||||||
|
"Optimal": constants.LpStatusOptimal,
|
||||||
|
"Infeasible": constants.LpStatusInfeasible,
|
||||||
|
"Integer": constants.LpStatusInfeasible,
|
||||||
|
"Unbounded": constants.LpStatusUnbounded,
|
||||||
|
"Stopped": constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
cbcSolStatus = {
|
||||||
|
"Optimal": constants.LpSolutionOptimal,
|
||||||
|
"Infeasible": constants.LpSolutionInfeasible,
|
||||||
|
"Unbounded": constants.LpSolutionUnbounded,
|
||||||
|
"Stopped": constants.LpSolutionNoSolutionFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
statusstrs = f.readline().split()
|
||||||
|
|
||||||
|
status = cbcStatus.get(statusstrs[0], constants.LpStatusUndefined)
|
||||||
|
sol_status = cbcSolStatus.get(
|
||||||
|
statusstrs[0], constants.LpSolutionNoSolutionFound
|
||||||
|
)
|
||||||
|
# here we could use some regex expression.
|
||||||
|
# Not sure what's more desirable
|
||||||
|
if status == constants.LpStatusNotSolved and len(statusstrs) >= 5:
|
||||||
|
if statusstrs[4] == "objective":
|
||||||
|
status = constants.LpStatusOptimal
|
||||||
|
sol_status = constants.LpSolutionIntegerFeasible
|
||||||
|
return status, sol_status
|
||||||
|
|
||||||
|
|
||||||
|
COIN = COIN_CMD
|
||||||
|
|
||||||
|
|
||||||
|
class PULP_CBC_CMD(COIN_CMD):
|
||||||
|
"""
|
||||||
|
This solver uses a precompiled version of cbc provided with the package
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "PULP_CBC_CMD"
|
||||||
|
pulp_cbc_path = pulp_cbc_path
|
||||||
|
try:
|
||||||
|
if os.name != "nt":
|
||||||
|
if not os.access(pulp_cbc_path, os.X_OK):
|
||||||
|
import stat
|
||||||
|
|
||||||
|
os.chmod(pulp_cbc_path, stat.S_IXUSR + stat.S_IXOTH)
|
||||||
|
except: # probably due to incorrect permissions
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError(
|
||||||
|
"PULP_CBC_CMD: Not Available (check permissions on %s)"
|
||||||
|
% self.pulp_cbc_path
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
fracGap=None,
|
||||||
|
maxSeconds=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
presolve=None,
|
||||||
|
cuts=None,
|
||||||
|
strong=None,
|
||||||
|
options=None,
|
||||||
|
warmStart=False,
|
||||||
|
keepFiles=False,
|
||||||
|
path=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
mip_start=False,
|
||||||
|
timeMode="elapsed",
|
||||||
|
):
|
||||||
|
if path is not None:
|
||||||
|
raise PulpSolverError("Use COIN_CMD if you want to set a path")
|
||||||
|
# check that the file is executable
|
||||||
|
COIN_CMD.__init__(
|
||||||
|
self,
|
||||||
|
path=self.pulp_cbc_path,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
fracGap=fracGap,
|
||||||
|
maxSeconds=maxSeconds,
|
||||||
|
gapRel=gapRel,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
presolve=presolve,
|
||||||
|
cuts=cuts,
|
||||||
|
strong=strong,
|
||||||
|
options=options,
|
||||||
|
warmStart=warmStart,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
threads=threads,
|
||||||
|
logPath=logPath,
|
||||||
|
mip_start=mip_start,
|
||||||
|
timeMode=timeMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def COINMP_DLL_load_dll(path):
|
||||||
|
"""
|
||||||
|
function that loads the DLL useful for debugging installation problems
|
||||||
|
"""
|
||||||
|
if os.name == "nt":
|
||||||
|
lib = ctypes.windll.LoadLibrary(str(path[-1]))
|
||||||
|
else:
|
||||||
|
# linux hack to get working
|
||||||
|
mode = ctypes.RTLD_GLOBAL
|
||||||
|
for libpath in path[:-1]:
|
||||||
|
# RTLD_LAZY = 0x00001
|
||||||
|
ctypes.CDLL(libpath, mode=mode)
|
||||||
|
lib = ctypes.CDLL(path[-1], mode=mode)
|
||||||
|
return lib
|
||||||
|
|
||||||
|
|
||||||
|
class COINMP_DLL(LpSolver):
|
||||||
|
"""
|
||||||
|
The COIN_MP LP MIP solver (via a DLL or linux so)
|
||||||
|
|
||||||
|
:param timeLimit: The number of seconds before forcing the solver to exit
|
||||||
|
:param epgap: The fractional mip tolerance
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "COINMP_DLL"
|
||||||
|
try:
|
||||||
|
lib = COINMP_DLL_load_dll(coinMP_path)
|
||||||
|
except (ImportError, OSError):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def available(cls):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError("COINMP_DLL: Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
COIN_INT_LOGLEVEL = 7
|
||||||
|
COIN_REAL_MAXSECONDS = 16
|
||||||
|
COIN_REAL_MIPMAXSEC = 19
|
||||||
|
COIN_REAL_MIPFRACGAP = 34
|
||||||
|
lib.CoinGetInfinity.restype = ctypes.c_double
|
||||||
|
lib.CoinGetVersionStr.restype = ctypes.c_char_p
|
||||||
|
lib.CoinGetSolutionText.restype = ctypes.c_char_p
|
||||||
|
lib.CoinGetObjectValue.restype = ctypes.c_double
|
||||||
|
lib.CoinGetMipBestBound.restype = ctypes.c_double
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cuts=1,
|
||||||
|
presolve=1,
|
||||||
|
dual=1,
|
||||||
|
crash=0,
|
||||||
|
scale=1,
|
||||||
|
rounding=1,
|
||||||
|
integerPresolve=1,
|
||||||
|
strong=5,
|
||||||
|
epgap=None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
LpSolver.__init__(self, *args, **kwargs)
|
||||||
|
self.fracGap = None
|
||||||
|
if epgap is not None:
|
||||||
|
self.fracGap = float(epgap)
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
self.timeLimit = float(self.timeLimit)
|
||||||
|
# Todo: these options are not yet implemented
|
||||||
|
self.cuts = cuts
|
||||||
|
self.presolve = presolve
|
||||||
|
self.dual = dual
|
||||||
|
self.crash = crash
|
||||||
|
self.scale = scale
|
||||||
|
self.rounding = rounding
|
||||||
|
self.integerPresolve = integerPresolve
|
||||||
|
self.strong = strong
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""Make a copy of self"""
|
||||||
|
aCopy = LpSolver.copy(self)
|
||||||
|
aCopy.cuts = self.cuts
|
||||||
|
aCopy.presolve = self.presolve
|
||||||
|
aCopy.dual = self.dual
|
||||||
|
aCopy.crash = self.crash
|
||||||
|
aCopy.scale = self.scale
|
||||||
|
aCopy.rounding = self.rounding
|
||||||
|
aCopy.integerPresolve = self.integerPresolve
|
||||||
|
aCopy.strong = self.strong
|
||||||
|
return aCopy
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def available(cls):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getSolverVersion(self):
|
||||||
|
"""
|
||||||
|
returns a solver version string
|
||||||
|
|
||||||
|
example:
|
||||||
|
>>> COINMP_DLL().getSolverVersion() # doctest: +ELLIPSIS
|
||||||
|
'...'
|
||||||
|
"""
|
||||||
|
return self.lib.CoinGetVersionStr()
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
# TODO alter so that msg parameter is handled correctly
|
||||||
|
self.debug = 0
|
||||||
|
# initialise solver
|
||||||
|
self.lib.CoinInitSolver("")
|
||||||
|
# create problem
|
||||||
|
self.hProb = hProb = self.lib.CoinCreateProblem(lp.name)
|
||||||
|
# set problem options
|
||||||
|
self.lib.CoinSetIntOption(
|
||||||
|
hProb, self.COIN_INT_LOGLEVEL, ctypes.c_int(self.msg)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.timeLimit:
|
||||||
|
if self.mip:
|
||||||
|
self.lib.CoinSetRealOption(
|
||||||
|
hProb, self.COIN_REAL_MIPMAXSEC, ctypes.c_double(self.timeLimit)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.lib.CoinSetRealOption(
|
||||||
|
hProb,
|
||||||
|
self.COIN_REAL_MAXSECONDS,
|
||||||
|
ctypes.c_double(self.timeLimit),
|
||||||
|
)
|
||||||
|
if self.fracGap:
|
||||||
|
# Hopefully this is the bound gap tolerance
|
||||||
|
self.lib.CoinSetRealOption(
|
||||||
|
hProb, self.COIN_REAL_MIPFRACGAP, ctypes.c_double(self.fracGap)
|
||||||
|
)
|
||||||
|
# CoinGetInfinity is needed for varibles with no bounds
|
||||||
|
coinDblMax = self.lib.CoinGetInfinity()
|
||||||
|
if self.debug:
|
||||||
|
print("Before getCoinMPArrays")
|
||||||
|
(
|
||||||
|
numVars,
|
||||||
|
numRows,
|
||||||
|
numels,
|
||||||
|
rangeCount,
|
||||||
|
objectSense,
|
||||||
|
objectCoeffs,
|
||||||
|
objectConst,
|
||||||
|
rhsValues,
|
||||||
|
rangeValues,
|
||||||
|
rowType,
|
||||||
|
startsBase,
|
||||||
|
lenBase,
|
||||||
|
indBase,
|
||||||
|
elemBase,
|
||||||
|
lowerBounds,
|
||||||
|
upperBounds,
|
||||||
|
initValues,
|
||||||
|
colNames,
|
||||||
|
rowNames,
|
||||||
|
columnType,
|
||||||
|
n2v,
|
||||||
|
n2c,
|
||||||
|
) = self.getCplexStyleArrays(lp)
|
||||||
|
self.lib.CoinLoadProblem(
|
||||||
|
hProb,
|
||||||
|
numVars,
|
||||||
|
numRows,
|
||||||
|
numels,
|
||||||
|
rangeCount,
|
||||||
|
objectSense,
|
||||||
|
objectConst,
|
||||||
|
objectCoeffs,
|
||||||
|
lowerBounds,
|
||||||
|
upperBounds,
|
||||||
|
rowType,
|
||||||
|
rhsValues,
|
||||||
|
rangeValues,
|
||||||
|
startsBase,
|
||||||
|
lenBase,
|
||||||
|
indBase,
|
||||||
|
elemBase,
|
||||||
|
colNames,
|
||||||
|
rowNames,
|
||||||
|
"Objective",
|
||||||
|
)
|
||||||
|
if lp.isMIP() and self.mip:
|
||||||
|
self.lib.CoinLoadInteger(hProb, columnType)
|
||||||
|
|
||||||
|
if self.msg == 0:
|
||||||
|
self.lib.CoinRegisterMsgLogCallback(
|
||||||
|
hProb, ctypes.c_char_p(""), ctypes.POINTER(ctypes.c_int)()
|
||||||
|
)
|
||||||
|
self.coinTime = -clock()
|
||||||
|
self.lib.CoinOptimizeProblem(hProb, 0)
|
||||||
|
self.coinTime += clock()
|
||||||
|
|
||||||
|
# TODO: check Integer Feasible status
|
||||||
|
CoinLpStatus = {
|
||||||
|
0: constants.LpStatusOptimal,
|
||||||
|
1: constants.LpStatusInfeasible,
|
||||||
|
2: constants.LpStatusInfeasible,
|
||||||
|
3: constants.LpStatusNotSolved,
|
||||||
|
4: constants.LpStatusNotSolved,
|
||||||
|
5: constants.LpStatusNotSolved,
|
||||||
|
-1: constants.LpStatusUndefined,
|
||||||
|
}
|
||||||
|
solutionStatus = self.lib.CoinGetSolutionStatus(hProb)
|
||||||
|
solutionText = self.lib.CoinGetSolutionText(hProb)
|
||||||
|
objectValue = self.lib.CoinGetObjectValue(hProb)
|
||||||
|
|
||||||
|
# get the solution values
|
||||||
|
NumVarDoubleArray = ctypes.c_double * numVars
|
||||||
|
NumRowsDoubleArray = ctypes.c_double * numRows
|
||||||
|
cActivity = NumVarDoubleArray()
|
||||||
|
cReducedCost = NumVarDoubleArray()
|
||||||
|
cSlackValues = NumRowsDoubleArray()
|
||||||
|
cShadowPrices = NumRowsDoubleArray()
|
||||||
|
self.lib.CoinGetSolutionValues(
|
||||||
|
hProb,
|
||||||
|
ctypes.byref(cActivity),
|
||||||
|
ctypes.byref(cReducedCost),
|
||||||
|
ctypes.byref(cSlackValues),
|
||||||
|
ctypes.byref(cShadowPrices),
|
||||||
|
)
|
||||||
|
|
||||||
|
variablevalues = {}
|
||||||
|
variabledjvalues = {}
|
||||||
|
constraintpivalues = {}
|
||||||
|
constraintslackvalues = {}
|
||||||
|
if lp.isMIP() and self.mip:
|
||||||
|
lp.bestBound = self.lib.CoinGetMipBestBound(hProb)
|
||||||
|
for i in range(numVars):
|
||||||
|
variablevalues[self.n2v[i].name] = cActivity[i]
|
||||||
|
variabledjvalues[self.n2v[i].name] = cReducedCost[i]
|
||||||
|
lp.assignVarsVals(variablevalues)
|
||||||
|
lp.assignVarsDj(variabledjvalues)
|
||||||
|
# put pi and slack variables against the constraints
|
||||||
|
for i in range(numRows):
|
||||||
|
constraintpivalues[self.n2c[i]] = cShadowPrices[i]
|
||||||
|
constraintslackvalues[self.n2c[i]] = cSlackValues[i]
|
||||||
|
lp.assignConsPi(constraintpivalues)
|
||||||
|
lp.assignConsSlack(constraintslackvalues)
|
||||||
|
|
||||||
|
self.lib.CoinFreeSolver()
|
||||||
|
status = CoinLpStatus[self.lib.CoinGetSolutionStatus(hProb)]
|
||||||
|
lp.assignStatus(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
if COINMP_DLL.available():
|
||||||
|
COIN = COINMP_DLL
|
||||||
|
|
||||||
|
yaposib = None
|
||||||
|
|
||||||
|
|
||||||
|
class YAPOSIB(LpSolver):
|
||||||
|
"""
|
||||||
|
COIN OSI (via its python interface)
|
||||||
|
|
||||||
|
Copyright Christophe-Marie Duquesne 2012
|
||||||
|
|
||||||
|
The yaposib variables are available (after a solve) in var.solverVar
|
||||||
|
The yaposib constraints are available in constraint.solverConstraint
|
||||||
|
The Model is in prob.solverModel
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "YAPOSIB"
|
||||||
|
try:
|
||||||
|
# import the model into the global scope
|
||||||
|
global yaposib
|
||||||
|
import yaposib
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError("YAPOSIB: Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
epgap=None,
|
||||||
|
solverName=None,
|
||||||
|
**solverParams,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initializes the yaposib solver.
|
||||||
|
|
||||||
|
@param mip: if False the solver will solve a MIP as
|
||||||
|
an LP
|
||||||
|
@param msg: displays information from the solver to
|
||||||
|
stdout
|
||||||
|
@param timeLimit: not supported
|
||||||
|
@param epgap: not supported
|
||||||
|
@param solverParams: not supported
|
||||||
|
"""
|
||||||
|
LpSolver.__init__(self, mip, msg)
|
||||||
|
if solverName:
|
||||||
|
self.solverName = solverName
|
||||||
|
else:
|
||||||
|
self.solverName = yaposib.available_solvers()[0]
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
model = lp.solverModel
|
||||||
|
solutionStatus = model.status
|
||||||
|
yaposibLpStatus = {
|
||||||
|
"optimal": constants.LpStatusOptimal,
|
||||||
|
"undefined": constants.LpStatusUndefined,
|
||||||
|
"abandoned": constants.LpStatusInfeasible,
|
||||||
|
"infeasible": constants.LpStatusInfeasible,
|
||||||
|
"limitreached": constants.LpStatusInfeasible,
|
||||||
|
}
|
||||||
|
# populate pulp solution values
|
||||||
|
for var in lp.variables():
|
||||||
|
var.varValue = var.solverVar.solution
|
||||||
|
var.dj = var.solverVar.reducedcost
|
||||||
|
# put pi and slack variables against the constraints
|
||||||
|
for constr in lp.constraints.values():
|
||||||
|
constr.pi = constr.solverConstraint.dual
|
||||||
|
constr.slack = -constr.constant - constr.solverConstraint.activity
|
||||||
|
if self.msg:
|
||||||
|
print("yaposib status=", solutionStatus)
|
||||||
|
lp.resolveOK = True
|
||||||
|
for var in lp.variables():
|
||||||
|
var.isModified = False
|
||||||
|
status = yaposibLpStatus.get(solutionStatus, constants.LpStatusUndefined)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def callSolver(self, lp, callback=None):
|
||||||
|
"""Solves the problem with yaposib"""
|
||||||
|
savestdout = None
|
||||||
|
if self.msg == 0:
|
||||||
|
# close stdout to get rid of messages
|
||||||
|
tempfile = open(mktemp(), "w")
|
||||||
|
savestdout = os.dup(1)
|
||||||
|
os.close(1)
|
||||||
|
if os.dup(tempfile.fileno()) != 1:
|
||||||
|
raise PulpSolverError("couldn't redirect stdout - dup() error")
|
||||||
|
self.solveTime = -clock()
|
||||||
|
lp.solverModel.solve(self.mip)
|
||||||
|
self.solveTime += clock()
|
||||||
|
if self.msg == 0:
|
||||||
|
# reopen stdout
|
||||||
|
os.close(1)
|
||||||
|
os.dup(savestdout)
|
||||||
|
os.close(savestdout)
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into a yaposib model
|
||||||
|
"""
|
||||||
|
log.debug("create the yaposib model")
|
||||||
|
lp.solverModel = yaposib.Problem(self.solverName)
|
||||||
|
prob = lp.solverModel
|
||||||
|
prob.name = lp.name
|
||||||
|
log.debug("set the sense of the problem")
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
prob.obj.maximize = True
|
||||||
|
log.debug("add the variables to the problem")
|
||||||
|
for var in lp.variables():
|
||||||
|
col = prob.cols.add(yaposib.vec([]))
|
||||||
|
col.name = var.name
|
||||||
|
if not var.lowBound is None:
|
||||||
|
col.lowerbound = var.lowBound
|
||||||
|
if not var.upBound is None:
|
||||||
|
col.upperbound = var.upBound
|
||||||
|
if var.cat == constants.LpInteger:
|
||||||
|
col.integer = True
|
||||||
|
prob.obj[col.index] = lp.objective.get(var, 0.0)
|
||||||
|
var.solverVar = col
|
||||||
|
log.debug("add the Constraints to the problem")
|
||||||
|
for name, constraint in lp.constraints.items():
|
||||||
|
row = prob.rows.add(
|
||||||
|
yaposib.vec(
|
||||||
|
[
|
||||||
|
(var.solverVar.index, value)
|
||||||
|
for var, value in constraint.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
row.upperbound = -constraint.constant
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
row.lowerbound = -constraint.constant
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
row.upperbound = -constraint.constant
|
||||||
|
row.lowerbound = -constraint.constant
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
row.name = name
|
||||||
|
constraint.solverConstraint = row
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
creates a yaposib model, variables and constraints and attaches
|
||||||
|
them to the lp model which it then solves
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
# set the initial solution
|
||||||
|
log.debug("Solve the model using yaposib")
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def actualResolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
uses the old solver and modifies the rhs of the modified
|
||||||
|
constraints
|
||||||
|
"""
|
||||||
|
log.debug("Resolve the model using yaposib")
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
row = constraint.solverConstraint
|
||||||
|
if constraint.modified:
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
row.upperbound = -constraint.constant
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
row.lowerbound = -constraint.constant
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
row.upperbound = -constraint.constant
|
||||||
|
row.lowerbound = -constraint.constant
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
1090
utils/pulp/apis/copt_api.py
Normal file
1090
utils/pulp/apis/copt_api.py
Normal file
File diff suppressed because it is too large
Load Diff
493
utils/pulp/apis/core.py
Normal file
493
utils/pulp/apis/core.py
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file contains the solver classes for PuLP
|
||||||
|
Note that the solvers that require a compiled extension may not work in
|
||||||
|
the current version
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
|
||||||
|
from time import monotonic as clock
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
Parser = configparser.ConfigParser
|
||||||
|
|
||||||
|
from .. import sparse
|
||||||
|
from .. import constants as const
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ujson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
devnull = subprocess.DEVNULL
|
||||||
|
to_string = lambda _obj: str(_obj).encode()
|
||||||
|
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
||||||
|
class PulpSolverError(const.PulpError):
|
||||||
|
"""
|
||||||
|
Pulp Solver-related exceptions
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# import configuration information
|
||||||
|
def initialize(filename, operating_system="linux", arch="64"):
|
||||||
|
"""reads the configuration file to initialise the module"""
|
||||||
|
here = os.path.dirname(filename)
|
||||||
|
config = Parser({"here": here, "os": operating_system, "arch": arch})
|
||||||
|
config.read(filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cplex_dll_path = config.get("locations", "CplexPath")
|
||||||
|
except configparser.Error:
|
||||||
|
cplex_dll_path = "libcplex110.so"
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
ilm_cplex_license = (
|
||||||
|
config.get("licenses", "ilm_cplex_license")
|
||||||
|
.decode("string-escape")
|
||||||
|
.replace('"', "")
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
ilm_cplex_license = config.get("licenses", "ilm_cplex_license").replace(
|
||||||
|
'"', ""
|
||||||
|
)
|
||||||
|
except configparser.Error:
|
||||||
|
ilm_cplex_license = ""
|
||||||
|
try:
|
||||||
|
ilm_cplex_license_signature = config.getint(
|
||||||
|
"licenses", "ilm_cplex_license_signature"
|
||||||
|
)
|
||||||
|
except configparser.Error:
|
||||||
|
ilm_cplex_license_signature = 0
|
||||||
|
try:
|
||||||
|
coinMP_path = config.get("locations", "CoinMPPath").split(", ")
|
||||||
|
except configparser.Error:
|
||||||
|
coinMP_path = ["libCoinMP.so"]
|
||||||
|
try:
|
||||||
|
gurobi_path = config.get("locations", "GurobiPath")
|
||||||
|
except configparser.Error:
|
||||||
|
gurobi_path = "/opt/gurobi201/linux32/lib/python2.5"
|
||||||
|
try:
|
||||||
|
cbc_path = config.get("locations", "CbcPath")
|
||||||
|
except configparser.Error:
|
||||||
|
cbc_path = "cbc"
|
||||||
|
try:
|
||||||
|
glpk_path = config.get("locations", "GlpkPath")
|
||||||
|
except configparser.Error:
|
||||||
|
glpk_path = "glpsol"
|
||||||
|
try:
|
||||||
|
pulp_cbc_path = config.get("locations", "PulpCbcPath")
|
||||||
|
except configparser.Error:
|
||||||
|
pulp_cbc_path = "cbc"
|
||||||
|
try:
|
||||||
|
scip_path = config.get("locations", "ScipPath")
|
||||||
|
except configparser.Error:
|
||||||
|
scip_path = "scip"
|
||||||
|
try:
|
||||||
|
fscip_path = config.get("locations", "FscipPath")
|
||||||
|
except configparser.Error:
|
||||||
|
fscip_path = "fscip"
|
||||||
|
for i, path in enumerate(coinMP_path):
|
||||||
|
if not os.path.dirname(path):
|
||||||
|
# if no pathname is supplied assume the file is in the same directory
|
||||||
|
coinMP_path[i] = os.path.join(os.path.dirname(config_filename), path)
|
||||||
|
return (
|
||||||
|
cplex_dll_path,
|
||||||
|
ilm_cplex_license,
|
||||||
|
ilm_cplex_license_signature,
|
||||||
|
coinMP_path,
|
||||||
|
gurobi_path,
|
||||||
|
cbc_path,
|
||||||
|
glpk_path,
|
||||||
|
pulp_cbc_path,
|
||||||
|
scip_path,
|
||||||
|
fscip_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# pick up the correct config file depending on operating system
|
||||||
|
PULPCFGFILE = "pulp.cfg"
|
||||||
|
is_64bits = sys.maxsize > 2**32
|
||||||
|
if is_64bits:
|
||||||
|
arch = "64"
|
||||||
|
if platform.machine().lower() in ["aarch64", "arm64"]:
|
||||||
|
arch = "arm64"
|
||||||
|
else:
|
||||||
|
arch = "32"
|
||||||
|
operating_system = None
|
||||||
|
if sys.platform in ["win32", "cli"]:
|
||||||
|
operating_system = "win"
|
||||||
|
PULPCFGFILE += ".win"
|
||||||
|
elif sys.platform in ["darwin"]:
|
||||||
|
operating_system = "osx"
|
||||||
|
arch = "64"
|
||||||
|
PULPCFGFILE += ".osx"
|
||||||
|
else:
|
||||||
|
operating_system = "linux"
|
||||||
|
PULPCFGFILE += ".linux"
|
||||||
|
|
||||||
|
DIRNAME = os.path.dirname(__file__)
|
||||||
|
config_filename = os.path.normpath(os.path.join(DIRNAME, "..", PULPCFGFILE))
|
||||||
|
(
|
||||||
|
cplex_dll_path,
|
||||||
|
ilm_cplex_license,
|
||||||
|
ilm_cplex_license_signature,
|
||||||
|
coinMP_path,
|
||||||
|
gurobi_path,
|
||||||
|
cbc_path,
|
||||||
|
glpk_path,
|
||||||
|
pulp_cbc_path,
|
||||||
|
scip_path,
|
||||||
|
fscip_path,
|
||||||
|
) = initialize(config_filename, operating_system, arch)
|
||||||
|
|
||||||
|
|
||||||
|
class LpSolver:
|
||||||
|
"""A generic LP Solver"""
|
||||||
|
|
||||||
|
name = "LpSolver"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, mip=True, msg=True, options=None, timeLimit=None, *args, **kwargs
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param list options:
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param args:
|
||||||
|
:param kwargs: optional named options to pass to each solver,
|
||||||
|
e.g. gapRel=0.1, gapAbs=10, logPath="",
|
||||||
|
"""
|
||||||
|
if options is None:
|
||||||
|
options = []
|
||||||
|
self.mip = mip
|
||||||
|
self.msg = msg
|
||||||
|
self.options = options
|
||||||
|
self.timeLimit = timeLimit
|
||||||
|
|
||||||
|
# here we will store all other relevant information including:
|
||||||
|
# gapRel, gapAbs, maxMemory, maxNodes, threads, logPath, timeMode
|
||||||
|
self.optionsDict = {k: v for k, v in kwargs.items() if v is not None}
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def actualResolve(self, lp, **kwargs):
|
||||||
|
"""
|
||||||
|
uses existing problem information and solves the problem
|
||||||
|
If it is not implemented in the solver
|
||||||
|
just solve again
|
||||||
|
"""
|
||||||
|
self.actualSolve(lp, **kwargs)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""Make a copy of self"""
|
||||||
|
|
||||||
|
aCopy = self.__class__()
|
||||||
|
aCopy.mip = self.mip
|
||||||
|
aCopy.msg = self.msg
|
||||||
|
aCopy.options = self.options
|
||||||
|
return aCopy
|
||||||
|
|
||||||
|
def solve(self, lp):
|
||||||
|
"""Solve the problem lp"""
|
||||||
|
# Always go through the solve method of LpProblem
|
||||||
|
return lp.solve(self)
|
||||||
|
|
||||||
|
# TODO: Not sure if this code should be here or in a child class
|
||||||
|
def getCplexStyleArrays(
|
||||||
|
self, lp, senseDict=None, LpVarCategories=None, LpObjSenses=None, infBound=1e20
|
||||||
|
):
|
||||||
|
"""returns the arrays suitable to pass to a cdll Cplex
|
||||||
|
or other solvers that are similar
|
||||||
|
|
||||||
|
Copyright (c) Stuart Mitchell 2007
|
||||||
|
"""
|
||||||
|
if senseDict is None:
|
||||||
|
senseDict = {
|
||||||
|
const.LpConstraintEQ: "E",
|
||||||
|
const.LpConstraintLE: "L",
|
||||||
|
const.LpConstraintGE: "G",
|
||||||
|
}
|
||||||
|
if LpVarCategories is None:
|
||||||
|
LpVarCategories = {const.LpContinuous: "C", const.LpInteger: "I"}
|
||||||
|
if LpObjSenses is None:
|
||||||
|
LpObjSenses = {const.LpMaximize: -1, const.LpMinimize: 1}
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
rangeCount = 0
|
||||||
|
variables = list(lp.variables())
|
||||||
|
numVars = len(variables)
|
||||||
|
# associate each variable with a ordinal
|
||||||
|
self.v2n = {variables[i]: i for i in range(numVars)}
|
||||||
|
self.vname2n = {variables[i].name: i for i in range(numVars)}
|
||||||
|
self.n2v = {i: variables[i] for i in range(numVars)}
|
||||||
|
# objective values
|
||||||
|
objSense = LpObjSenses[lp.sense]
|
||||||
|
NumVarDoubleArray = ctypes.c_double * numVars
|
||||||
|
objectCoeffs = NumVarDoubleArray()
|
||||||
|
# print "Get objective Values"
|
||||||
|
for v, val in lp.objective.items():
|
||||||
|
objectCoeffs[self.v2n[v]] = val
|
||||||
|
# values for variables
|
||||||
|
objectConst = ctypes.c_double(0.0)
|
||||||
|
NumVarStrArray = ctypes.c_char_p * numVars
|
||||||
|
colNames = NumVarStrArray()
|
||||||
|
lowerBounds = NumVarDoubleArray()
|
||||||
|
upperBounds = NumVarDoubleArray()
|
||||||
|
initValues = NumVarDoubleArray()
|
||||||
|
for v in lp.variables():
|
||||||
|
colNames[self.v2n[v]] = to_string(v.name)
|
||||||
|
initValues[self.v2n[v]] = 0.0
|
||||||
|
if v.lowBound != None:
|
||||||
|
lowerBounds[self.v2n[v]] = v.lowBound
|
||||||
|
else:
|
||||||
|
lowerBounds[self.v2n[v]] = -infBound
|
||||||
|
if v.upBound != None:
|
||||||
|
upperBounds[self.v2n[v]] = v.upBound
|
||||||
|
else:
|
||||||
|
upperBounds[self.v2n[v]] = infBound
|
||||||
|
# values for constraints
|
||||||
|
numRows = len(lp.constraints)
|
||||||
|
NumRowDoubleArray = ctypes.c_double * numRows
|
||||||
|
NumRowStrArray = ctypes.c_char_p * numRows
|
||||||
|
NumRowCharArray = ctypes.c_char * numRows
|
||||||
|
rhsValues = NumRowDoubleArray()
|
||||||
|
rangeValues = NumRowDoubleArray()
|
||||||
|
rowNames = NumRowStrArray()
|
||||||
|
rowType = NumRowCharArray()
|
||||||
|
self.c2n = {}
|
||||||
|
self.n2c = {}
|
||||||
|
i = 0
|
||||||
|
for c in lp.constraints:
|
||||||
|
rhsValues[i] = -lp.constraints[c].constant
|
||||||
|
# for ranged constraints a<= constraint >=b
|
||||||
|
rangeValues[i] = 0.0
|
||||||
|
rowNames[i] = to_string(c)
|
||||||
|
rowType[i] = to_string(senseDict[lp.constraints[c].sense])
|
||||||
|
self.c2n[c] = i
|
||||||
|
self.n2c[i] = c
|
||||||
|
i = i + 1
|
||||||
|
# return the coefficient matrix as a series of vectors
|
||||||
|
coeffs = lp.coefficients()
|
||||||
|
sparseMatrix = sparse.Matrix(list(range(numRows)), list(range(numVars)))
|
||||||
|
for var, row, coeff in coeffs:
|
||||||
|
sparseMatrix.add(self.c2n[row], self.vname2n[var], coeff)
|
||||||
|
(
|
||||||
|
numels,
|
||||||
|
mystartsBase,
|
||||||
|
mylenBase,
|
||||||
|
myindBase,
|
||||||
|
myelemBase,
|
||||||
|
) = sparseMatrix.col_based_arrays()
|
||||||
|
elemBase = ctypesArrayFill(myelemBase, ctypes.c_double)
|
||||||
|
indBase = ctypesArrayFill(myindBase, ctypes.c_int)
|
||||||
|
startsBase = ctypesArrayFill(mystartsBase, ctypes.c_int)
|
||||||
|
lenBase = ctypesArrayFill(mylenBase, ctypes.c_int)
|
||||||
|
# MIP Variables
|
||||||
|
NumVarCharArray = ctypes.c_char * numVars
|
||||||
|
columnType = NumVarCharArray()
|
||||||
|
if lp.isMIP():
|
||||||
|
for v in lp.variables():
|
||||||
|
columnType[self.v2n[v]] = to_string(LpVarCategories[v.cat])
|
||||||
|
self.addedVars = numVars
|
||||||
|
self.addedRows = numRows
|
||||||
|
return (
|
||||||
|
numVars,
|
||||||
|
numRows,
|
||||||
|
numels,
|
||||||
|
rangeCount,
|
||||||
|
objSense,
|
||||||
|
objectCoeffs,
|
||||||
|
objectConst,
|
||||||
|
rhsValues,
|
||||||
|
rangeValues,
|
||||||
|
rowType,
|
||||||
|
startsBase,
|
||||||
|
lenBase,
|
||||||
|
indBase,
|
||||||
|
elemBase,
|
||||||
|
lowerBounds,
|
||||||
|
upperBounds,
|
||||||
|
initValues,
|
||||||
|
colNames,
|
||||||
|
rowNames,
|
||||||
|
columnType,
|
||||||
|
self.n2v,
|
||||||
|
self.n2c,
|
||||||
|
)
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
data = dict(solver=self.name)
|
||||||
|
for k in ["mip", "msg", "keepFiles"]:
|
||||||
|
try:
|
||||||
|
data[k] = getattr(self, k)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
for k in ["timeLimit", "options"]:
|
||||||
|
# with these ones, we only export if it has some content:
|
||||||
|
try:
|
||||||
|
value = getattr(self, k)
|
||||||
|
if value:
|
||||||
|
data[k] = value
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
data.update(self.optionsDict)
|
||||||
|
return data
|
||||||
|
|
||||||
|
to_dict = toDict
|
||||||
|
|
||||||
|
def toJson(self, filename, *args, **kwargs):
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
json.dump(self.toDict(), f, *args, **kwargs)
|
||||||
|
|
||||||
|
to_json = toJson
|
||||||
|
|
||||||
|
|
||||||
|
class LpSolver_CMD(LpSolver):
|
||||||
|
"""A generic command line LP Solver"""
|
||||||
|
|
||||||
|
name = "LpSolver_CMD"
|
||||||
|
|
||||||
|
def __init__(self, path=None, keepFiles=False, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param list options: list of additional options to pass to solver (format depends on the solver)
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param str path: a path to the solver binary
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param args: parameters to pass to :py:class:`LpSolver`
|
||||||
|
:param kwargs: parameters to pass to :py:class:`LpSolver`
|
||||||
|
"""
|
||||||
|
LpSolver.__init__(self, *args, **kwargs)
|
||||||
|
if path is None:
|
||||||
|
self.path = self.defaultPath()
|
||||||
|
else:
|
||||||
|
self.path = path
|
||||||
|
self.keepFiles = keepFiles
|
||||||
|
self.setTmpDir()
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""Make a copy of self"""
|
||||||
|
|
||||||
|
aCopy = LpSolver.copy(self)
|
||||||
|
aCopy.path = self.path
|
||||||
|
aCopy.keepFiles = self.keepFiles
|
||||||
|
aCopy.tmpDir = self.tmpDir
|
||||||
|
return aCopy
|
||||||
|
|
||||||
|
def setTmpDir(self):
|
||||||
|
"""Set the tmpDir attribute to a reasonnable location for a temporary
|
||||||
|
directory"""
|
||||||
|
if os.name != "nt":
|
||||||
|
# On unix use /tmp by default
|
||||||
|
self.tmpDir = os.environ.get("TMPDIR", "/tmp")
|
||||||
|
self.tmpDir = os.environ.get("TMP", self.tmpDir)
|
||||||
|
else:
|
||||||
|
# On Windows use the current directory
|
||||||
|
self.tmpDir = os.environ.get("TMPDIR", "")
|
||||||
|
self.tmpDir = os.environ.get("TMP", self.tmpDir)
|
||||||
|
self.tmpDir = os.environ.get("TEMP", self.tmpDir)
|
||||||
|
if not os.path.isdir(self.tmpDir):
|
||||||
|
self.tmpDir = ""
|
||||||
|
elif not os.access(self.tmpDir, os.F_OK + os.W_OK):
|
||||||
|
self.tmpDir = ""
|
||||||
|
|
||||||
|
def create_tmp_files(self, name, *args):
|
||||||
|
if self.keepFiles:
|
||||||
|
prefix = name
|
||||||
|
else:
|
||||||
|
prefix = os.path.join(self.tmpDir, uuid4().hex)
|
||||||
|
return (f"{prefix}-pulp.{n}" for n in args)
|
||||||
|
|
||||||
|
def silent_remove(self, file: Union[str, bytes, os.PathLike]) -> None:
|
||||||
|
try:
|
||||||
|
os.remove(file)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete_tmp_files(self, *args):
|
||||||
|
if self.keepFiles:
|
||||||
|
return
|
||||||
|
for file in args:
|
||||||
|
self.silent_remove(file)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def executableExtension(name):
|
||||||
|
if os.name != "nt":
|
||||||
|
return name
|
||||||
|
else:
|
||||||
|
return name + ".exe"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def executable(command):
|
||||||
|
"""Checks that the solver command is executable,
|
||||||
|
And returns the actual path to it."""
|
||||||
|
return shutil.which(command)
|
||||||
|
|
||||||
|
|
||||||
|
def ctypesArrayFill(myList, type=ctypes.c_double):
|
||||||
|
"""
|
||||||
|
Creates a c array with ctypes from a python list
|
||||||
|
type is the type of the c array
|
||||||
|
"""
|
||||||
|
ctype = type * len(myList)
|
||||||
|
cList = ctype()
|
||||||
|
for i, elem in enumerate(myList):
|
||||||
|
cList[i] = elem
|
||||||
|
return cList
|
||||||
579
utils/pulp/apis/cplex_api.py
Normal file
579
utils/pulp/apis/cplex_api.py
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
|
||||||
|
from .. import constants
|
||||||
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class CPLEX_CMD(LpSolver_CMD):
|
||||||
|
"""The CPLEX LP solver"""
|
||||||
|
|
||||||
|
name = "CPLEX_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timelimit=None,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
options=None,
|
||||||
|
warmStart=False,
|
||||||
|
keepFiles=False,
|
||||||
|
path=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
maxMemory=None,
|
||||||
|
maxNodes=None,
|
||||||
|
mip_start=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool warmStart: if True, the solver will use the current value of variables as a start
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param float maxMemory: max memory to use during the solving. Stops the solving when reached.
|
||||||
|
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
|
||||||
|
:param bool mip_start: deprecated for warmStart
|
||||||
|
:param float timelimit: deprecated for timeLimit
|
||||||
|
"""
|
||||||
|
if timelimit is not None:
|
||||||
|
warnings.warn("Parameter timelimit is being depreciated for timeLimit")
|
||||||
|
if timeLimit is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter timeLimit and timelimit passed, using timeLimit "
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
timeLimit = timelimit
|
||||||
|
if mip_start:
|
||||||
|
warnings.warn("Parameter mip_start is being depreciated for warmStart")
|
||||||
|
if warmStart:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter mipStart and mip_start passed, using warmStart"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warmStart = mip_start
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
maxMemory=maxMemory,
|
||||||
|
maxNodes=maxNodes,
|
||||||
|
warmStart=warmStart,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
threads=threads,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
logPath=logPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("cplex")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpLp, tmpSol, tmpMst = self.create_tmp_files(lp.name, "lp", "sol", "mst")
|
||||||
|
vs = lp.writeLP(tmpLp, writeSOS=1)
|
||||||
|
try:
|
||||||
|
os.remove(tmpSol)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not self.msg:
|
||||||
|
cplex = subprocess.Popen(
|
||||||
|
self.path,
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cplex = subprocess.Popen(self.path, stdin=subprocess.PIPE)
|
||||||
|
cplex_cmds = "read " + tmpLp + "\n"
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
self.writesol(filename=tmpMst, vs=vs)
|
||||||
|
cplex_cmds += "read " + tmpMst + "\n"
|
||||||
|
cplex_cmds += "set advance 1\n"
|
||||||
|
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
cplex_cmds += "set timelimit " + str(self.timeLimit) + "\n"
|
||||||
|
options = self.options + self.getOptions()
|
||||||
|
for option in options:
|
||||||
|
cplex_cmds += option + "\n"
|
||||||
|
if lp.isMIP():
|
||||||
|
if self.mip:
|
||||||
|
cplex_cmds += "mipopt\n"
|
||||||
|
cplex_cmds += "change problem fixed\n"
|
||||||
|
else:
|
||||||
|
cplex_cmds += "change problem lp\n"
|
||||||
|
cplex_cmds += "optimize\n"
|
||||||
|
cplex_cmds += "write " + tmpSol + "\n"
|
||||||
|
cplex_cmds += "quit\n"
|
||||||
|
cplex_cmds = cplex_cmds.encode("UTF-8")
|
||||||
|
cplex.communicate(cplex_cmds)
|
||||||
|
if cplex.returncode != 0:
|
||||||
|
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
status = constants.LpStatusInfeasible
|
||||||
|
values = reducedCosts = shadowPrices = slacks = solStatus = None
|
||||||
|
else:
|
||||||
|
(
|
||||||
|
status,
|
||||||
|
values,
|
||||||
|
reducedCosts,
|
||||||
|
shadowPrices,
|
||||||
|
slacks,
|
||||||
|
solStatus,
|
||||||
|
) = self.readsol(tmpSol)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpMst, tmpSol)
|
||||||
|
if self.optionsDict.get("logPath") != "cplex.log":
|
||||||
|
self.delete_tmp_files("cplex.log")
|
||||||
|
if status != constants.LpStatusInfeasible:
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
lp.assignVarsDj(reducedCosts)
|
||||||
|
lp.assignConsPi(shadowPrices)
|
||||||
|
lp.assignConsSlack(slacks)
|
||||||
|
lp.assignStatus(status, solStatus)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def getOptions(self):
|
||||||
|
# CPLEX parameters: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.6.0/ilog.odms.cplex.help/CPLEX/GettingStarted/topics/tutorials/InteractiveOptimizer/settingParams.html
|
||||||
|
# CPLEX status: https://www.ibm.com/support/knowledgecenter/en/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/macros/Solution_status_codes.html
|
||||||
|
params_eq = dict(
|
||||||
|
logPath="set logFile {}",
|
||||||
|
gapRel="set mip tolerances mipgap {}",
|
||||||
|
gapAbs="set mip tolerances absmipgap {}",
|
||||||
|
maxMemory="set mip limits treememory {}",
|
||||||
|
threads="set threads {}",
|
||||||
|
maxNodes="set mip limits nodes {}",
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
v.format(self.optionsDict[k])
|
||||||
|
for k, v in params_eq.items()
|
||||||
|
if k in self.optionsDict and self.optionsDict[k] is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
def readsol(self, filename):
|
||||||
|
"""Read a CPLEX solution file"""
|
||||||
|
# CPLEX solution codes: http://www-eio.upc.es/lceio/manuals/cplex-11/html/overviewcplex/statuscodes.html
|
||||||
|
try:
|
||||||
|
import xml.etree.ElementTree as et
|
||||||
|
except ImportError:
|
||||||
|
import elementtree.ElementTree as et
|
||||||
|
solutionXML = et.parse(filename).getroot()
|
||||||
|
solutionheader = solutionXML.find("header")
|
||||||
|
statusString = solutionheader.get("solutionStatusString")
|
||||||
|
statusValue = solutionheader.get("solutionStatusValue")
|
||||||
|
cplexStatus = {
|
||||||
|
"1": constants.LpStatusOptimal, # optimal
|
||||||
|
"101": constants.LpStatusOptimal, # mip optimal
|
||||||
|
"102": constants.LpStatusOptimal, # mip optimal tolerance
|
||||||
|
"104": constants.LpStatusOptimal, # max solution limit
|
||||||
|
"105": constants.LpStatusOptimal, # node limit feasible
|
||||||
|
"107": constants.LpStatusOptimal, # time lim feasible
|
||||||
|
"109": constants.LpStatusOptimal, # fail but feasible
|
||||||
|
"113": constants.LpStatusOptimal, # abort feasible
|
||||||
|
}
|
||||||
|
if statusValue not in cplexStatus:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"Unknown status returned by CPLEX: \ncode: '{}', string: '{}'".format(
|
||||||
|
statusValue, statusString
|
||||||
|
)
|
||||||
|
)
|
||||||
|
status = cplexStatus[statusValue]
|
||||||
|
# we check for integer feasible status to differentiate from optimal in solution status
|
||||||
|
cplexSolStatus = {
|
||||||
|
"104": constants.LpSolutionIntegerFeasible, # max solution limit
|
||||||
|
"105": constants.LpSolutionIntegerFeasible, # node limit feasible
|
||||||
|
"107": constants.LpSolutionIntegerFeasible, # time lim feasible
|
||||||
|
"109": constants.LpSolutionIntegerFeasible, # fail but feasible
|
||||||
|
"111": constants.LpSolutionIntegerFeasible, # memory limit feasible
|
||||||
|
"113": constants.LpSolutionIntegerFeasible, # abort feasible
|
||||||
|
}
|
||||||
|
solStatus = cplexSolStatus.get(statusValue)
|
||||||
|
shadowPrices = {}
|
||||||
|
slacks = {}
|
||||||
|
constraints = solutionXML.find("linearConstraints")
|
||||||
|
for constraint in constraints:
|
||||||
|
name = constraint.get("name")
|
||||||
|
slack = constraint.get("slack")
|
||||||
|
shadowPrice = constraint.get("dual")
|
||||||
|
try:
|
||||||
|
# See issue #508
|
||||||
|
shadowPrices[name] = float(shadowPrice)
|
||||||
|
except TypeError:
|
||||||
|
shadowPrices[name] = None
|
||||||
|
slacks[name] = float(slack)
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
reducedCosts = {}
|
||||||
|
for variable in solutionXML.find("variables"):
|
||||||
|
name = variable.get("name")
|
||||||
|
value = variable.get("value")
|
||||||
|
values[name] = float(value)
|
||||||
|
reducedCost = variable.get("reducedCost")
|
||||||
|
try:
|
||||||
|
# See issue #508
|
||||||
|
reducedCosts[name] = float(reducedCost)
|
||||||
|
except TypeError:
|
||||||
|
reducedCosts[name] = None
|
||||||
|
|
||||||
|
return status, values, reducedCosts, shadowPrices, slacks, solStatus
|
||||||
|
|
||||||
|
def writesol(self, filename, vs):
|
||||||
|
"""Writes a CPLEX solution file"""
|
||||||
|
try:
|
||||||
|
import xml.etree.ElementTree as et
|
||||||
|
except ImportError:
|
||||||
|
import elementtree.ElementTree as et
|
||||||
|
root = et.Element("CPLEXSolution", version="1.2")
|
||||||
|
attrib_head = dict()
|
||||||
|
attrib_quality = dict()
|
||||||
|
et.SubElement(root, "header", attrib=attrib_head)
|
||||||
|
et.SubElement(root, "header", attrib=attrib_quality)
|
||||||
|
variables = et.SubElement(root, "variables")
|
||||||
|
|
||||||
|
values = [(v.name, v.value()) for v in vs if v.value() is not None]
|
||||||
|
for index, (name, value) in enumerate(values):
|
||||||
|
attrib_vars = dict(name=name, value=str(value), index=str(index))
|
||||||
|
et.SubElement(variables, "variable", attrib=attrib_vars)
|
||||||
|
mst = et.ElementTree(root)
|
||||||
|
mst.write(filename, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class CPLEX_PY(LpSolver):
|
||||||
|
"""
|
||||||
|
The CPLEX LP/MIP solver (via a Python Binding)
|
||||||
|
|
||||||
|
This solver wraps the python api of cplex.
|
||||||
|
It has been tested against cplex 12.3.
|
||||||
|
For api functions that have not been wrapped in this solver please use
|
||||||
|
the base cplex classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "CPLEX_PY"
|
||||||
|
try:
|
||||||
|
global cplex
|
||||||
|
import cplex
|
||||||
|
except Exception as e:
|
||||||
|
err = e
|
||||||
|
"""The CPLEX LP/MIP solver from python. Something went wrong!!!!"""
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError(f"CPLEX_PY: Not Available:\n{self.err}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
warmStart=False,
|
||||||
|
logPath=None,
|
||||||
|
epgap=None,
|
||||||
|
logfilename=None,
|
||||||
|
threads=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param bool warmStart: if True, the solver will use the current value of variables as a start
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param float epgap: deprecated for gapRel
|
||||||
|
:param str logfilename: deprecated for logPath
|
||||||
|
:param int threads: number of threads to be used by CPLEX to solve a problem (default None uses all available)
|
||||||
|
"""
|
||||||
|
if epgap is not None:
|
||||||
|
warnings.warn("Parameter epgap is being depreciated for gapRel")
|
||||||
|
if gapRel is not None:
|
||||||
|
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
|
||||||
|
else:
|
||||||
|
gapRel = epgap
|
||||||
|
if logfilename is not None:
|
||||||
|
warnings.warn("Parameter logfilename is being depreciated for logPath")
|
||||||
|
if logPath is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter logPath and logfilename passed, using logPath"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logPath = logfilename
|
||||||
|
|
||||||
|
LpSolver.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
warmStart=warmStart,
|
||||||
|
logPath=logPath,
|
||||||
|
threads=threads,
|
||||||
|
)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
creates a cplex model, variables and constraints and attaches
|
||||||
|
them to the lp model which it then solves
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
# set the initial solution
|
||||||
|
log.debug("Solve the Model using cplex")
|
||||||
|
self.callSolver(lp)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp._variables:
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into a cplex model
|
||||||
|
"""
|
||||||
|
model_variables = lp.variables()
|
||||||
|
self.n2v = {var.name: var for var in model_variables}
|
||||||
|
if len(self.n2v) != len(model_variables):
|
||||||
|
raise PulpSolverError(
|
||||||
|
"Variables must have unique names for cplex solver"
|
||||||
|
)
|
||||||
|
log.debug("create the cplex model")
|
||||||
|
self.solverModel = lp.solverModel = cplex.Cplex()
|
||||||
|
log.debug("set the name of the problem")
|
||||||
|
if not self.mip:
|
||||||
|
self.solverModel.set_problem_name(lp.name)
|
||||||
|
log.debug("set the sense of the problem")
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
lp.solverModel.objective.set_sense(
|
||||||
|
lp.solverModel.objective.sense.maximize
|
||||||
|
)
|
||||||
|
obj = [float(lp.objective.get(var, 0.0)) for var in model_variables]
|
||||||
|
|
||||||
|
def cplex_var_lb(var):
|
||||||
|
if var.lowBound is not None:
|
||||||
|
return float(var.lowBound)
|
||||||
|
else:
|
||||||
|
return -cplex.infinity
|
||||||
|
|
||||||
|
lb = [cplex_var_lb(var) for var in model_variables]
|
||||||
|
|
||||||
|
def cplex_var_ub(var):
|
||||||
|
if var.upBound is not None:
|
||||||
|
return float(var.upBound)
|
||||||
|
else:
|
||||||
|
return cplex.infinity
|
||||||
|
|
||||||
|
ub = [cplex_var_ub(var) for var in model_variables]
|
||||||
|
colnames = [var.name for var in model_variables]
|
||||||
|
|
||||||
|
def cplex_var_types(var):
|
||||||
|
if var.cat == constants.LpInteger:
|
||||||
|
return "I"
|
||||||
|
else:
|
||||||
|
return "C"
|
||||||
|
|
||||||
|
ctype = [cplex_var_types(var) for var in model_variables]
|
||||||
|
ctype = "".join(ctype)
|
||||||
|
lp.solverModel.variables.add(
|
||||||
|
obj=obj, lb=lb, ub=ub, types=ctype, names=colnames
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
senses = []
|
||||||
|
rhs = []
|
||||||
|
rownames = []
|
||||||
|
for name, constraint in lp.constraints.items():
|
||||||
|
# build the expression
|
||||||
|
expr = [(var.name, float(coeff)) for var, coeff in constraint.items()]
|
||||||
|
if not expr:
|
||||||
|
# if the constraint is empty
|
||||||
|
rows.append(([], []))
|
||||||
|
else:
|
||||||
|
rows.append(list(zip(*expr)))
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
senses.append("L")
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
senses.append("G")
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
senses.append("E")
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
rownames.append(name)
|
||||||
|
rhs.append(float(-constraint.constant))
|
||||||
|
lp.solverModel.linear_constraints.add(
|
||||||
|
lin_expr=rows, senses=senses, rhs=rhs, names=rownames
|
||||||
|
)
|
||||||
|
log.debug("set the type of the problem")
|
||||||
|
if not self.mip:
|
||||||
|
self.solverModel.set_problem_type(cplex.Cplex.problem_type.LP)
|
||||||
|
log.debug("set the logging")
|
||||||
|
if not self.msg:
|
||||||
|
self.setlogfile(None)
|
||||||
|
logPath = self.optionsDict.get("logPath")
|
||||||
|
if logPath is not None:
|
||||||
|
if self.msg:
|
||||||
|
warnings.warn(
|
||||||
|
"`logPath` argument replaces `msg=1`. The output will be redirected to the log file."
|
||||||
|
)
|
||||||
|
self.setlogfile(open(logPath, "w"))
|
||||||
|
gapRel = self.optionsDict.get("gapRel")
|
||||||
|
if gapRel is not None:
|
||||||
|
self.changeEpgap(gapRel)
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
self.setTimeLimit(self.timeLimit)
|
||||||
|
self.setThreads(self.optionsDict.get("threads", None))
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
# We assume "auto" for the effort_level
|
||||||
|
effort = self.solverModel.MIP_starts.effort_level.auto
|
||||||
|
start = [
|
||||||
|
(k, v.value()) for k, v in self.n2v.items() if v.value() is not None
|
||||||
|
]
|
||||||
|
if not start:
|
||||||
|
warnings.warn("No variable with value found: mipStart aborted")
|
||||||
|
return
|
||||||
|
ind, val = zip(*start)
|
||||||
|
self.solverModel.MIP_starts.add(
|
||||||
|
cplex.SparsePair(ind=ind, val=val), effort, "1"
|
||||||
|
)
|
||||||
|
|
||||||
|
def setlogfile(self, fileobj):
|
||||||
|
"""
|
||||||
|
sets the logfile for cplex output
|
||||||
|
"""
|
||||||
|
self.solverModel.set_error_stream(fileobj)
|
||||||
|
self.solverModel.set_log_stream(fileobj)
|
||||||
|
self.solverModel.set_warning_stream(fileobj)
|
||||||
|
self.solverModel.set_results_stream(fileobj)
|
||||||
|
|
||||||
|
def setThreads(self, threads=None):
|
||||||
|
"""
|
||||||
|
Change cplex thread count used (None is default which uses all available resources)
|
||||||
|
"""
|
||||||
|
self.solverModel.parameters.threads.set(threads or 0)
|
||||||
|
|
||||||
|
def changeEpgap(self, epgap=10**-4):
|
||||||
|
"""
|
||||||
|
Change cplex solver integer bound gap tolerence
|
||||||
|
"""
|
||||||
|
self.solverModel.parameters.mip.tolerances.mipgap.set(epgap)
|
||||||
|
|
||||||
|
def setTimeLimit(self, timeLimit=0.0):
|
||||||
|
"""
|
||||||
|
Make cplex limit the time it takes --added CBM 8/28/09
|
||||||
|
"""
|
||||||
|
self.solverModel.parameters.timelimit.set(timeLimit)
|
||||||
|
|
||||||
|
def callSolver(self, isMIP):
|
||||||
|
"""Solves the problem with cplex"""
|
||||||
|
# solve the problem
|
||||||
|
self.solveTime = -clock()
|
||||||
|
self.solverModel.solve()
|
||||||
|
self.solveTime += clock()
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
CplexLpStatus = {
|
||||||
|
lp.solverModel.solution.status.MIP_optimal: constants.LpStatusOptimal,
|
||||||
|
lp.solverModel.solution.status.optimal: constants.LpStatusOptimal,
|
||||||
|
lp.solverModel.solution.status.optimal_tolerance: constants.LpStatusOptimal,
|
||||||
|
lp.solverModel.solution.status.infeasible: constants.LpStatusInfeasible,
|
||||||
|
lp.solverModel.solution.status.infeasible_or_unbounded: constants.LpStatusInfeasible,
|
||||||
|
lp.solverModel.solution.status.MIP_infeasible: constants.LpStatusInfeasible,
|
||||||
|
lp.solverModel.solution.status.MIP_infeasible_or_unbounded: constants.LpStatusInfeasible,
|
||||||
|
lp.solverModel.solution.status.unbounded: constants.LpStatusUnbounded,
|
||||||
|
lp.solverModel.solution.status.MIP_unbounded: constants.LpStatusUnbounded,
|
||||||
|
lp.solverModel.solution.status.abort_dual_obj_limit: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.abort_iteration_limit: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.abort_obj_limit: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.abort_relaxed: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.abort_time_limit: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.abort_user: constants.LpStatusNotSolved,
|
||||||
|
lp.solverModel.solution.status.MIP_abort_feasible: constants.LpStatusOptimal,
|
||||||
|
lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpStatusOptimal,
|
||||||
|
lp.solverModel.solution.status.MIP_time_limit_infeasible: constants.LpStatusInfeasible,
|
||||||
|
}
|
||||||
|
lp.cplex_status = lp.solverModel.solution.get_status()
|
||||||
|
status = CplexLpStatus.get(lp.cplex_status, constants.LpStatusUndefined)
|
||||||
|
CplexSolStatus = {
|
||||||
|
lp.solverModel.solution.status.MIP_time_limit_feasible: constants.LpSolutionIntegerFeasible,
|
||||||
|
lp.solverModel.solution.status.MIP_abort_feasible: constants.LpSolutionIntegerFeasible,
|
||||||
|
lp.solverModel.solution.status.MIP_feasible: constants.LpSolutionIntegerFeasible,
|
||||||
|
}
|
||||||
|
# TODO: I did not find the following status: CPXMIP_NODE_LIM_FEAS, CPXMIP_MEM_LIM_FEAS
|
||||||
|
sol_status = CplexSolStatus.get(lp.cplex_status)
|
||||||
|
lp.assignStatus(status, sol_status)
|
||||||
|
var_names = [var.name for var in lp._variables]
|
||||||
|
con_names = [con for con in lp.constraints]
|
||||||
|
try:
|
||||||
|
objectiveValue = lp.solverModel.solution.get_objective_value()
|
||||||
|
variablevalues = dict(
|
||||||
|
zip(var_names, lp.solverModel.solution.get_values(var_names))
|
||||||
|
)
|
||||||
|
lp.assignVarsVals(variablevalues)
|
||||||
|
constraintslackvalues = dict(
|
||||||
|
zip(con_names, lp.solverModel.solution.get_linear_slacks(con_names))
|
||||||
|
)
|
||||||
|
lp.assignConsSlack(constraintslackvalues)
|
||||||
|
if lp.solverModel.get_problem_type() == cplex.Cplex.problem_type.LP:
|
||||||
|
variabledjvalues = dict(
|
||||||
|
zip(
|
||||||
|
var_names,
|
||||||
|
lp.solverModel.solution.get_reduced_costs(var_names),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lp.assignVarsDj(variabledjvalues)
|
||||||
|
constraintpivalues = dict(
|
||||||
|
zip(
|
||||||
|
con_names,
|
||||||
|
lp.solverModel.solution.get_dual_values(con_names),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lp.assignConsPi(constraintpivalues)
|
||||||
|
except cplex.exceptions.CplexSolverError:
|
||||||
|
# raises this error when there is no solution
|
||||||
|
pass
|
||||||
|
# put pi and slack variables against the constraints
|
||||||
|
# TODO: clear up the name of self.n2c
|
||||||
|
if self.msg:
|
||||||
|
print("Cplex status=", lp.cplex_status)
|
||||||
|
lp.resolveOK = True
|
||||||
|
for var in lp._variables:
|
||||||
|
var.isModified = False
|
||||||
|
return status
|
||||||
|
|
||||||
|
def actualResolve(self, lp, **kwargs):
|
||||||
|
"""
|
||||||
|
looks at which variables have been modified and changes them
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Resolves in CPLEX_PY not yet implemented")
|
||||||
|
|
||||||
|
|
||||||
|
CPLEX = CPLEX_CMD
|
||||||
409
utils/pulp/apis/glpk_api.py
Normal file
409
utils/pulp/apis/glpk_api.py
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock
|
||||||
|
from .core import glpk_path, operating_system, log
|
||||||
|
import os
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
|
||||||
|
class GLPK_CMD(LpSolver_CMD):
|
||||||
|
"""The GLPK LP solver"""
|
||||||
|
|
||||||
|
name = "GLPK_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
keepFiles=False,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension(glpk_path)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpLp, tmpSol = self.create_tmp_files(lp.name, "lp", "sol")
|
||||||
|
lp.writeLP(tmpLp, writeSOS=0)
|
||||||
|
proc = ["glpsol", "--cpxlp", tmpLp, "-o", tmpSol]
|
||||||
|
if self.timeLimit:
|
||||||
|
proc.extend(["--tmlim", str(self.timeLimit)])
|
||||||
|
if not self.mip:
|
||||||
|
proc.append("--nomip")
|
||||||
|
proc.extend(self.options)
|
||||||
|
|
||||||
|
self.solution_time = clock()
|
||||||
|
if not self.msg:
|
||||||
|
proc[0] = self.path
|
||||||
|
pipe = open(os.devnull, "w")
|
||||||
|
if operating_system == "win":
|
||||||
|
# Prevent flashing windows if used from a GUI application
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
rc = subprocess.call(
|
||||||
|
proc, stdout=pipe, stderr=pipe, startupinfo=startupinfo
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rc = subprocess.call(proc, stdout=pipe, stderr=pipe)
|
||||||
|
if rc:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"PuLP: Error while trying to execute " + self.path
|
||||||
|
)
|
||||||
|
pipe.close()
|
||||||
|
else:
|
||||||
|
if os.name != "nt":
|
||||||
|
rc = os.spawnvp(os.P_WAIT, self.path, proc)
|
||||||
|
else:
|
||||||
|
rc = os.spawnv(os.P_WAIT, self.executable(self.path), proc)
|
||||||
|
if rc == 127:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"PuLP: Error while trying to execute " + self.path
|
||||||
|
)
|
||||||
|
self.solution_time += clock()
|
||||||
|
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
raise PulpSolverError("PuLP: Error while executing " + self.path)
|
||||||
|
status, values = self.readsol(tmpSol)
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpSol)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def readsol(self, filename):
|
||||||
|
"""Read a GLPK solution file"""
|
||||||
|
with open(filename) as f:
|
||||||
|
f.readline()
|
||||||
|
rows = int(f.readline().split()[1])
|
||||||
|
cols = int(f.readline().split()[1])
|
||||||
|
f.readline()
|
||||||
|
statusString = f.readline()[12:-1]
|
||||||
|
glpkStatus = {
|
||||||
|
"INTEGER OPTIMAL": constants.LpStatusOptimal,
|
||||||
|
"INTEGER NON-OPTIMAL": constants.LpStatusOptimal,
|
||||||
|
"OPTIMAL": constants.LpStatusOptimal,
|
||||||
|
"INFEASIBLE (FINAL)": constants.LpStatusInfeasible,
|
||||||
|
"INTEGER UNDEFINED": constants.LpStatusUndefined,
|
||||||
|
"UNBOUNDED": constants.LpStatusUnbounded,
|
||||||
|
"UNDEFINED": constants.LpStatusUndefined,
|
||||||
|
"INTEGER EMPTY": constants.LpStatusInfeasible,
|
||||||
|
}
|
||||||
|
if statusString not in glpkStatus:
|
||||||
|
raise PulpSolverError("Unknown status returned by GLPK")
|
||||||
|
status = glpkStatus[statusString]
|
||||||
|
isInteger = statusString in [
|
||||||
|
"INTEGER NON-OPTIMAL",
|
||||||
|
"INTEGER OPTIMAL",
|
||||||
|
"INTEGER UNDEFINED",
|
||||||
|
"INTEGER EMPTY",
|
||||||
|
]
|
||||||
|
values = {}
|
||||||
|
for i in range(4):
|
||||||
|
f.readline()
|
||||||
|
for i in range(rows):
|
||||||
|
line = f.readline().split()
|
||||||
|
if len(line) == 2:
|
||||||
|
f.readline()
|
||||||
|
for i in range(3):
|
||||||
|
f.readline()
|
||||||
|
for i in range(cols):
|
||||||
|
line = f.readline().split()
|
||||||
|
name = line[1]
|
||||||
|
if len(line) == 2:
|
||||||
|
line = [0, 0] + f.readline().split()
|
||||||
|
if isInteger:
|
||||||
|
if line[2] == "*":
|
||||||
|
value = int(float(line[3]))
|
||||||
|
else:
|
||||||
|
value = float(line[2])
|
||||||
|
else:
|
||||||
|
value = float(line[3])
|
||||||
|
values[name] = value
|
||||||
|
return status, values
|
||||||
|
|
||||||
|
|
||||||
|
GLPK = GLPK_CMD
|
||||||
|
|
||||||
|
# get the glpk name in global scope
|
||||||
|
glpk = None
|
||||||
|
|
||||||
|
|
||||||
|
class PYGLPK(LpSolver):
|
||||||
|
"""
|
||||||
|
The glpk LP/MIP solver (via its python interface)
|
||||||
|
|
||||||
|
Copyright Christophe-Marie Duquesne 2012
|
||||||
|
|
||||||
|
The glpk variables are available (after a solve) in var.solverVar
|
||||||
|
The glpk constraints are available in constraint.solverConstraint
|
||||||
|
The Model is in prob.solverModel
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "PYGLPK"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# import the model into the global scope
|
||||||
|
global glpk
|
||||||
|
import glpk.glpkpi as glpk
|
||||||
|
except:
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError("GLPK: Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, mip=True, msg=True, timeLimit=None, epgap=None, **solverParams
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initializes the glpk solver.
|
||||||
|
|
||||||
|
@param mip: if False the solver will solve a MIP as an LP
|
||||||
|
@param msg: displays information from the solver to stdout
|
||||||
|
@param timeLimit: not handled
|
||||||
|
@param epgap: not handled
|
||||||
|
@param solverParams: not handled
|
||||||
|
"""
|
||||||
|
LpSolver.__init__(self, mip, msg)
|
||||||
|
if not self.msg:
|
||||||
|
glpk.glp_term_out(glpk.GLP_OFF)
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
prob = lp.solverModel
|
||||||
|
if self.mip and self.hasMIPConstraints(lp.solverModel):
|
||||||
|
solutionStatus = glpk.glp_mip_status(prob)
|
||||||
|
else:
|
||||||
|
solutionStatus = glpk.glp_get_status(prob)
|
||||||
|
glpkLpStatus = {
|
||||||
|
glpk.GLP_OPT: constants.LpStatusOptimal,
|
||||||
|
glpk.GLP_UNDEF: constants.LpStatusUndefined,
|
||||||
|
glpk.GLP_FEAS: constants.LpStatusOptimal,
|
||||||
|
glpk.GLP_INFEAS: constants.LpStatusInfeasible,
|
||||||
|
glpk.GLP_NOFEAS: constants.LpStatusInfeasible,
|
||||||
|
glpk.GLP_UNBND: constants.LpStatusUnbounded,
|
||||||
|
}
|
||||||
|
# populate pulp solution values
|
||||||
|
for var in lp.variables():
|
||||||
|
if self.mip and self.hasMIPConstraints(lp.solverModel):
|
||||||
|
var.varValue = glpk.glp_mip_col_val(prob, var.glpk_index)
|
||||||
|
else:
|
||||||
|
var.varValue = glpk.glp_get_col_prim(prob, var.glpk_index)
|
||||||
|
var.dj = glpk.glp_get_col_dual(prob, var.glpk_index)
|
||||||
|
# put pi and slack variables against the constraints
|
||||||
|
for constr in lp.constraints.values():
|
||||||
|
if self.mip and self.hasMIPConstraints(lp.solverModel):
|
||||||
|
row_val = glpk.glp_mip_row_val(prob, constr.glpk_index)
|
||||||
|
else:
|
||||||
|
row_val = glpk.glp_get_row_prim(prob, constr.glpk_index)
|
||||||
|
constr.slack = -constr.constant - row_val
|
||||||
|
constr.pi = glpk.glp_get_row_dual(prob, constr.glpk_index)
|
||||||
|
lp.resolveOK = True
|
||||||
|
for var in lp.variables():
|
||||||
|
var.isModified = False
|
||||||
|
status = glpkLpStatus.get(solutionStatus, constants.LpStatusUndefined)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def hasMIPConstraints(self, solverModel):
|
||||||
|
return (
|
||||||
|
glpk.glp_get_num_int(solverModel) > 0
|
||||||
|
or glpk.glp_get_num_bin(solverModel) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def callSolver(self, lp, callback=None):
|
||||||
|
"""Solves the problem with glpk"""
|
||||||
|
self.solveTime = -clock()
|
||||||
|
glpk.glp_adv_basis(lp.solverModel, 0)
|
||||||
|
glpk.glp_simplex(lp.solverModel, None)
|
||||||
|
if self.mip and self.hasMIPConstraints(lp.solverModel):
|
||||||
|
status = glpk.glp_get_status(lp.solverModel)
|
||||||
|
if status in (glpk.GLP_OPT, glpk.GLP_UNDEF, glpk.GLP_FEAS):
|
||||||
|
glpk.glp_intopt(lp.solverModel, None)
|
||||||
|
self.solveTime += clock()
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into a glpk model
|
||||||
|
"""
|
||||||
|
log.debug("create the glpk model")
|
||||||
|
prob = glpk.glp_create_prob()
|
||||||
|
glpk.glp_set_prob_name(prob, lp.name)
|
||||||
|
log.debug("set the sense of the problem")
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
glpk.glp_set_obj_dir(prob, glpk.GLP_MAX)
|
||||||
|
log.debug("add the constraints to the problem")
|
||||||
|
glpk.glp_add_rows(prob, len(list(lp.constraints.keys())))
|
||||||
|
for i, v in enumerate(lp.constraints.items(), start=1):
|
||||||
|
name, constraint = v
|
||||||
|
glpk.glp_set_row_name(prob, i, name)
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob, i, glpk.GLP_UP, 0.0, -constraint.constant
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob, i, glpk.GLP_LO, -constraint.constant, 0.0
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob, i, glpk.GLP_FX, -constraint.constant, -constraint.constant
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
constraint.glpk_index = i
|
||||||
|
log.debug("add the variables to the problem")
|
||||||
|
glpk.glp_add_cols(prob, len(lp.variables()))
|
||||||
|
for j, var in enumerate(lp.variables(), start=1):
|
||||||
|
glpk.glp_set_col_name(prob, j, var.name)
|
||||||
|
lb = 0.0
|
||||||
|
ub = 0.0
|
||||||
|
t = glpk.GLP_FR
|
||||||
|
if not var.lowBound is None:
|
||||||
|
lb = var.lowBound
|
||||||
|
t = glpk.GLP_LO
|
||||||
|
if not var.upBound is None:
|
||||||
|
ub = var.upBound
|
||||||
|
t = glpk.GLP_UP
|
||||||
|
if not var.upBound is None and not var.lowBound is None:
|
||||||
|
if ub == lb:
|
||||||
|
t = glpk.GLP_FX
|
||||||
|
else:
|
||||||
|
t = glpk.GLP_DB
|
||||||
|
glpk.glp_set_col_bnds(prob, j, t, lb, ub)
|
||||||
|
if var.cat == constants.LpInteger:
|
||||||
|
glpk.glp_set_col_kind(prob, j, glpk.GLP_IV)
|
||||||
|
assert glpk.glp_get_col_kind(prob, j) == glpk.GLP_IV
|
||||||
|
var.glpk_index = j
|
||||||
|
log.debug("set the objective function")
|
||||||
|
for var in lp.variables():
|
||||||
|
value = lp.objective.get(var)
|
||||||
|
if value:
|
||||||
|
glpk.glp_set_obj_coef(prob, var.glpk_index, value)
|
||||||
|
log.debug("set the problem matrix")
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
l = len(list(constraint.items()))
|
||||||
|
ind = glpk.intArray(l + 1)
|
||||||
|
val = glpk.doubleArray(l + 1)
|
||||||
|
for j, v in enumerate(constraint.items(), start=1):
|
||||||
|
var, value = v
|
||||||
|
ind[j] = var.glpk_index
|
||||||
|
val[j] = value
|
||||||
|
glpk.glp_set_mat_row(prob, constraint.glpk_index, l, ind, val)
|
||||||
|
lp.solverModel = prob
|
||||||
|
# glpk.glp_write_lp(prob, None, "glpk.lp")
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
creates a glpk model, variables and constraints and attaches
|
||||||
|
them to the lp model which it then solves
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
# set the initial solution
|
||||||
|
log.debug("Solve the Model using glpk")
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def actualResolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
uses the old solver and modifies the rhs of the modified
|
||||||
|
constraints
|
||||||
|
"""
|
||||||
|
prob = lp.solverModel
|
||||||
|
log.debug("Resolve the Model using glpk")
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
i = constraint.glpk_index
|
||||||
|
if constraint.modified:
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob, i, glpk.GLP_UP, 0.0, -constraint.constant
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob, i, glpk.GLP_LO, -constraint.constant, 0.0
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
glpk.glp_set_row_bnds(
|
||||||
|
prob,
|
||||||
|
i,
|
||||||
|
glpk.GLP_FX,
|
||||||
|
-constraint.constant,
|
||||||
|
-constraint.constant,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
566
utils/pulp/apis/gurobi_api.py
Normal file
566
utils/pulp/apis/gurobi_api.py
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError, clock, log
|
||||||
|
from .core import gurobi_path
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from .. import constants
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
# to import the gurobipy name into the module scope
|
||||||
|
gp = None
|
||||||
|
|
||||||
|
|
||||||
|
class GUROBI(LpSolver):
|
||||||
|
"""
|
||||||
|
The Gurobi LP/MIP solver (via its python interface)
|
||||||
|
|
||||||
|
The Gurobi variables are available (after a solve) in var.solverVar
|
||||||
|
Constraints in constraint.solverConstraint
|
||||||
|
and the Model is in prob.solverModel
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "GUROBI"
|
||||||
|
env = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.path.append(gurobi_path)
|
||||||
|
# to import the name into the module scope
|
||||||
|
global gp
|
||||||
|
import gurobipy as gp
|
||||||
|
except: # FIXME: Bug because gurobi returns
|
||||||
|
# a gurobi exception on failed imports
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError("GUROBI: Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
epgap=None,
|
||||||
|
gapRel=None,
|
||||||
|
warmStart=False,
|
||||||
|
logPath=None,
|
||||||
|
env=None,
|
||||||
|
envOptions=None,
|
||||||
|
manageEnv=False,
|
||||||
|
**solverParams,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param bool warmStart: if True, the solver will use the current value of variables as a start
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param float epgap: deprecated for gapRel
|
||||||
|
:param gp.Env env: Gurobi environment to use. Default None.
|
||||||
|
:param dict envOptions: environment options.
|
||||||
|
:param bool manageEnv: if False, assume the environment is handled by the user.
|
||||||
|
|
||||||
|
|
||||||
|
If ``manageEnv`` is set to True, the ``GUROBI`` object creates a
|
||||||
|
local Gurobi environment and manages all associated Gurobi
|
||||||
|
resources. Importantly, this enables Gurobi licenses to be freed
|
||||||
|
and connections terminated when the ``.close()`` function is called
|
||||||
|
(this function always disposes of the Gurobi model, and the
|
||||||
|
environment)::
|
||||||
|
|
||||||
|
solver = GUROBI(manageEnv=True)
|
||||||
|
prob.solve(solver)
|
||||||
|
solver.close() # Must be called to free Gurobi resources.
|
||||||
|
# All Gurobi models and environments are freed
|
||||||
|
|
||||||
|
``manageEnv=True`` is required when setting license or connection
|
||||||
|
parameters. The ``envOptions`` argument is used to pass parameters
|
||||||
|
to the Gurobi environment. For example, to connect to a Gurobi
|
||||||
|
Cluster Manager::
|
||||||
|
|
||||||
|
options = {
|
||||||
|
"CSManager": "<url>",
|
||||||
|
"CSAPIAccessID": "<access-id>",
|
||||||
|
"CSAPISecret": "<api-key>",
|
||||||
|
}
|
||||||
|
solver = GUROBI(manageEnv=True, envOptions=options)
|
||||||
|
solver.close()
|
||||||
|
# Compute server connection terminated
|
||||||
|
|
||||||
|
Alternatively, one can also pass a ``gp.Env`` object. In this case,
|
||||||
|
to be safe, one should still call ``.close()`` to dispose of the
|
||||||
|
model::
|
||||||
|
|
||||||
|
with gp.Env(params=options) as env:
|
||||||
|
# Pass environment as a parameter
|
||||||
|
solver = GUROBI(env=env)
|
||||||
|
prob.solve(solver)
|
||||||
|
solver.close()
|
||||||
|
# Still call `close` as this disposes the model which is required to correctly free env
|
||||||
|
|
||||||
|
If ``manageEnv`` is set to False (the default), the ``GUROBI``
|
||||||
|
object uses the global default Gurobi environment which will be
|
||||||
|
freed once the object is deleted. In this case, one can still call
|
||||||
|
``.close()`` to dispose of the model::
|
||||||
|
|
||||||
|
solver = GUROBI()
|
||||||
|
prob.solve(solver)
|
||||||
|
# The global default environment and model remain active
|
||||||
|
solver.close()
|
||||||
|
# Only the global default environment remains active
|
||||||
|
"""
|
||||||
|
self.env = env
|
||||||
|
self.env_options = envOptions if envOptions else {}
|
||||||
|
self.manage_env = False if self.env is not None else manageEnv
|
||||||
|
self.solver_params = solverParams
|
||||||
|
|
||||||
|
self.model = None
|
||||||
|
self.init_gurobi = False # whether env and model have been initialised
|
||||||
|
|
||||||
|
if epgap is not None:
|
||||||
|
warnings.warn("Parameter epgap is being depreciated for gapRel")
|
||||||
|
if gapRel is not None:
|
||||||
|
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
|
||||||
|
else:
|
||||||
|
gapRel = epgap
|
||||||
|
|
||||||
|
LpSolver.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
gapRel=gapRel,
|
||||||
|
logPath=logPath,
|
||||||
|
warmStart=warmStart,
|
||||||
|
)
|
||||||
|
|
||||||
|
# set the output of gurobi
|
||||||
|
if not self.msg:
|
||||||
|
if self.manage_env:
|
||||||
|
self.env_options["OutputFlag"] = 0
|
||||||
|
else:
|
||||||
|
self.env_options["OutputFlag"] = 0
|
||||||
|
self.solver_params["OutputFlag"] = 0
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Must be called when internal Gurobi model and/or environment
|
||||||
|
requires disposing. The environment (default or otherwise) will be
|
||||||
|
disposed only if ``manageEnv`` is set to True.
|
||||||
|
"""
|
||||||
|
if not self.init_gurobi:
|
||||||
|
return
|
||||||
|
self.model.dispose()
|
||||||
|
if self.manage_env:
|
||||||
|
self.env.dispose()
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
model = lp.solverModel
|
||||||
|
solutionStatus = model.Status
|
||||||
|
GRB = gp.GRB
|
||||||
|
# TODO: check status for Integer Feasible
|
||||||
|
gurobiLpStatus = {
|
||||||
|
GRB.OPTIMAL: constants.LpStatusOptimal,
|
||||||
|
GRB.INFEASIBLE: constants.LpStatusInfeasible,
|
||||||
|
GRB.INF_OR_UNBD: constants.LpStatusInfeasible,
|
||||||
|
GRB.UNBOUNDED: constants.LpStatusUnbounded,
|
||||||
|
GRB.ITERATION_LIMIT: constants.LpStatusNotSolved,
|
||||||
|
GRB.NODE_LIMIT: constants.LpStatusNotSolved,
|
||||||
|
GRB.TIME_LIMIT: constants.LpStatusNotSolved,
|
||||||
|
GRB.SOLUTION_LIMIT: constants.LpStatusNotSolved,
|
||||||
|
GRB.INTERRUPTED: constants.LpStatusNotSolved,
|
||||||
|
GRB.NUMERIC: constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
if self.msg:
|
||||||
|
print("Gurobi status=", solutionStatus)
|
||||||
|
lp.resolveOK = True
|
||||||
|
for var in lp._variables:
|
||||||
|
var.isModified = False
|
||||||
|
status = gurobiLpStatus.get(solutionStatus, constants.LpStatusUndefined)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
if model.SolCount >= 1:
|
||||||
|
# populate pulp solution values
|
||||||
|
for var, value in zip(
|
||||||
|
lp._variables, model.getAttr(GRB.Attr.X, model.getVars())
|
||||||
|
):
|
||||||
|
var.varValue = value
|
||||||
|
# populate pulp constraints slack
|
||||||
|
for constr, value in zip(
|
||||||
|
lp.constraints.values(),
|
||||||
|
model.getAttr(GRB.Attr.Slack, model.getConstrs()),
|
||||||
|
):
|
||||||
|
constr.slack = value
|
||||||
|
# put pi and slack variables against the constraints
|
||||||
|
if not model.IsMIP:
|
||||||
|
for var, value in zip(
|
||||||
|
lp._variables, model.getAttr(GRB.Attr.RC, model.getVars())
|
||||||
|
):
|
||||||
|
var.dj = value
|
||||||
|
|
||||||
|
for constr, value in zip(
|
||||||
|
lp.constraints.values(),
|
||||||
|
model.getAttr(GRB.Attr.Pi, model.getConstrs()),
|
||||||
|
):
|
||||||
|
constr.pi = value
|
||||||
|
return status
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
try:
|
||||||
|
with gp.Env(params=self.env_options):
|
||||||
|
pass
|
||||||
|
except gurobipy.GurobiError as e:
|
||||||
|
warnings.warn(f"GUROBI error: {e}.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def initGurobi(self):
|
||||||
|
if self.init_gurobi:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.init_gurobi = True
|
||||||
|
try:
|
||||||
|
if self.manage_env:
|
||||||
|
self.env = gp.Env(params=self.env_options)
|
||||||
|
self.model = gp.Model(env=self.env)
|
||||||
|
# Environment handled by user or default Env
|
||||||
|
else:
|
||||||
|
self.model = gp.Model(env=self.env)
|
||||||
|
# Set solver parameters
|
||||||
|
for param, value in self.solver_params.items():
|
||||||
|
self.model.setParam(param, value)
|
||||||
|
except gp.GurobiError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def callSolver(self, lp, callback=None):
|
||||||
|
"""Solves the problem with gurobi"""
|
||||||
|
# solve the problem
|
||||||
|
self.solveTime = -clock()
|
||||||
|
lp.solverModel.optimize(callback=callback)
|
||||||
|
self.solveTime += clock()
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into a gurobi model
|
||||||
|
"""
|
||||||
|
log.debug("create the gurobi model")
|
||||||
|
self.initGurobi()
|
||||||
|
self.model.ModelName = lp.name
|
||||||
|
lp.solverModel = self.model
|
||||||
|
log.debug("set the sense of the problem")
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
lp.solverModel.setAttr("ModelSense", -1)
|
||||||
|
if self.timeLimit:
|
||||||
|
lp.solverModel.setParam("TimeLimit", self.timeLimit)
|
||||||
|
gapRel = self.optionsDict.get("gapRel")
|
||||||
|
logPath = self.optionsDict.get("logPath")
|
||||||
|
if gapRel:
|
||||||
|
lp.solverModel.setParam("MIPGap", gapRel)
|
||||||
|
if logPath:
|
||||||
|
lp.solverModel.setParam("LogFile", logPath)
|
||||||
|
|
||||||
|
log.debug("add the variables to the problem")
|
||||||
|
lp.solverModel.update()
|
||||||
|
nvars = lp.solverModel.NumVars
|
||||||
|
for var in lp.variables():
|
||||||
|
lowBound = var.lowBound
|
||||||
|
if lowBound is None:
|
||||||
|
lowBound = -gp.GRB.INFINITY
|
||||||
|
upBound = var.upBound
|
||||||
|
if upBound is None:
|
||||||
|
upBound = gp.GRB.INFINITY
|
||||||
|
obj = lp.objective.get(var, 0.0)
|
||||||
|
varType = gp.GRB.CONTINUOUS
|
||||||
|
if var.cat == constants.LpInteger and self.mip:
|
||||||
|
varType = gp.GRB.INTEGER
|
||||||
|
# only add variable once, ow new variable will be created.
|
||||||
|
if not hasattr(var, "solverVar") or nvars == 0:
|
||||||
|
var.solverVar = lp.solverModel.addVar(
|
||||||
|
lowBound, upBound, vtype=varType, obj=obj, name=var.name
|
||||||
|
)
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
# Once lp.variables() has been used at least once in the building of the model.
|
||||||
|
# we can use the lp._variables with the cache.
|
||||||
|
for var in lp._variables:
|
||||||
|
if var.varValue is not None:
|
||||||
|
var.solverVar.start = var.varValue
|
||||||
|
|
||||||
|
lp.solverModel.update()
|
||||||
|
log.debug("add the Constraints to the problem")
|
||||||
|
for name, constraint in lp.constraints.items():
|
||||||
|
# build the expression
|
||||||
|
expr = gp.LinExpr(
|
||||||
|
list(constraint.values()), [v.solverVar for v in constraint.keys()]
|
||||||
|
)
|
||||||
|
if constraint.sense == constants.LpConstraintLE:
|
||||||
|
constraint.solverConstraint = lp.solverModel.addConstr(
|
||||||
|
expr <= -constraint.constant, name=name
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintGE:
|
||||||
|
constraint.solverConstraint = lp.solverModel.addConstr(
|
||||||
|
expr >= -constraint.constant, name=name
|
||||||
|
)
|
||||||
|
elif constraint.sense == constants.LpConstraintEQ:
|
||||||
|
constraint.solverConstraint = lp.solverModel.addConstr(
|
||||||
|
expr == -constraint.constant, name=name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Detected an invalid constraint type")
|
||||||
|
lp.solverModel.update()
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
creates a gurobi model, variables and constraints and attaches
|
||||||
|
them to the lp model which it then solves
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
# set the initial solution
|
||||||
|
log.debug("Solve the Model using gurobi")
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp._variables:
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def actualResolve(self, lp, callback=None):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
uses the old solver and modifies the rhs of the modified constraints
|
||||||
|
"""
|
||||||
|
log.debug("Resolve the Model using gurobi")
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
if constraint.modified:
|
||||||
|
constraint.solverConstraint.setAttr(
|
||||||
|
gp.GRB.Attr.RHS, -constraint.constant
|
||||||
|
)
|
||||||
|
lp.solverModel.update()
|
||||||
|
self.callSolver(lp, callback=callback)
|
||||||
|
# get the solution information
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for var in lp._variables:
|
||||||
|
var.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
|
||||||
|
class GUROBI_CMD(LpSolver_CMD):
|
||||||
|
"""The GUROBI_CMD solver"""
|
||||||
|
|
||||||
|
name = "GUROBI_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
options=None,
|
||||||
|
warmStart=False,
|
||||||
|
keepFiles=False,
|
||||||
|
path=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
mip_start=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool warmStart: if True, the solver will use the current value of variables as a start
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param bool mip_start: deprecated for warmStart
|
||||||
|
"""
|
||||||
|
if mip_start:
|
||||||
|
warnings.warn("Parameter mip_start is being depreciated for warmStart")
|
||||||
|
if warmStart:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter warmStart and mip_start passed, using warmStart"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warmStart = mip_start
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
warmStart=warmStart,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
threads=threads,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
logPath=logPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("gurobi_cl")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
return False
|
||||||
|
# we execute gurobi once to check the return code.
|
||||||
|
# this is to test that the license is active
|
||||||
|
result = subprocess.Popen(
|
||||||
|
self.path, stdout=subprocess.PIPE, universal_newlines=True
|
||||||
|
)
|
||||||
|
out, err = result.communicate()
|
||||||
|
if result.returncode == 0:
|
||||||
|
# normal execution
|
||||||
|
return True
|
||||||
|
# error: we display the gurobi message
|
||||||
|
warnings.warn(f"GUROBI error: {out}.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpLp, tmpSol, tmpMst = self.create_tmp_files(lp.name, "lp", "sol", "mst")
|
||||||
|
vs = lp.writeLP(tmpLp, writeSOS=1)
|
||||||
|
try:
|
||||||
|
os.remove(tmpSol)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
cmd = self.path
|
||||||
|
options = self.options + self.getOptions()
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
options.append(("TimeLimit", self.timeLimit))
|
||||||
|
cmd += " " + " ".join([f"{key}={value}" for key, value in options])
|
||||||
|
cmd += f" ResultFile={tmpSol}"
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
self.writesol(filename=tmpMst, vs=vs)
|
||||||
|
cmd += f" InputFile={tmpMst}"
|
||||||
|
|
||||||
|
if lp.isMIP():
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn("GUROBI_CMD does not allow a problem to be relaxed")
|
||||||
|
cmd += f" {tmpLp}"
|
||||||
|
if self.msg:
|
||||||
|
pipe = None
|
||||||
|
else:
|
||||||
|
pipe = open(os.devnull, "w")
|
||||||
|
|
||||||
|
return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe)
|
||||||
|
|
||||||
|
# Close the pipe now if we used it.
|
||||||
|
if pipe is not None:
|
||||||
|
pipe.close()
|
||||||
|
|
||||||
|
if return_code != 0:
|
||||||
|
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
# TODO: the status should be infeasible here, I think
|
||||||
|
status = constants.LpStatusNotSolved
|
||||||
|
values = reducedCosts = shadowPrices = slacks = None
|
||||||
|
else:
|
||||||
|
# TODO: the status should be infeasible here, I think
|
||||||
|
status, values, reducedCosts, shadowPrices, slacks = self.readsol(tmpSol)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpMst, tmpSol, "gurobi.log")
|
||||||
|
if status != constants.LpStatusInfeasible:
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
lp.assignVarsDj(reducedCosts)
|
||||||
|
lp.assignConsPi(shadowPrices)
|
||||||
|
lp.assignConsSlack(slacks)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
def readsol(self, filename):
|
||||||
|
"""Read a Gurobi solution file"""
|
||||||
|
with open(filename) as my_file:
|
||||||
|
try:
|
||||||
|
next(my_file) # skip the objective value
|
||||||
|
except StopIteration:
|
||||||
|
# Empty file not solved
|
||||||
|
status = constants.LpStatusNotSolved
|
||||||
|
return status, {}, {}, {}, {}
|
||||||
|
# We have no idea what the status is assume optimal
|
||||||
|
# TODO: check status for Integer Feasible
|
||||||
|
status = constants.LpStatusOptimal
|
||||||
|
|
||||||
|
shadowPrices = {}
|
||||||
|
slacks = {}
|
||||||
|
shadowPrices = {}
|
||||||
|
slacks = {}
|
||||||
|
values = {}
|
||||||
|
reducedCosts = {}
|
||||||
|
for line in my_file:
|
||||||
|
if line[0] != "#": # skip comments
|
||||||
|
name, value = line.split()
|
||||||
|
values[name] = float(value)
|
||||||
|
return status, values, reducedCosts, shadowPrices, slacks
|
||||||
|
|
||||||
|
def writesol(self, filename, vs):
|
||||||
|
"""Writes a GUROBI solution file"""
|
||||||
|
|
||||||
|
values = [(v.name, v.value()) for v in vs if v.value() is not None]
|
||||||
|
rows = []
|
||||||
|
for name, value in values:
|
||||||
|
rows.append(f"{name} {value}")
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write("\n".join(rows))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getOptions(self):
|
||||||
|
# GUROBI parameters: http://www.gurobi.com/documentation/7.5/refman/parameters.html#sec:Parameters
|
||||||
|
params_eq = dict(
|
||||||
|
logPath="LogFile",
|
||||||
|
gapRel="MIPGap",
|
||||||
|
gapAbs="MIPGapAbs",
|
||||||
|
threads="Threads",
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
(v, self.optionsDict[k])
|
||||||
|
for k, v in params_eq.items()
|
||||||
|
if k in self.optionsDict and self.optionsDict[k] is not None
|
||||||
|
]
|
||||||
409
utils/pulp/apis/highs_api.py
Normal file
409
utils/pulp/apis/highs_api.py
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 2.4
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
# Modified by Sam Mathew (@samiit on Github)
|
||||||
|
# Users would need to install HiGHS on their machine and provide the path to the executable. Please look at this thread: https://github.com/ERGO-Code/HiGHS/issues/527#issuecomment-894852288
|
||||||
|
# More instructions on: https://www.highs.dev
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .core import LpSolver, LpSolver_CMD, subprocess, PulpSolverError
|
||||||
|
import os, sys
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
|
||||||
|
class HiGHS_CMD(LpSolver_CMD):
|
||||||
|
"""The HiGHS_CMD solver"""
|
||||||
|
|
||||||
|
name: str = "HiGHS_CMD"
|
||||||
|
|
||||||
|
SOLUTION_STYLE: int = 0
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
keepFiles=False,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param list[str] options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary (you can get binaries for your platform from https://github.com/JuliaBinaryWrappers/HiGHS_jll.jl/releases, or else compile from source - https://highs.dev)
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
gapRel=gapRel,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
threads=threads,
|
||||||
|
logPath=logPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("highs")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
lp.checkDuplicateVars()
|
||||||
|
|
||||||
|
tmpMps, tmpSol, tmpOptions, tmpLog = self.create_tmp_files(
|
||||||
|
lp.name, "mps", "sol", "HiGHS", "HiGHS_log"
|
||||||
|
)
|
||||||
|
lp.writeMPS(tmpMps, with_objsense=True)
|
||||||
|
|
||||||
|
file_options: List[str] = []
|
||||||
|
file_options.append(f"solution_file={tmpSol}")
|
||||||
|
file_options.append("write_solution_to_file=true")
|
||||||
|
file_options.append(f"write_solution_style={HiGHS_CMD.SOLUTION_STYLE}")
|
||||||
|
if not self.msg:
|
||||||
|
file_options.append("log_to_console=false")
|
||||||
|
if "threads" in self.optionsDict:
|
||||||
|
file_options.append(f"threads={self.optionsDict['threads']}")
|
||||||
|
if "gapRel" in self.optionsDict:
|
||||||
|
file_options.append(f"mip_rel_gap={self.optionsDict['gapRel']}")
|
||||||
|
if "gapAbs" in self.optionsDict:
|
||||||
|
file_options.append(f"mip_abs_gap={self.optionsDict['gapAbs']}")
|
||||||
|
if "logPath" in self.optionsDict:
|
||||||
|
highs_log_file = self.optionsDict["logPath"]
|
||||||
|
else:
|
||||||
|
highs_log_file = tmpLog
|
||||||
|
file_options.append(f"log_file={highs_log_file}")
|
||||||
|
|
||||||
|
command: List[str] = []
|
||||||
|
command.append(self.path)
|
||||||
|
command.append(tmpMps)
|
||||||
|
command.append(f"--options_file={tmpOptions}")
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
command.append(f"--time_limit={self.timeLimit}")
|
||||||
|
if not self.mip:
|
||||||
|
command.append("--solver=simplex")
|
||||||
|
if "threads" in self.optionsDict:
|
||||||
|
command.append("--parallel=on")
|
||||||
|
|
||||||
|
options = iter(self.options)
|
||||||
|
for option in options:
|
||||||
|
# assumption: all cli and file options require an argument which is provided after the equal sign (=)
|
||||||
|
if "=" not in option:
|
||||||
|
option += f"={next(options)}"
|
||||||
|
|
||||||
|
# identify cli options by a leading dash (-) and treat other options as file options
|
||||||
|
if option.startswith("-"):
|
||||||
|
command.append(option)
|
||||||
|
else:
|
||||||
|
file_options.append(option)
|
||||||
|
|
||||||
|
with open(tmpOptions, "w") as options_file:
|
||||||
|
options_file.write("\n".join(file_options))
|
||||||
|
process = subprocess.run(command, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
|
||||||
|
# HiGHS return code semantics (see: https://github.com/ERGO-Code/HiGHS/issues/527#issuecomment-946575028)
|
||||||
|
# - -1: error
|
||||||
|
# - 0: success
|
||||||
|
# - 1: warning
|
||||||
|
if process.returncode == -1:
|
||||||
|
raise PulpSolverError("Error while executing HiGHS")
|
||||||
|
|
||||||
|
with open(highs_log_file, "r") as log_file:
|
||||||
|
lines = log_file.readlines()
|
||||||
|
lines = [line.strip().split() for line in lines]
|
||||||
|
|
||||||
|
# LP
|
||||||
|
model_line = [line for line in lines if line[:2] == ["Model", "status"]]
|
||||||
|
if len(model_line) > 0:
|
||||||
|
model_status = " ".join(model_line[0][3:]) # Model status: ...
|
||||||
|
else:
|
||||||
|
# ILP
|
||||||
|
model_line = [line for line in lines if "Status" in line][0]
|
||||||
|
model_status = " ".join(model_line[1:])
|
||||||
|
sol_line = [line for line in lines if line[:2] == ["Solution", "status"]]
|
||||||
|
sol_line = sol_line[0] if len(sol_line) > 0 else ["Not solved"]
|
||||||
|
sol_status = sol_line[-1]
|
||||||
|
if model_status.lower() == "optimal": # optimal
|
||||||
|
status, status_sol = (
|
||||||
|
constants.LpStatusOptimal,
|
||||||
|
constants.LpSolutionOptimal,
|
||||||
|
)
|
||||||
|
elif sol_status.lower() == "feasible": # feasible
|
||||||
|
# Following the PuLP convention
|
||||||
|
status, status_sol = (
|
||||||
|
constants.LpStatusOptimal,
|
||||||
|
constants.LpSolutionIntegerFeasible,
|
||||||
|
)
|
||||||
|
elif model_status.lower() == "infeasible": # infeasible
|
||||||
|
status, status_sol = (
|
||||||
|
constants.LpStatusInfeasible,
|
||||||
|
constants.LpSolutionNoSolutionFound,
|
||||||
|
)
|
||||||
|
elif model_status.lower() == "unbounded": # unbounded
|
||||||
|
status, status_sol = (
|
||||||
|
constants.LpStatusUnbounded,
|
||||||
|
constants.LpSolutionNoSolutionFound,
|
||||||
|
)
|
||||||
|
else: # no solution
|
||||||
|
status, status_sol = (
|
||||||
|
constants.LpStatusNotSolved,
|
||||||
|
constants.LpSolutionNoSolutionFound,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(tmpSol) or os.stat(tmpSol).st_size == 0:
|
||||||
|
status_sol = constants.LpSolutionNoSolutionFound
|
||||||
|
values = None
|
||||||
|
elif status_sol == constants.LpSolutionNoSolutionFound:
|
||||||
|
values = None
|
||||||
|
else:
|
||||||
|
values = self.readsol(lp.variables(), tmpSol)
|
||||||
|
|
||||||
|
self.delete_tmp_files(tmpMps, tmpSol, tmpOptions, tmpLog)
|
||||||
|
lp.assignStatus(status, status_sol)
|
||||||
|
|
||||||
|
if status == constants.LpStatusOptimal:
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(variables, filename):
|
||||||
|
"""Read a HiGHS solution file"""
|
||||||
|
with open(filename) as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
begin, end = None, None
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if begin is None and line.startswith("# Columns"):
|
||||||
|
begin = index + 1
|
||||||
|
if end is None and line.startswith("# Rows"):
|
||||||
|
end = index
|
||||||
|
if begin is None or end is None:
|
||||||
|
raise PulpSolverError("Cannot read HiGHS solver output")
|
||||||
|
|
||||||
|
values = {}
|
||||||
|
for line in lines[begin:end]:
|
||||||
|
name, value = line.split()
|
||||||
|
values[name] = float(value)
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class HiGHS(LpSolver):
|
||||||
|
name = "HiGHS"
|
||||||
|
|
||||||
|
try:
|
||||||
|
global highspy
|
||||||
|
import highspy
|
||||||
|
except:
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError("HiGHS: Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Note(maciej): It was surprising to me that higshpy wasn't logging out of the box,
|
||||||
|
# even with the different logging options set. This callback seems to work, but there
|
||||||
|
# are probably better ways of doing this ¯\_(ツ)_/¯
|
||||||
|
DEFAULT_CALLBACK = lambda logType, logMsg, callbackValue: print(
|
||||||
|
f"[{logType.name}] {logMsg}"
|
||||||
|
)
|
||||||
|
DEFAULT_CALLBACK_VALUE = ""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
callbackTuple=None,
|
||||||
|
gapAbs=None,
|
||||||
|
gapRel=None,
|
||||||
|
threads=None,
|
||||||
|
timeLimit=None,
|
||||||
|
**solverParams,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param tuple callbackTuple: Tuple of log callback function (see DEFAULT_CALLBACK above for definition)
|
||||||
|
and callbackValue (tag embedded in every callback)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param dict solverParams: list of named options to pass directly to the HiGHS solver
|
||||||
|
"""
|
||||||
|
super().__init__(mip=mip, msg=msg, timeLimit=timeLimit, **solverParams)
|
||||||
|
self.callbackTuple = callbackTuple
|
||||||
|
self.gapAbs = gapAbs
|
||||||
|
self.gapRel = gapRel
|
||||||
|
self.threads = threads
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def callSolver(self, lp):
|
||||||
|
lp.solverModel.run()
|
||||||
|
|
||||||
|
def createAndConfigureSolver(self, lp):
|
||||||
|
lp.solverModel = highspy.Highs()
|
||||||
|
|
||||||
|
if self.msg or self.callbackTuple:
|
||||||
|
callbackTuple = self.callbackTuple or (
|
||||||
|
HiGHS.DEFAULT_CALLBACK,
|
||||||
|
HiGHS.DEFAULT_CALLBACK_VALUE,
|
||||||
|
)
|
||||||
|
lp.solverModel.setLogCallback(*callbackTuple)
|
||||||
|
|
||||||
|
if self.gapRel is not None:
|
||||||
|
lp.solverModel.setOptionValue("mip_rel_gap", self.gapRel)
|
||||||
|
|
||||||
|
if self.gapAbs is not None:
|
||||||
|
lp.solverModel.setOptionValue("mip_abs_gap", self.gapAbs)
|
||||||
|
|
||||||
|
if self.threads is not None:
|
||||||
|
lp.solverModel.setOptionValue("threads", self.threads)
|
||||||
|
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
lp.solverModel.setOptionValue("time_limit", float(self.timeLimit))
|
||||||
|
|
||||||
|
# set remaining parameter values
|
||||||
|
for key, value in self.optionsDict.items():
|
||||||
|
lp.solverModel.setOptionValue(key, value)
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
inf = highspy.kHighsInf
|
||||||
|
|
||||||
|
obj_mult = -1 if lp.sense == constants.LpMaximize else 1
|
||||||
|
|
||||||
|
for i, var in enumerate(lp.variables()):
|
||||||
|
lb = var.lowBound
|
||||||
|
ub = var.upBound
|
||||||
|
lp.solverModel.addCol(
|
||||||
|
obj_mult * lp.objective.get(var, 0.0),
|
||||||
|
-inf if lb is None else lb,
|
||||||
|
inf if ub is None else ub,
|
||||||
|
0,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
var.index = i
|
||||||
|
|
||||||
|
if var.cat == constants.LpInteger and self.mip:
|
||||||
|
lp.solverModel.changeColIntegrality(
|
||||||
|
var.index, highspy.HighsVarType.kInteger
|
||||||
|
)
|
||||||
|
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
non_zero_constraint_items = [
|
||||||
|
(var.index, coefficient)
|
||||||
|
for var, coefficient in constraint.items()
|
||||||
|
if coefficient != 0
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(non_zero_constraint_items) == 0:
|
||||||
|
indices, coefficients = [], []
|
||||||
|
else:
|
||||||
|
indices, coefficients = zip(*non_zero_constraint_items)
|
||||||
|
|
||||||
|
lb = constraint.getLb()
|
||||||
|
ub = constraint.getUb()
|
||||||
|
lp.solverModel.addRow(
|
||||||
|
-inf if lb is None else lb,
|
||||||
|
inf if ub is None else ub,
|
||||||
|
len(indices),
|
||||||
|
indices,
|
||||||
|
coefficients,
|
||||||
|
)
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
status = lp.solverModel.getModelStatus()
|
||||||
|
solution = lp.solverModel.getSolution()
|
||||||
|
HighsModelStatus = highspy.HighsModelStatus
|
||||||
|
status_dict = {
|
||||||
|
HighsModelStatus.kNotset: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kLoadError: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kModelError: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kPresolveError: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kSolveError: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kPostsolveError: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kModelEmpty: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kOptimal: constants.LpStatusOptimal,
|
||||||
|
HighsModelStatus.kInfeasible: constants.LpStatusInfeasible,
|
||||||
|
HighsModelStatus.kUnboundedOrInfeasible: constants.LpStatusInfeasible,
|
||||||
|
HighsModelStatus.kUnbounded: constants.LpStatusUnbounded,
|
||||||
|
HighsModelStatus.kObjectiveBound: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kObjectiveTarget: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kTimeLimit: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kIterationLimit: constants.LpStatusNotSolved,
|
||||||
|
HighsModelStatus.kUnknown: constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
col_values = list(solution.col_value)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.varValue = col_values[var.index]
|
||||||
|
|
||||||
|
return status_dict[status]
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
self.createAndConfigureSolver(lp)
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
self.callSolver(lp)
|
||||||
|
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modifier = False
|
||||||
|
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def actualResolve(self, lp, **kwargs):
|
||||||
|
raise PulpSolverError("HiGHS: Resolving is not supported")
|
||||||
154
utils/pulp/apis/mipcl_api.py
Normal file
154
utils/pulp/apis/mipcl_api.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, subprocess, PulpSolverError
|
||||||
|
import os
|
||||||
|
from .. import constants
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
class MIPCL_CMD(LpSolver_CMD):
|
||||||
|
"""The MIPCL_CMD solver"""
|
||||||
|
|
||||||
|
name = "MIPCL_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
keepFiles=False,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("mps_mipcl")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpMps, tmpSol = self.create_tmp_files(lp.name, "mps", "sol")
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
# we swap the objectives
|
||||||
|
# because it does not handle maximization.
|
||||||
|
warnings.warn(
|
||||||
|
"MIPCL_CMD does not allow maximization, "
|
||||||
|
"we will minimize the inverse of the objective function."
|
||||||
|
)
|
||||||
|
lp += -lp.objective
|
||||||
|
lp.checkDuplicateVars()
|
||||||
|
lp.checkLengthVars(52)
|
||||||
|
lp.writeMPS(tmpMps, mpsSense=lp.sense)
|
||||||
|
|
||||||
|
# just to report duplicated variables:
|
||||||
|
try:
|
||||||
|
os.remove(tmpSol)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
cmd = self.path
|
||||||
|
cmd += f" {tmpMps}"
|
||||||
|
cmd += f" -solfile {tmpSol}"
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
cmd += f" -time {self.timeLimit}"
|
||||||
|
for option in self.options:
|
||||||
|
cmd += " " + option
|
||||||
|
if lp.isMIP():
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn("MIPCL_CMD cannot solve the relaxation of a problem")
|
||||||
|
if self.msg:
|
||||||
|
pipe = None
|
||||||
|
else:
|
||||||
|
pipe = open(os.devnull, "w")
|
||||||
|
|
||||||
|
return_code = subprocess.call(cmd.split(), stdout=pipe, stderr=pipe)
|
||||||
|
# We need to undo the objective swap before finishing
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
lp += -lp.objective
|
||||||
|
if return_code != 0:
|
||||||
|
raise PulpSolverError("PuLP: Error while trying to execute " + self.path)
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
status = constants.LpStatusNotSolved
|
||||||
|
status_sol = constants.LpSolutionNoSolutionFound
|
||||||
|
values = None
|
||||||
|
else:
|
||||||
|
status, values, status_sol = self.readsol(tmpSol)
|
||||||
|
self.delete_tmp_files(tmpMps, tmpSol)
|
||||||
|
lp.assignStatus(status, status_sol)
|
||||||
|
if status not in [constants.LpStatusInfeasible, constants.LpStatusNotSolved]:
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(filename):
|
||||||
|
"""Read a MIPCL solution file"""
|
||||||
|
with open(filename) as f:
|
||||||
|
content = f.readlines()
|
||||||
|
content = [l.strip() for l in content]
|
||||||
|
values = {}
|
||||||
|
if not len(content):
|
||||||
|
return (
|
||||||
|
constants.LpStatusNotSolved,
|
||||||
|
values,
|
||||||
|
constants.LpSolutionNoSolutionFound,
|
||||||
|
)
|
||||||
|
first_line = content[0]
|
||||||
|
if first_line == "=infeas=":
|
||||||
|
return constants.LpStatusInfeasible, values, constants.LpSolutionInfeasible
|
||||||
|
objective, value = first_line.split()
|
||||||
|
# this is a workaround.
|
||||||
|
# Not sure if it always returns this limit when unbounded.
|
||||||
|
if abs(float(value)) >= 9.999999995e10:
|
||||||
|
return constants.LpStatusUnbounded, values, constants.LpSolutionUnbounded
|
||||||
|
for line in content[1:]:
|
||||||
|
name, value = line.split()
|
||||||
|
values[name] = float(value)
|
||||||
|
# I'm not sure how this solver announces the optimality
|
||||||
|
# of a solution so we assume it is integer feasible
|
||||||
|
return constants.LpStatusOptimal, values, constants.LpSolutionIntegerFeasible
|
||||||
345
utils/pulp/apis/mosek_api.py
Normal file
345
utils/pulp/apis/mosek_api.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver, PulpSolverError
|
||||||
|
from .. import constants
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class MOSEK(LpSolver):
|
||||||
|
"""Mosek lp and mip solver (via Mosek Optimizer API)."""
|
||||||
|
|
||||||
|
name = "MOSEK"
|
||||||
|
try:
|
||||||
|
global mosek
|
||||||
|
import mosek
|
||||||
|
|
||||||
|
env = mosek.Env()
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if Mosek is available."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp, callback=None):
|
||||||
|
"""Solves a well-formulated lp problem."""
|
||||||
|
raise PulpSolverError("MOSEK : Not Available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit: Optional[float] = None,
|
||||||
|
options: Optional[dict] = None,
|
||||||
|
task_file_name="",
|
||||||
|
sol_type=mosek.soltype.bas,
|
||||||
|
):
|
||||||
|
"""Initializes the Mosek solver.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
|
||||||
|
@param mip: If False, then solve MIP as LP.
|
||||||
|
|
||||||
|
@param msg: Enable Mosek log output.
|
||||||
|
|
||||||
|
@param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
|
||||||
|
@param options: Accepts a dictionary of Mosek solver parameters. Ignore to
|
||||||
|
use default parameter values. Eg: options = {mosek.dparam.mio_max_time:30}
|
||||||
|
sets the maximum time spent by the Mixed Integer optimizer to 30 seconds.
|
||||||
|
Equivalently, one could also write: options = {"MSK_DPAR_MIO_MAX_TIME":30}
|
||||||
|
which uses the generic parameter name as used within the solver, instead of
|
||||||
|
using an object from the Mosek Optimizer API (Python), as before.
|
||||||
|
|
||||||
|
@param task_file_name: Writes a Mosek task file of the given name. By default,
|
||||||
|
no task file will be written. Eg: task_file_name = "eg1.opf".
|
||||||
|
|
||||||
|
@param sol_type: Mosek supports three types of solutions: mosek.soltype.bas
|
||||||
|
(Basic solution, default), mosek.soltype.itr (Interior-point
|
||||||
|
solution) and mosek.soltype.itg (Integer solution).
|
||||||
|
|
||||||
|
For a full list of Mosek parameters (for the Mosek Optimizer API) and supported task file
|
||||||
|
formats, please see https://docs.mosek.com/9.1/pythonapi/parameters.html#doc-all-parameter-list.
|
||||||
|
"""
|
||||||
|
self.mip = mip
|
||||||
|
self.msg = msg
|
||||||
|
self.timeLimit = timeLimit
|
||||||
|
self.task_file_name = task_file_name
|
||||||
|
self.solution_type = sol_type
|
||||||
|
if options is None:
|
||||||
|
options = {}
|
||||||
|
self.options = options
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
timeLimit_keys = {"MSK_DPAR_MIO_MAX_TIME", mosek.dparam.mio_max_time}
|
||||||
|
if not timeLimit_keys.isdisjoint(self.options.keys()):
|
||||||
|
raise ValueError(
|
||||||
|
"timeLimit parameter has been provided trough `timeLimit` and `options`."
|
||||||
|
)
|
||||||
|
self.options["MSK_DPAR_MIO_MAX_TIME"] = self.timeLimit
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if Mosek is available."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setOutStream(self, text):
|
||||||
|
"""Sets the log-output stream."""
|
||||||
|
sys.stdout.write(text)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp, inf=1e20):
|
||||||
|
"""Translate the problem into a Mosek task object."""
|
||||||
|
self.cons = lp.constraints
|
||||||
|
self.numcons = len(self.cons)
|
||||||
|
self.cons_dict = {}
|
||||||
|
i = 0
|
||||||
|
for c in self.cons:
|
||||||
|
self.cons_dict[c] = i
|
||||||
|
i = i + 1
|
||||||
|
self.vars = list(lp.variables())
|
||||||
|
self.numvars = len(self.vars)
|
||||||
|
self.var_dict = {}
|
||||||
|
# Checking for repeated names
|
||||||
|
lp.checkDuplicateVars()
|
||||||
|
self.task = MOSEK.env.Task()
|
||||||
|
self.task.appendcons(self.numcons)
|
||||||
|
self.task.appendvars(self.numvars)
|
||||||
|
if self.msg:
|
||||||
|
self.task.set_Stream(mosek.streamtype.log, self.setOutStream)
|
||||||
|
# Adding variables
|
||||||
|
for i in range(self.numvars):
|
||||||
|
vname = self.vars[i].name
|
||||||
|
self.var_dict[vname] = i
|
||||||
|
self.task.putvarname(i, vname)
|
||||||
|
# Variable type (Default: Continuous)
|
||||||
|
if self.mip & (self.vars[i].cat == constants.LpInteger):
|
||||||
|
self.task.putvartype(i, mosek.variabletype.type_int)
|
||||||
|
self.solution_type = mosek.soltype.itg
|
||||||
|
# Variable bounds
|
||||||
|
vbkey = mosek.boundkey.fr
|
||||||
|
vup = inf
|
||||||
|
vlow = -inf
|
||||||
|
if self.vars[i].lowBound != None:
|
||||||
|
vlow = self.vars[i].lowBound
|
||||||
|
if self.vars[i].upBound != None:
|
||||||
|
vup = self.vars[i].upBound
|
||||||
|
vbkey = mosek.boundkey.ra
|
||||||
|
else:
|
||||||
|
vbkey = mosek.boundkey.lo
|
||||||
|
elif self.vars[i].upBound != None:
|
||||||
|
vup = self.vars[i].upBound
|
||||||
|
vbkey = mosek.boundkey.up
|
||||||
|
self.task.putvarbound(i, vbkey, vlow, vup)
|
||||||
|
# Objective coefficient for the current variable.
|
||||||
|
self.task.putcj(i, lp.objective.get(self.vars[i], 0.0))
|
||||||
|
# Coefficient matrix
|
||||||
|
self.A_rows, self.A_cols, self.A_vals = zip(
|
||||||
|
*[
|
||||||
|
[self.cons_dict[row], self.var_dict[col], coeff]
|
||||||
|
for col, row, coeff in lp.coefficients()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.task.putaijlist(self.A_rows, self.A_cols, self.A_vals)
|
||||||
|
# Constraints
|
||||||
|
self.constraint_data_list = []
|
||||||
|
for c in self.cons:
|
||||||
|
cname = self.cons[c].name
|
||||||
|
if cname != None:
|
||||||
|
self.task.putconname(self.cons_dict[c], cname)
|
||||||
|
else:
|
||||||
|
self.task.putconname(self.cons_dict[c], c)
|
||||||
|
csense = self.cons[c].sense
|
||||||
|
cconst = -self.cons[c].constant
|
||||||
|
clow = -inf
|
||||||
|
cup = inf
|
||||||
|
# Constraint bounds
|
||||||
|
if csense == constants.LpConstraintEQ:
|
||||||
|
cbkey = mosek.boundkey.fx
|
||||||
|
clow = cconst
|
||||||
|
cup = cconst
|
||||||
|
elif csense == constants.LpConstraintGE:
|
||||||
|
cbkey = mosek.boundkey.lo
|
||||||
|
clow = cconst
|
||||||
|
elif csense == constants.LpConstraintLE:
|
||||||
|
cbkey = mosek.boundkey.up
|
||||||
|
cup = cconst
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Invalid constraint type.")
|
||||||
|
self.constraint_data_list.append([self.cons_dict[c], cbkey, clow, cup])
|
||||||
|
self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list = zip(
|
||||||
|
*self.constraint_data_list
|
||||||
|
)
|
||||||
|
self.task.putconboundlist(
|
||||||
|
self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list
|
||||||
|
)
|
||||||
|
# Objective sense
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
self.task.putobjsense(mosek.objsense.maximize)
|
||||||
|
else:
|
||||||
|
self.task.putobjsense(mosek.objsense.minimize)
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
"""
|
||||||
|
Read the solution values and status from the Mosek task object. Note: Since the status
|
||||||
|
map from mosek.solsta to LpStatus is not exact, it is recommended that one enables the
|
||||||
|
log output and then refer to Mosek documentation for a better understanding of the
|
||||||
|
solution (especially in the case of mip problems).
|
||||||
|
"""
|
||||||
|
self.solsta = self.task.getsolsta(self.solution_type)
|
||||||
|
self.solution_status_dict = {
|
||||||
|
mosek.solsta.optimal: constants.LpStatusOptimal,
|
||||||
|
mosek.solsta.prim_infeas_cer: constants.LpStatusInfeasible,
|
||||||
|
mosek.solsta.dual_infeas_cer: constants.LpStatusUnbounded,
|
||||||
|
mosek.solsta.unknown: constants.LpStatusUndefined,
|
||||||
|
mosek.solsta.integer_optimal: constants.LpStatusOptimal,
|
||||||
|
mosek.solsta.prim_illposed_cer: constants.LpStatusNotSolved,
|
||||||
|
mosek.solsta.dual_illposed_cer: constants.LpStatusNotSolved,
|
||||||
|
mosek.solsta.prim_feas: constants.LpStatusNotSolved,
|
||||||
|
mosek.solsta.dual_feas: constants.LpStatusNotSolved,
|
||||||
|
mosek.solsta.prim_and_dual_feas: constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
# Variable values.
|
||||||
|
try:
|
||||||
|
self.xx = [0.0] * self.numvars
|
||||||
|
self.task.getxx(self.solution_type, self.xx)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.varValue = self.xx[self.var_dict[var.name]]
|
||||||
|
except mosek.Error:
|
||||||
|
pass
|
||||||
|
# Constraint slack variables.
|
||||||
|
try:
|
||||||
|
self.xc = [0.0] * self.numcons
|
||||||
|
self.task.getxc(self.solution_type, self.xc)
|
||||||
|
for con in lp.constraints:
|
||||||
|
lp.constraints[con].slack = -(
|
||||||
|
self.cons[con].constant + self.xc[self.cons_dict[con]]
|
||||||
|
)
|
||||||
|
except mosek.Error:
|
||||||
|
pass
|
||||||
|
# Reduced costs.
|
||||||
|
if self.solution_type != mosek.soltype.itg:
|
||||||
|
try:
|
||||||
|
self.x_rc = [0.0] * self.numvars
|
||||||
|
self.task.getreducedcosts(
|
||||||
|
self.solution_type, 0, self.numvars, self.x_rc
|
||||||
|
)
|
||||||
|
for var in lp.variables():
|
||||||
|
var.dj = self.x_rc[self.var_dict[var.name]]
|
||||||
|
except mosek.Error:
|
||||||
|
pass
|
||||||
|
# Constraint Pi variables.
|
||||||
|
try:
|
||||||
|
self.y = [0.0] * self.numcons
|
||||||
|
self.task.gety(self.solution_type, self.y)
|
||||||
|
for con in lp.constraints:
|
||||||
|
lp.constraints[con].pi = self.y[self.cons_dict[con]]
|
||||||
|
except mosek.Error:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def putparam(self, par, val):
|
||||||
|
"""
|
||||||
|
Pass the values of valid parameters to Mosek.
|
||||||
|
"""
|
||||||
|
if isinstance(par, mosek.dparam):
|
||||||
|
self.task.putdouparam(par, val)
|
||||||
|
elif isinstance(par, mosek.iparam):
|
||||||
|
self.task.putintparam(par, val)
|
||||||
|
elif isinstance(par, mosek.sparam):
|
||||||
|
self.task.putstrparam(par, val)
|
||||||
|
elif isinstance(par, str):
|
||||||
|
if par.startswith("MSK_DPAR_"):
|
||||||
|
self.task.putnadouparam(par, val)
|
||||||
|
elif par.startswith("MSK_IPAR_"):
|
||||||
|
self.task.putnaintparam(par, val)
|
||||||
|
elif par.startswith("MSK_SPAR_"):
|
||||||
|
self.task.putnastrparam(par, val)
|
||||||
|
else:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"Invalid MOSEK parameter: '{}'. Check MOSEK documentation for a list of valid parameters.".format(
|
||||||
|
par
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""
|
||||||
|
Solve a well-formulated lp problem.
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
# Set solver parameters
|
||||||
|
for msk_par in self.options:
|
||||||
|
self.putparam(msk_par, self.options[msk_par])
|
||||||
|
# Task file
|
||||||
|
if self.task_file_name:
|
||||||
|
self.task.writedata(self.task_file_name)
|
||||||
|
# Optimize
|
||||||
|
self.task.optimize()
|
||||||
|
# Mosek solver log (default: standard output stream)
|
||||||
|
if self.msg:
|
||||||
|
self.task.solutionsummary(mosek.streamtype.msg)
|
||||||
|
self.findSolutionValues(lp)
|
||||||
|
lp.assignStatus(self.solution_status_dict[self.solsta])
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for con in lp.constraints.values():
|
||||||
|
con.modified = False
|
||||||
|
return lp.status
|
||||||
|
|
||||||
|
def actualResolve(self, lp, inf=1e20, **kwargs):
|
||||||
|
"""
|
||||||
|
Modify constraints and re-solve an lp. The Mosek task object created in the first solve is used.
|
||||||
|
"""
|
||||||
|
for c in self.cons:
|
||||||
|
if self.cons[c].modified:
|
||||||
|
csense = self.cons[c].sense
|
||||||
|
cconst = -self.cons[c].constant
|
||||||
|
clow = -inf
|
||||||
|
cup = inf
|
||||||
|
# Constraint bounds
|
||||||
|
if csense == constants.LpConstraintEQ:
|
||||||
|
cbkey = mosek.boundkey.fx
|
||||||
|
clow = cconst
|
||||||
|
cup = cconst
|
||||||
|
elif csense == constants.LpConstraintGE:
|
||||||
|
cbkey = mosek.boundkey.lo
|
||||||
|
clow = cconst
|
||||||
|
elif csense == constants.LpConstraintLE:
|
||||||
|
cbkey = mosek.boundkey.up
|
||||||
|
cup = cconst
|
||||||
|
else:
|
||||||
|
raise PulpSolverError("Invalid constraint type.")
|
||||||
|
self.task.putconbound(self.cons_dict[c], cbkey, clow, cup)
|
||||||
|
# Re-solve
|
||||||
|
self.task.optimize()
|
||||||
|
self.findSolutionValues(lp)
|
||||||
|
lp.assignStatus(self.solution_status_dict[self.solsta])
|
||||||
|
for var in lp.variables():
|
||||||
|
var.modified = False
|
||||||
|
for con in lp.constraints.values():
|
||||||
|
con.modified = False
|
||||||
|
return lp.status
|
||||||
679
utils/pulp/apis/scip_api.py
Normal file
679
utils/pulp/apis/scip_api.py
Normal file
@@ -0,0 +1,679 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
import operator
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from .core import LpSolver_CMD, LpSolver, subprocess, PulpSolverError
|
||||||
|
from .core import scip_path, fscip_path
|
||||||
|
from .. import constants
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class SCIP_CMD(LpSolver_CMD):
|
||||||
|
"""The SCIP optimization solver"""
|
||||||
|
|
||||||
|
name = "SCIP_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
mip=True,
|
||||||
|
keepFiles=False,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
maxNodes=None,
|
||||||
|
logPath=None,
|
||||||
|
threads=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
gapRel=gapRel,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
maxNodes=maxNodes,
|
||||||
|
threads=threads,
|
||||||
|
logPath=logPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
SCIP_STATUSES = {
|
||||||
|
"unknown": constants.LpStatusUndefined,
|
||||||
|
"user interrupt": constants.LpStatusNotSolved,
|
||||||
|
"node limit reached": constants.LpStatusNotSolved,
|
||||||
|
"total node limit reached": constants.LpStatusNotSolved,
|
||||||
|
"stall node limit reached": constants.LpStatusNotSolved,
|
||||||
|
"time limit reached": constants.LpStatusNotSolved,
|
||||||
|
"memory limit reached": constants.LpStatusNotSolved,
|
||||||
|
"gap limit reached": constants.LpStatusOptimal,
|
||||||
|
"solution limit reached": constants.LpStatusNotSolved,
|
||||||
|
"solution improvement limit reached": constants.LpStatusNotSolved,
|
||||||
|
"restart limit reached": constants.LpStatusNotSolved,
|
||||||
|
"optimal solution found": constants.LpStatusOptimal,
|
||||||
|
"infeasible": constants.LpStatusInfeasible,
|
||||||
|
"unbounded": constants.LpStatusUnbounded,
|
||||||
|
"infeasible or unbounded": constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
NO_SOLUTION_STATUSES = {
|
||||||
|
constants.LpStatusInfeasible,
|
||||||
|
constants.LpStatusUnbounded,
|
||||||
|
constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension(scip_path)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
|
||||||
|
tmpLp, tmpSol, tmpOptions = self.create_tmp_files(lp.name, "lp", "sol", "set")
|
||||||
|
lp.writeLP(tmpLp)
|
||||||
|
|
||||||
|
file_options: List[str] = []
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
file_options.append(f"limits/time={self.timeLimit}")
|
||||||
|
if "gapRel" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/gap={self.optionsDict['gapRel']}")
|
||||||
|
if "gapAbs" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}")
|
||||||
|
if "maxNodes" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}")
|
||||||
|
if "threads" in self.optionsDict and int(self.optionsDict["threads"]) > 1:
|
||||||
|
warnings.warn(
|
||||||
|
"SCIP can only run with a single thread - use FSCIP_CMD for a parallel version of SCIP"
|
||||||
|
)
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
|
||||||
|
|
||||||
|
command: List[str] = []
|
||||||
|
command.append(self.path)
|
||||||
|
command.extend(["-s", tmpOptions])
|
||||||
|
if not self.msg:
|
||||||
|
command.append("-q")
|
||||||
|
if "logPath" in self.optionsDict:
|
||||||
|
command.extend(["-l", self.optionsDict["logPath"]])
|
||||||
|
|
||||||
|
options = iter(self.options)
|
||||||
|
for option in options:
|
||||||
|
# identify cli options by a leading dash (-) and treat other options as file options
|
||||||
|
if option.startswith("-"):
|
||||||
|
# assumption: all cli options require an argument which is provided as a separate parameter
|
||||||
|
argument = next(options)
|
||||||
|
command.extend([option, argument])
|
||||||
|
else:
|
||||||
|
# assumption: all file options require an argument which is provided after the equal sign (=)
|
||||||
|
if "=" not in option:
|
||||||
|
argument = next(options)
|
||||||
|
option += f"={argument}"
|
||||||
|
file_options.append(option)
|
||||||
|
|
||||||
|
# append scip commands after parsing self.options to allow the user to specify additional -c arguments
|
||||||
|
command.extend(["-c", f'read "{tmpLp}"'])
|
||||||
|
command.extend(["-c", "optimize"])
|
||||||
|
command.extend(["-c", f'write solution "{tmpSol}"'])
|
||||||
|
command.extend(["-c", "quit"])
|
||||||
|
|
||||||
|
with open(tmpOptions, "w") as options_file:
|
||||||
|
options_file.write("\n".join(file_options))
|
||||||
|
subprocess.check_call(command, stdout=sys.stdout, stderr=sys.stderr)
|
||||||
|
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
raise PulpSolverError("PuLP: Error while executing " + self.path)
|
||||||
|
status, values = self.readsol(tmpSol)
|
||||||
|
# Make sure to add back in any 0-valued variables SCIP leaves out.
|
||||||
|
finalVals = {}
|
||||||
|
for v in lp.variables():
|
||||||
|
finalVals[v.name] = values.get(v.name, 0.0)
|
||||||
|
|
||||||
|
lp.assignVarsVals(finalVals)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpSol, tmpOptions)
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(filename):
|
||||||
|
"""Read a SCIP solution file"""
|
||||||
|
with open(filename) as f:
|
||||||
|
# First line must contain 'solution status: <something>'
|
||||||
|
try:
|
||||||
|
line = f.readline()
|
||||||
|
comps = line.split(": ")
|
||||||
|
assert comps[0] == "solution status"
|
||||||
|
assert len(comps) == 2
|
||||||
|
except Exception:
|
||||||
|
raise PulpSolverError(f"Can't get SCIP solver status: {line!r}")
|
||||||
|
|
||||||
|
status = SCIP_CMD.SCIP_STATUSES.get(
|
||||||
|
comps[1].strip(), constants.LpStatusUndefined
|
||||||
|
)
|
||||||
|
values = {}
|
||||||
|
|
||||||
|
if status in SCIP_CMD.NO_SOLUTION_STATUSES:
|
||||||
|
return status, values
|
||||||
|
|
||||||
|
# Look for an objective value. If we can't find one, stop.
|
||||||
|
try:
|
||||||
|
line = f.readline()
|
||||||
|
comps = line.split(": ")
|
||||||
|
assert comps[0] == "objective value"
|
||||||
|
assert len(comps) == 2
|
||||||
|
float(comps[1].strip())
|
||||||
|
except Exception:
|
||||||
|
raise PulpSolverError(f"Can't get SCIP solver objective: {line!r}")
|
||||||
|
|
||||||
|
# Parse the variable values.
|
||||||
|
for line in f:
|
||||||
|
try:
|
||||||
|
comps = line.split()
|
||||||
|
values[comps[0]] = float(comps[1])
|
||||||
|
except:
|
||||||
|
raise PulpSolverError(f"Can't read SCIP solver output: {line!r}")
|
||||||
|
|
||||||
|
return status, values
|
||||||
|
|
||||||
|
|
||||||
|
SCIP = SCIP_CMD
|
||||||
|
|
||||||
|
|
||||||
|
class FSCIP_CMD(LpSolver_CMD):
|
||||||
|
"""The multi-threaded FiberSCIP version of the SCIP optimization solver"""
|
||||||
|
|
||||||
|
name = "FSCIP_CMD"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
path=None,
|
||||||
|
mip=True,
|
||||||
|
keepFiles=False,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
maxNodes=None,
|
||||||
|
threads=None,
|
||||||
|
logPath=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param bool keepFiles: if True, files are saved in the current directory and not deleted after solving
|
||||||
|
:param str path: path to the solver binary
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
"""
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
gapRel=gapRel,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
maxNodes=maxNodes,
|
||||||
|
threads=threads,
|
||||||
|
logPath=logPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
FSCIP_STATUSES = {
|
||||||
|
"No Solution": constants.LpStatusNotSolved,
|
||||||
|
"Final Solution": constants.LpStatusOptimal,
|
||||||
|
}
|
||||||
|
NO_SOLUTION_STATUSES = {
|
||||||
|
constants.LpStatusInfeasible,
|
||||||
|
constants.LpStatusUnbounded,
|
||||||
|
constants.LpStatusNotSolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension(fscip_path)
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
|
||||||
|
tmpLp, tmpSol, tmpOptions, tmpParams = self.create_tmp_files(
|
||||||
|
lp.name, "lp", "sol", "set", "prm"
|
||||||
|
)
|
||||||
|
lp.writeLP(tmpLp)
|
||||||
|
|
||||||
|
file_options: List[str] = []
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
file_options.append(f"limits/time={self.timeLimit}")
|
||||||
|
if "gapRel" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/gap={self.optionsDict['gapRel']}")
|
||||||
|
if "gapAbs" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/absgap={self.optionsDict['gapAbs']}")
|
||||||
|
if "maxNodes" in self.optionsDict:
|
||||||
|
file_options.append(f"limits/nodes={self.optionsDict['maxNodes']}")
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
|
||||||
|
|
||||||
|
file_parameters: List[str] = []
|
||||||
|
# disable presolving in the LoadCoordinator to make sure a solution file is always written
|
||||||
|
file_parameters.append("NoPreprocessingInLC = TRUE")
|
||||||
|
|
||||||
|
command: List[str] = []
|
||||||
|
command.append(self.path)
|
||||||
|
command.append(tmpParams)
|
||||||
|
command.append(tmpLp)
|
||||||
|
command.extend(["-s", tmpOptions])
|
||||||
|
command.extend(["-fsol", tmpSol])
|
||||||
|
if not self.msg:
|
||||||
|
command.append("-q")
|
||||||
|
if "logPath" in self.optionsDict:
|
||||||
|
command.extend(["-l", self.optionsDict["logPath"]])
|
||||||
|
if "threads" in self.optionsDict:
|
||||||
|
command.extend(["-sth", f"{self.optionsDict['threads']}"])
|
||||||
|
|
||||||
|
options = iter(self.options)
|
||||||
|
for option in options:
|
||||||
|
# identify cli options by a leading dash (-) and treat other options as file options
|
||||||
|
if option.startswith("-"):
|
||||||
|
# assumption: all cli options require an argument which is provided as a separate parameter
|
||||||
|
argument = next(options)
|
||||||
|
command.extend([option, argument])
|
||||||
|
else:
|
||||||
|
# assumption: all file options contain a slash (/)
|
||||||
|
is_file_options = "/" in option
|
||||||
|
|
||||||
|
# assumption: all file options and parameters require an argument which is provided after the equal sign (=)
|
||||||
|
if "=" not in option:
|
||||||
|
argument = next(options)
|
||||||
|
option += f"={argument}"
|
||||||
|
|
||||||
|
if is_file_options:
|
||||||
|
file_options.append(option)
|
||||||
|
else:
|
||||||
|
file_parameters.append(option)
|
||||||
|
|
||||||
|
# wipe the solution file since FSCIP does not overwrite it if no solution was found which causes parsing errors
|
||||||
|
self.silent_remove(tmpSol)
|
||||||
|
with open(tmpOptions, "w") as options_file:
|
||||||
|
options_file.write("\n".join(file_options))
|
||||||
|
with open(tmpParams, "w") as parameters_file:
|
||||||
|
parameters_file.write("\n".join(file_parameters))
|
||||||
|
subprocess.check_call(
|
||||||
|
command,
|
||||||
|
stdout=sys.stdout if self.msg else subprocess.DEVNULL,
|
||||||
|
stderr=sys.stderr if self.msg else subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(tmpSol):
|
||||||
|
raise PulpSolverError("PuLP: Error while executing " + self.path)
|
||||||
|
status, values = self.readsol(tmpSol)
|
||||||
|
# Make sure to add back in any 0-valued variables SCIP leaves out.
|
||||||
|
finalVals = {}
|
||||||
|
for v in lp.variables():
|
||||||
|
finalVals[v.name] = values.get(v.name, 0.0)
|
||||||
|
|
||||||
|
lp.assignVarsVals(finalVals)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpSol, tmpOptions, tmpParams)
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_status(string: str) -> Optional[int]:
|
||||||
|
for fscip_status, pulp_status in FSCIP_CMD.FSCIP_STATUSES.items():
|
||||||
|
if fscip_status in string:
|
||||||
|
return pulp_status
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_objective(string: str) -> Optional[float]:
|
||||||
|
fields = string.split(":")
|
||||||
|
if len(fields) != 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
label, objective = fields
|
||||||
|
if label != "objective value":
|
||||||
|
return None
|
||||||
|
|
||||||
|
objective = objective.strip()
|
||||||
|
try:
|
||||||
|
objective = float(objective)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return objective
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_variable(string: str) -> Optional[Tuple[str, float]]:
|
||||||
|
fields = string.split()
|
||||||
|
if len(fields) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
name, value = fields[:2]
|
||||||
|
try:
|
||||||
|
value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return name, value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(filename):
|
||||||
|
"""Read a FSCIP solution file"""
|
||||||
|
with open(filename) as file:
|
||||||
|
# First line must contain a solution status
|
||||||
|
status_line = file.readline()
|
||||||
|
status = FSCIP_CMD.parse_status(status_line)
|
||||||
|
if status is None:
|
||||||
|
raise PulpSolverError(f"Can't get FSCIP solver status: {status_line!r}")
|
||||||
|
|
||||||
|
if status in FSCIP_CMD.NO_SOLUTION_STATUSES:
|
||||||
|
return status, {}
|
||||||
|
|
||||||
|
# Look for an objective value. If we can't find one, stop.
|
||||||
|
objective_line = file.readline()
|
||||||
|
objective = FSCIP_CMD.parse_objective(objective_line)
|
||||||
|
if objective is None:
|
||||||
|
raise PulpSolverError(
|
||||||
|
f"Can't get FSCIP solver objective: {objective_line!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse the variable values.
|
||||||
|
variables: Dict[str, float] = {}
|
||||||
|
for variable_line in file:
|
||||||
|
variable = FSCIP_CMD.parse_variable(variable_line)
|
||||||
|
if variable is None:
|
||||||
|
raise PulpSolverError(
|
||||||
|
f"Can't read FSCIP solver output: {variable_line!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
name, value = variable
|
||||||
|
variables[name] = value
|
||||||
|
|
||||||
|
return status, variables
|
||||||
|
|
||||||
|
|
||||||
|
FSCIP = FSCIP_CMD
|
||||||
|
|
||||||
|
|
||||||
|
class SCIP_PY(LpSolver):
|
||||||
|
"""
|
||||||
|
The SCIP Optimization Suite (via its python interface)
|
||||||
|
|
||||||
|
The SCIP internals are available after calling solve as:
|
||||||
|
- each variable in variable.solverVar
|
||||||
|
- each constraint in constraint.solverConstraint
|
||||||
|
- the model in problem.solverModel
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "SCIP_PY"
|
||||||
|
|
||||||
|
try:
|
||||||
|
global scip
|
||||||
|
import pyscipopt as scip
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
raise PulpSolverError(f"The {self.name} solver is not available")
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
options=None,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
gapAbs=None,
|
||||||
|
maxNodes=None,
|
||||||
|
logPath=None,
|
||||||
|
threads=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param list options: list of additional options to pass to solver
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param float gapAbs: absolute gap tolerance for the solver to stop
|
||||||
|
:param int maxNodes: max number of nodes during branching. Stops the solving when reached.
|
||||||
|
:param str logPath: path to the log file
|
||||||
|
:param int threads: sets the maximum number of threads
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
options=options,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
gapRel=gapRel,
|
||||||
|
gapAbs=gapAbs,
|
||||||
|
maxNodes=maxNodes,
|
||||||
|
logPath=logPath,
|
||||||
|
threads=threads,
|
||||||
|
)
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
lp.resolveOK = True
|
||||||
|
|
||||||
|
solutionStatus = lp.solverModel.getStatus()
|
||||||
|
scip_to_pulp_status = {
|
||||||
|
"optimal": constants.LpStatusOptimal,
|
||||||
|
"unbounded": constants.LpStatusUnbounded,
|
||||||
|
"infeasible": constants.LpStatusInfeasible,
|
||||||
|
"inforunbd": constants.LpStatusNotSolved,
|
||||||
|
"timelimit": constants.LpStatusNotSolved,
|
||||||
|
"userinterrupt": constants.LpStatusNotSolved,
|
||||||
|
"nodelimit": constants.LpStatusNotSolved,
|
||||||
|
"totalnodelimit": constants.LpStatusNotSolved,
|
||||||
|
"stallnodelimit": constants.LpStatusNotSolved,
|
||||||
|
"gaplimit": constants.LpStatusNotSolved,
|
||||||
|
"memlimit": constants.LpStatusNotSolved,
|
||||||
|
"sollimit": constants.LpStatusNotSolved,
|
||||||
|
"bestsollimit": constants.LpStatusNotSolved,
|
||||||
|
"restartlimit": constants.LpStatusNotSolved,
|
||||||
|
"unknown": constants.LpStatusUndefined,
|
||||||
|
}
|
||||||
|
status = scip_to_pulp_status[solutionStatus]
|
||||||
|
lp.assignStatus(status)
|
||||||
|
|
||||||
|
if status == constants.LpStatusOptimal:
|
||||||
|
solution = lp.solverModel.getBestSol()
|
||||||
|
for variable in lp._variables:
|
||||||
|
variable.varValue = solution[variable.solverVar]
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.slack = lp.solverModel.getSlack(
|
||||||
|
constraint.solverConstraint, solution
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: check if problem is an LP i.e. does not have integer variables
|
||||||
|
# if :
|
||||||
|
# for variable in lp._variables:
|
||||||
|
# variable.dj = lp.solverModel.getVarRedcost(variable.solverVar)
|
||||||
|
# for constraint in lp.constraints.values():
|
||||||
|
# constraint.pi = lp.solverModel.getDualSolVal(constraint.solverConstraint)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
# if pyscipopt can be installed (and therefore imported) it has access to scip
|
||||||
|
return True
|
||||||
|
|
||||||
|
def callSolver(self, lp):
|
||||||
|
"""Solves the problem with scip"""
|
||||||
|
lp.solverModel.optimize()
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into a scip model
|
||||||
|
"""
|
||||||
|
##################################################
|
||||||
|
# create model
|
||||||
|
##################################################
|
||||||
|
lp.solverModel = scip.Model(lp.name)
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
lp.solverModel.setMaximize()
|
||||||
|
else:
|
||||||
|
lp.solverModel.setMinimize()
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# add options
|
||||||
|
##################################################
|
||||||
|
if not self.msg:
|
||||||
|
lp.solverModel.hideOutput()
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
lp.solverModel.setParam("limits/time", self.timeLimit)
|
||||||
|
if "gapRel" in self.optionsDict:
|
||||||
|
lp.solverModel.setParam("limits/gap", self.optionsDict["gapRel"])
|
||||||
|
if "gapAbs" in self.optionsDict:
|
||||||
|
lp.solverModel.setParam("limits/absgap", self.optionsDict["gapAbs"])
|
||||||
|
if "maxNodes" in self.optionsDict:
|
||||||
|
lp.solverModel.setParam("limits/nodes", self.optionsDict["maxNodes"])
|
||||||
|
if "logPath" in self.optionsDict:
|
||||||
|
lp.solverModel.setLogfile(self.optionsDict["logPath"])
|
||||||
|
if "threads" in self.optionsDict and int(self.optionsDict["threads"]) > 1:
|
||||||
|
warnings.warn(
|
||||||
|
f"The solver {self.name} can only run with a single thread"
|
||||||
|
)
|
||||||
|
if not self.mip:
|
||||||
|
warnings.warn(f"{self.name} does not allow a problem to be relaxed")
|
||||||
|
|
||||||
|
options = iter(self.options)
|
||||||
|
for option in options:
|
||||||
|
# assumption: all file options require an argument which is provided after the equal sign (=)
|
||||||
|
if "=" in option:
|
||||||
|
name, value = option.split("=", maxsplit=2)
|
||||||
|
else:
|
||||||
|
name, value = option, next(options)
|
||||||
|
lp.solverModel.setParam(name, value)
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# add variables
|
||||||
|
##################################################
|
||||||
|
category_to_vtype = {
|
||||||
|
constants.LpBinary: "B",
|
||||||
|
constants.LpContinuous: "C",
|
||||||
|
constants.LpInteger: "I",
|
||||||
|
}
|
||||||
|
for var in lp.variables():
|
||||||
|
var.solverVar = lp.solverModel.addVar(
|
||||||
|
name=var.name,
|
||||||
|
vtype=category_to_vtype[var.cat],
|
||||||
|
lb=var.lowBound, # a lower bound of None represents -infinity
|
||||||
|
ub=var.upBound, # an upper bound of None represents +infinity
|
||||||
|
obj=lp.objective.get(var, 0.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
##################################################
|
||||||
|
# add constraints
|
||||||
|
##################################################
|
||||||
|
sense_to_operator = {
|
||||||
|
constants.LpConstraintLE: operator.le,
|
||||||
|
constants.LpConstraintGE: operator.ge,
|
||||||
|
constants.LpConstraintEQ: operator.eq,
|
||||||
|
}
|
||||||
|
for name, constraint in lp.constraints.items():
|
||||||
|
constraint.solverConstraint = lp.solverModel.addCons(
|
||||||
|
cons=sense_to_operator[constraint.sense](
|
||||||
|
scip.quicksum(
|
||||||
|
coefficient * variable.solverVar
|
||||||
|
for variable, coefficient in constraint.items()
|
||||||
|
),
|
||||||
|
-constraint.constant,
|
||||||
|
),
|
||||||
|
name=name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
creates a scip model, variables and constraints and attaches
|
||||||
|
them to the lp model which it then solves
|
||||||
|
"""
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
self.callSolver(lp)
|
||||||
|
solutionStatus = self.findSolutionValues(lp)
|
||||||
|
for variable in lp._variables:
|
||||||
|
variable.modified = False
|
||||||
|
for constraint in lp.constraints.values():
|
||||||
|
constraint.modified = False
|
||||||
|
return solutionStatus
|
||||||
|
|
||||||
|
def actualResolve(self, lp):
|
||||||
|
"""
|
||||||
|
Solve a well formulated lp problem
|
||||||
|
|
||||||
|
uses the old solver and modifies the rhs of the modified constraints
|
||||||
|
"""
|
||||||
|
# TODO: add ability to resolve pysciptopt models
|
||||||
|
# - http://listserv.zib.de/pipermail/scip/2020-May/003977.html
|
||||||
|
# - https://scipopt.org/doc-8.0.0/html/REOPT.php
|
||||||
|
raise PulpSolverError(
|
||||||
|
f"The {self.name} solver does not implement resolving"
|
||||||
|
)
|
||||||
760
utils/pulp/apis/xpress_api.py
Normal file
760
utils/pulp/apis/xpress_api.py
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
# Version 1.4.2
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
from .core import LpSolver, LpSolver_CMD, subprocess, PulpSolverError
|
||||||
|
from .. import constants
|
||||||
|
import warnings
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _ismip(lp):
|
||||||
|
"""Check whether lp is a MIP.
|
||||||
|
|
||||||
|
From an XPRESS point of view, a problem is also a MIP if it contains
|
||||||
|
SOS constraints."""
|
||||||
|
return lp.isMIP() or len(lp.sos1) or len(lp.sos2)
|
||||||
|
|
||||||
|
|
||||||
|
class XPRESS(LpSolver_CMD):
|
||||||
|
"""The XPRESS LP solver that uses the XPRESS command line tool
|
||||||
|
in a subprocess"""
|
||||||
|
|
||||||
|
name = "XPRESS"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
options=None,
|
||||||
|
keepFiles=False,
|
||||||
|
path=None,
|
||||||
|
maxSeconds=None,
|
||||||
|
targetGap=None,
|
||||||
|
heurFreq=None,
|
||||||
|
heurStra=None,
|
||||||
|
coverCuts=None,
|
||||||
|
preSolve=None,
|
||||||
|
warmStart=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initializes the Xpress solver.
|
||||||
|
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param maxSeconds: deprecated for timeLimit
|
||||||
|
:param targetGap: deprecated for gapRel
|
||||||
|
:param heurFreq: the frequency at which heuristics are used in the tree search
|
||||||
|
:param heurStra: heuristic strategy
|
||||||
|
:param coverCuts: the number of rounds of lifted cover inequalities at the top node
|
||||||
|
:param preSolve: whether presolving should be performed before the main algorithm
|
||||||
|
:param options: Adding more options, e.g. options = ["NODESELECTION=1", "HEURDEPTH=5"]
|
||||||
|
More about Xpress options and control parameters please see
|
||||||
|
https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
|
||||||
|
:param bool warmStart: if True, then use current variable values as start
|
||||||
|
"""
|
||||||
|
if maxSeconds:
|
||||||
|
warnings.warn("Parameter maxSeconds is being depreciated for timeLimit")
|
||||||
|
if timeLimit is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Parameter timeLimit and maxSeconds passed, using timeLimit"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
timeLimit = maxSeconds
|
||||||
|
if targetGap is not None:
|
||||||
|
warnings.warn("Parameter targetGap is being depreciated for gapRel")
|
||||||
|
if gapRel is not None:
|
||||||
|
warnings.warn("Parameter gapRel and epgap passed, using gapRel")
|
||||||
|
else:
|
||||||
|
gapRel = targetGap
|
||||||
|
LpSolver_CMD.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
path=path,
|
||||||
|
keepFiles=keepFiles,
|
||||||
|
heurFreq=heurFreq,
|
||||||
|
heurStra=heurStra,
|
||||||
|
coverCuts=coverCuts,
|
||||||
|
preSolve=preSolve,
|
||||||
|
warmStart=warmStart,
|
||||||
|
)
|
||||||
|
|
||||||
|
def defaultPath(self):
|
||||||
|
return self.executableExtension("optimizer")
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
return self.executable(self.path)
|
||||||
|
|
||||||
|
def actualSolve(self, lp):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.executable(self.path):
|
||||||
|
raise PulpSolverError("PuLP: cannot execute " + self.path)
|
||||||
|
tmpLp, tmpSol, tmpCmd, tmpAttr, tmpStart = self.create_tmp_files(
|
||||||
|
lp.name, "lp", "prt", "cmd", "attr", "slx"
|
||||||
|
)
|
||||||
|
variables = lp.writeLP(tmpLp, writeSOS=1, mip=self.mip)
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
start = [(v.name, v.value()) for v in variables if v.value() is not None]
|
||||||
|
self.writeslxsol(tmpStart, start)
|
||||||
|
# Explicitly capture some attributes so that we can easily get
|
||||||
|
# information about the solution.
|
||||||
|
attrNames = []
|
||||||
|
if _ismip(lp) and self.mip:
|
||||||
|
attrNames.extend(["mipobjval", "bestbound", "mipstatus"])
|
||||||
|
statusmap = {
|
||||||
|
0: constants.LpStatusUndefined, # XPRS_MIP_NOT_LOADED
|
||||||
|
1: constants.LpStatusUndefined, # XPRS_MIP_LP_NOT_OPTIMAL
|
||||||
|
2: constants.LpStatusUndefined, # XPRS_MIP_LP_OPTIMAL
|
||||||
|
3: constants.LpStatusUndefined, # XPRS_MIP_NO_SOL_FOUND
|
||||||
|
4: constants.LpStatusUndefined, # XPRS_MIP_SOLUTION
|
||||||
|
5: constants.LpStatusInfeasible, # XPRS_MIP_INFEAS
|
||||||
|
6: constants.LpStatusOptimal, # XPRS_MIP_OPTIMAL
|
||||||
|
7: constants.LpStatusUndefined, # XPRS_MIP_UNBOUNDED
|
||||||
|
}
|
||||||
|
statuskey = "mipstatus"
|
||||||
|
else:
|
||||||
|
attrNames.extend(["lpobjval", "lpstatus"])
|
||||||
|
statusmap = {
|
||||||
|
0: constants.LpStatusNotSolved, # XPRS_LP_UNSTARTED
|
||||||
|
1: constants.LpStatusOptimal, # XPRS_LP_OPTIMAL
|
||||||
|
2: constants.LpStatusInfeasible, # XPRS_LP_INFEAS
|
||||||
|
3: constants.LpStatusUndefined, # XPRS_LP_CUTOFF
|
||||||
|
4: constants.LpStatusUndefined, # XPRS_LP_UNFINISHED
|
||||||
|
5: constants.LpStatusUnbounded, # XPRS_LP_UNBOUNDED
|
||||||
|
6: constants.LpStatusUndefined, # XPRS_LP_CUTOFF_IN_DUAL
|
||||||
|
7: constants.LpStatusNotSolved, # XPRS_LP_UNSOLVED
|
||||||
|
8: constants.LpStatusUndefined, # XPRS_LP_NONCONVEX
|
||||||
|
}
|
||||||
|
statuskey = "lpstatus"
|
||||||
|
with open(tmpCmd, "w") as cmd:
|
||||||
|
if not self.msg:
|
||||||
|
cmd.write("OUTPUTLOG=0\n")
|
||||||
|
# The readprob command must be in lower case for correct filename handling
|
||||||
|
cmd.write("readprob " + self.quote_path(tmpLp) + "\n")
|
||||||
|
if self.timeLimit is not None:
|
||||||
|
cmd.write("MAXTIME=%d\n" % self.timeLimit)
|
||||||
|
targetGap = self.optionsDict.get("gapRel")
|
||||||
|
if targetGap is not None:
|
||||||
|
cmd.write(f"MIPRELSTOP={targetGap:f}\n")
|
||||||
|
heurFreq = self.optionsDict.get("heurFreq")
|
||||||
|
if heurFreq is not None:
|
||||||
|
cmd.write("HEURFREQ=%d\n" % heurFreq)
|
||||||
|
heurStra = self.optionsDict.get("heurStra")
|
||||||
|
if heurStra is not None:
|
||||||
|
cmd.write("HEURSTRATEGY=%d\n" % heurStra)
|
||||||
|
coverCuts = self.optionsDict.get("coverCuts")
|
||||||
|
if coverCuts is not None:
|
||||||
|
cmd.write("COVERCUTS=%d\n" % coverCuts)
|
||||||
|
preSolve = self.optionsDict.get("preSolve")
|
||||||
|
if preSolve is not None:
|
||||||
|
cmd.write("PRESOLVE=%d\n" % preSolve)
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
cmd.write("readslxsol " + self.quote_path(tmpStart) + "\n")
|
||||||
|
for option in self.options:
|
||||||
|
cmd.write(option + "\n")
|
||||||
|
if _ismip(lp) and self.mip:
|
||||||
|
cmd.write("mipoptimize\n")
|
||||||
|
else:
|
||||||
|
cmd.write("lpoptimize\n")
|
||||||
|
# The writeprtsol command must be in lower case for correct filename handling
|
||||||
|
cmd.write("writeprtsol " + self.quote_path(tmpSol) + "\n")
|
||||||
|
cmd.write(
|
||||||
|
f"set fh [open {self.quote_path(tmpAttr)} w]; list\n"
|
||||||
|
) # `list` to suppress output
|
||||||
|
|
||||||
|
for attr in attrNames:
|
||||||
|
cmd.write(f'puts $fh "{attr}=${attr}"\n')
|
||||||
|
cmd.write("close $fh\n")
|
||||||
|
cmd.write("QUIT\n")
|
||||||
|
with open(tmpCmd) as cmd:
|
||||||
|
consume = False
|
||||||
|
subout = None
|
||||||
|
suberr = None
|
||||||
|
if not self.msg:
|
||||||
|
# Xpress writes a banner before we can disable output. So
|
||||||
|
# we have to explicitly consume the banner.
|
||||||
|
if sys.hexversion >= 0x03030000:
|
||||||
|
subout = subprocess.DEVNULL
|
||||||
|
suberr = subprocess.DEVNULL
|
||||||
|
else:
|
||||||
|
# We could also use open(os.devnull, 'w') but then we
|
||||||
|
# would be responsible for closing the file.
|
||||||
|
subout = subprocess.PIPE
|
||||||
|
suberr = subprocess.STDOUT
|
||||||
|
consume = True
|
||||||
|
xpress = subprocess.Popen(
|
||||||
|
[self.path, lp.name],
|
||||||
|
shell=True,
|
||||||
|
stdin=cmd,
|
||||||
|
stdout=subout,
|
||||||
|
stderr=suberr,
|
||||||
|
universal_newlines=True,
|
||||||
|
)
|
||||||
|
if consume:
|
||||||
|
# Special case in which messages are disabled and we have
|
||||||
|
# to consume any output
|
||||||
|
for _ in xpress.stdout:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if xpress.wait() != 0:
|
||||||
|
raise PulpSolverError("PuLP: Error while executing " + self.path)
|
||||||
|
values, redcost, slacks, duals, attrs = self.readsol(tmpSol, tmpAttr)
|
||||||
|
self.delete_tmp_files(tmpLp, tmpSol, tmpCmd, tmpAttr)
|
||||||
|
status = statusmap.get(attrs.get(statuskey, -1), constants.LpStatusUndefined)
|
||||||
|
lp.assignVarsVals(values)
|
||||||
|
lp.assignVarsDj(redcost)
|
||||||
|
lp.assignConsSlack(slacks)
|
||||||
|
lp.assignConsPi(duals)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def readsol(filename, attrfile):
|
||||||
|
"""Read an XPRESS solution file"""
|
||||||
|
values = {}
|
||||||
|
redcost = {}
|
||||||
|
slacks = {}
|
||||||
|
duals = {}
|
||||||
|
with open(filename) as f:
|
||||||
|
for lineno, _line in enumerate(f):
|
||||||
|
# The first 6 lines are status information
|
||||||
|
if lineno < 6:
|
||||||
|
continue
|
||||||
|
elif lineno == 6:
|
||||||
|
# Line with status information
|
||||||
|
_line = _line.split()
|
||||||
|
rows = int(_line[2])
|
||||||
|
cols = int(_line[5])
|
||||||
|
elif lineno < 10:
|
||||||
|
# Empty line, "Solution Statistics", objective direction
|
||||||
|
pass
|
||||||
|
elif lineno == 10:
|
||||||
|
# Solution status
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# There is some more stuff and then follows the "Rows" and
|
||||||
|
# "Columns" section. That other stuff does not match the
|
||||||
|
# format of the rows/columns lines, so we can keep the
|
||||||
|
# parser simple
|
||||||
|
line = _line.split()
|
||||||
|
if len(line) > 1:
|
||||||
|
if line[0] == "C":
|
||||||
|
# A column
|
||||||
|
# (C, Number, Name, At, Value, Input Cost, Reduced Cost)
|
||||||
|
name = line[2]
|
||||||
|
values[name] = float(line[4])
|
||||||
|
redcost[name] = float(line[6])
|
||||||
|
elif len(line[0]) == 1 and line[0] in "LGRE":
|
||||||
|
# A row
|
||||||
|
# ([LGRE], Number, Name, At, Value, Slack, Dual, RHS)
|
||||||
|
name = line[2]
|
||||||
|
slacks[name] = float(line[5])
|
||||||
|
duals[name] = float(line[6])
|
||||||
|
# Read the attributes that we wrote explicitly
|
||||||
|
attrs = dict()
|
||||||
|
with open(attrfile) as f:
|
||||||
|
for line in f:
|
||||||
|
fields = line.strip().split("=")
|
||||||
|
if len(fields) == 2 and fields[0].lower() == fields[0]:
|
||||||
|
value = fields[1].strip()
|
||||||
|
try:
|
||||||
|
value = int(fields[1].strip())
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
value = float(fields[1].strip())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
attrs[fields[0].strip()] = value
|
||||||
|
return values, redcost, slacks, duals, attrs
|
||||||
|
|
||||||
|
def writeslxsol(self, name, *values):
|
||||||
|
"""
|
||||||
|
Write a solution file in SLX format.
|
||||||
|
The function can write multiple solutions to the same file, each
|
||||||
|
solution must be passed as a list of (name,value) pairs. Solutions
|
||||||
|
are written in the order specified and are given names "solutionN"
|
||||||
|
where N is the index of the solution in the list.
|
||||||
|
|
||||||
|
:param string name: file name
|
||||||
|
:param list values: list of lists of (name,value) pairs
|
||||||
|
"""
|
||||||
|
with open(name, "w") as slx:
|
||||||
|
for i, sol in enumerate(values):
|
||||||
|
slx.write("NAME solution%d\n" % i)
|
||||||
|
for name, value in sol:
|
||||||
|
slx.write(f" C {name} {value:.16f}\n")
|
||||||
|
slx.write("ENDATA\n")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def quote_path(path):
|
||||||
|
r"""
|
||||||
|
Quotes a path for the Xpress optimizer console, by wrapping it in
|
||||||
|
double quotes and escaping the following characters, which would
|
||||||
|
otherwise be interpreted by the Tcl shell: \ $ " [
|
||||||
|
"""
|
||||||
|
return '"' + re.sub(r'([\\$"[])', r"\\\1", path) + '"'
|
||||||
|
|
||||||
|
|
||||||
|
XPRESS_CMD = XPRESS
|
||||||
|
|
||||||
|
xpress = None
|
||||||
|
|
||||||
|
|
||||||
|
class XPRESS_PY(LpSolver):
|
||||||
|
"""The XPRESS LP solver that uses XPRESS Python API"""
|
||||||
|
|
||||||
|
name = "XPRESS_PY"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mip=True,
|
||||||
|
msg=True,
|
||||||
|
timeLimit=None,
|
||||||
|
gapRel=None,
|
||||||
|
heurFreq=None,
|
||||||
|
heurStra=None,
|
||||||
|
coverCuts=None,
|
||||||
|
preSolve=None,
|
||||||
|
warmStart=None,
|
||||||
|
export=None,
|
||||||
|
options=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initializes the Xpress solver.
|
||||||
|
|
||||||
|
:param bool mip: if False, assume LP even if integer variables
|
||||||
|
:param bool msg: if False, no log is shown
|
||||||
|
:param float timeLimit: maximum time for solver (in seconds)
|
||||||
|
:param float gapRel: relative gap tolerance for the solver to stop (in fraction)
|
||||||
|
:param heurFreq: the frequency at which heuristics are used in the tree search
|
||||||
|
:param heurStra: heuristic strategy
|
||||||
|
:param coverCuts: the number of rounds of lifted cover inequalities at the top node
|
||||||
|
:param preSolve: whether presolving should be performed before the main algorithm
|
||||||
|
:param bool warmStart: if set then use current variable values as warm start
|
||||||
|
:param string export: if set then the model will be exported to this file before solving
|
||||||
|
:param options: Adding more options. This is a list the elements of which
|
||||||
|
are either (name,value) pairs or strings "name=value".
|
||||||
|
More about Xpress options and control parameters please see
|
||||||
|
https://www.fico.com/fico-xpress-optimization/docs/latest/solver/optimizer/HTML/chapter7.html
|
||||||
|
"""
|
||||||
|
if timeLimit is not None:
|
||||||
|
# The Xpress time limit has this interpretation:
|
||||||
|
# timelimit <0: Stop after -timelimit, no matter what
|
||||||
|
# timelimit >0: Stop after timelimit only if a feasible solution
|
||||||
|
# exists. We overwrite this meaning here since it is
|
||||||
|
# somewhat counterintuitive when compared to other
|
||||||
|
# solvers. You can always pass a positive timlimit
|
||||||
|
# via `options` to get that behavior.
|
||||||
|
timeLimit = -abs(timeLimit)
|
||||||
|
LpSolver.__init__(
|
||||||
|
self,
|
||||||
|
gapRel=gapRel,
|
||||||
|
mip=mip,
|
||||||
|
msg=msg,
|
||||||
|
timeLimit=timeLimit,
|
||||||
|
options=options,
|
||||||
|
heurFreq=heurFreq,
|
||||||
|
heurStra=heurStra,
|
||||||
|
coverCuts=coverCuts,
|
||||||
|
preSolve=preSolve,
|
||||||
|
warmStart=warmStart,
|
||||||
|
)
|
||||||
|
self._available = None
|
||||||
|
self._export = export
|
||||||
|
|
||||||
|
def available(self):
|
||||||
|
"""True if the solver is available"""
|
||||||
|
if self._available is None:
|
||||||
|
try:
|
||||||
|
global xpress
|
||||||
|
import xpress
|
||||||
|
|
||||||
|
# Always disable the global output. We only want output if
|
||||||
|
# we install callbacks explicitly
|
||||||
|
xpress.setOutputEnabled(False)
|
||||||
|
self._available = True
|
||||||
|
except:
|
||||||
|
self._available = False
|
||||||
|
return self._available
|
||||||
|
|
||||||
|
def callSolver(self, lp, prepare=None):
|
||||||
|
"""Perform the actual solve from actualSolve() or actualResolve().
|
||||||
|
|
||||||
|
:param prepare: a function that is called with `lp` as argument
|
||||||
|
and allows final tweaks to `lp.solverModel` before
|
||||||
|
the low level solve is started.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
model = lp.solverModel
|
||||||
|
# Mark all variables and constraints as unmodified so that
|
||||||
|
# actualResolve will do the correct thing.
|
||||||
|
for v in lp.variables():
|
||||||
|
v.modified = False
|
||||||
|
for c in lp.constraints.values():
|
||||||
|
c.modified = False
|
||||||
|
|
||||||
|
if self._export is not None:
|
||||||
|
if self._export.lower().endswith(".lp"):
|
||||||
|
model.write(self._export, "l")
|
||||||
|
else:
|
||||||
|
model.write(self._export)
|
||||||
|
if prepare is not None:
|
||||||
|
prepare(lp)
|
||||||
|
if _ismip(lp) and not self.mip:
|
||||||
|
# Solve only the LP relaxation
|
||||||
|
model.lpoptimize()
|
||||||
|
else:
|
||||||
|
# In all other cases, solve() does the correct thing
|
||||||
|
model.solve()
|
||||||
|
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
|
||||||
|
raise PulpSolverError(str(err))
|
||||||
|
|
||||||
|
def findSolutionValues(self, lp):
|
||||||
|
try:
|
||||||
|
model = lp.solverModel
|
||||||
|
# Collect results
|
||||||
|
if _ismip(lp) and self.mip:
|
||||||
|
# Solved as MIP
|
||||||
|
x, slacks, duals, djs = [], [], None, None
|
||||||
|
try:
|
||||||
|
model.getmipsol(x, slacks)
|
||||||
|
except:
|
||||||
|
x, slacks = None, None
|
||||||
|
statusmap = {
|
||||||
|
0: constants.LpStatusUndefined, # XPRS_MIP_NOT_LOADED
|
||||||
|
1: constants.LpStatusUndefined, # XPRS_MIP_LP_NOT_OPTIMAL
|
||||||
|
2: constants.LpStatusUndefined, # XPRS_MIP_LP_OPTIMAL
|
||||||
|
3: constants.LpStatusUndefined, # XPRS_MIP_NO_SOL_FOUND
|
||||||
|
4: constants.LpStatusUndefined, # XPRS_MIP_SOLUTION
|
||||||
|
5: constants.LpStatusInfeasible, # XPRS_MIP_INFEAS
|
||||||
|
6: constants.LpStatusOptimal, # XPRS_MIP_OPTIMAL
|
||||||
|
7: constants.LpStatusUndefined, # XPRS_MIP_UNBOUNDED
|
||||||
|
}
|
||||||
|
statuskey = "mipstatus"
|
||||||
|
else:
|
||||||
|
# Solved as continuous
|
||||||
|
x, slacks, duals, djs = [], [], [], []
|
||||||
|
try:
|
||||||
|
model.getlpsol(x, slacks, duals, djs)
|
||||||
|
except:
|
||||||
|
# No solution available
|
||||||
|
x, slacks, duals, djs = None, None, None, None
|
||||||
|
statusmap = {
|
||||||
|
0: constants.LpStatusNotSolved, # XPRS_LP_UNSTARTED
|
||||||
|
1: constants.LpStatusOptimal, # XPRS_LP_OPTIMAL
|
||||||
|
2: constants.LpStatusInfeasible, # XPRS_LP_INFEAS
|
||||||
|
3: constants.LpStatusUndefined, # XPRS_LP_CUTOFF
|
||||||
|
4: constants.LpStatusUndefined, # XPRS_LP_UNFINISHED
|
||||||
|
5: constants.LpStatusUnbounded, # XPRS_LP_UNBOUNDED
|
||||||
|
6: constants.LpStatusUndefined, # XPRS_LP_CUTOFF_IN_DUAL
|
||||||
|
7: constants.LpStatusNotSolved, # XPRS_LP_UNSOLVED
|
||||||
|
8: constants.LpStatusUndefined, # XPRS_LP_NONCONVEX
|
||||||
|
}
|
||||||
|
statuskey = "lpstatus"
|
||||||
|
if x is not None:
|
||||||
|
lp.assignVarsVals({v.name: x[v._xprs[0]] for v in lp.variables()})
|
||||||
|
if djs is not None:
|
||||||
|
lp.assignVarsDj({v.name: djs[v._xprs[0]] for v in lp.variables()})
|
||||||
|
if duals is not None:
|
||||||
|
lp.assignConsPi(
|
||||||
|
{c.name: duals[c._xprs[0]] for c in lp.constraints.values()}
|
||||||
|
)
|
||||||
|
if slacks is not None:
|
||||||
|
lp.assignConsSlack(
|
||||||
|
{c.name: slacks[c._xprs[0]] for c in lp.constraints.values()}
|
||||||
|
)
|
||||||
|
|
||||||
|
status = statusmap.get(
|
||||||
|
model.getAttrib(statuskey), constants.LpStatusUndefined
|
||||||
|
)
|
||||||
|
lp.assignStatus(status)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
|
||||||
|
raise PulpSolverError(str(err))
|
||||||
|
|
||||||
|
def actualSolve(self, lp, prepare=None):
|
||||||
|
"""Solve a well formulated lp problem"""
|
||||||
|
if not self.available():
|
||||||
|
# Import again to get a more verbose error message
|
||||||
|
message = "XPRESS Python API not available"
|
||||||
|
try:
|
||||||
|
import xpress
|
||||||
|
except ImportError as err:
|
||||||
|
message = str(err)
|
||||||
|
raise PulpSolverError(message)
|
||||||
|
|
||||||
|
self.buildSolverModel(lp)
|
||||||
|
self.callSolver(lp, prepare)
|
||||||
|
return self.findSolutionValues(lp)
|
||||||
|
|
||||||
|
def buildSolverModel(self, lp):
|
||||||
|
"""
|
||||||
|
Takes the pulp lp model and translates it into an xpress model
|
||||||
|
"""
|
||||||
|
self._extract(lp)
|
||||||
|
try:
|
||||||
|
# Apply controls, warmstart etc. We do this here rather than in
|
||||||
|
# callSolver() so that the caller has a chance to overwrite things
|
||||||
|
# either using the `prepare` argument to callSolver() or by
|
||||||
|
# explicitly calling
|
||||||
|
# self.buildSolverModel()
|
||||||
|
# self.callSolver()
|
||||||
|
# self.findSolutionValues()
|
||||||
|
# This also avoids setting warmstart information passed to the
|
||||||
|
# constructor from actualResolve(), which would almost certainly
|
||||||
|
# be unintended.
|
||||||
|
model = lp.solverModel
|
||||||
|
# Apply controls that were passed to the constructor
|
||||||
|
for key, name in [
|
||||||
|
("gapRel", "MIPRELSTOP"),
|
||||||
|
("timeLimit", "MAXTIME"),
|
||||||
|
("heurFreq", "HEURFREQ"),
|
||||||
|
("heurStra", "HEURSTRATEGY"),
|
||||||
|
("coverCuts", "COVERCUTS"),
|
||||||
|
("preSolve", "PRESOLVE"),
|
||||||
|
]:
|
||||||
|
value = self.optionsDict.get(key, None)
|
||||||
|
if value is not None:
|
||||||
|
model.setControl(name, value)
|
||||||
|
|
||||||
|
# Apply any other controls. These overwrite controls that were
|
||||||
|
# passed explicitly into the constructor.
|
||||||
|
for option in self.options:
|
||||||
|
if isinstance(option, tuple):
|
||||||
|
name = optione[0]
|
||||||
|
value = option[1]
|
||||||
|
else:
|
||||||
|
fields = option.split("=", 1)
|
||||||
|
if len(fields) != 2:
|
||||||
|
raise PulpSolverError("Invalid option " + str(option))
|
||||||
|
name = fields[0].strip()
|
||||||
|
value = fields[1].strip()
|
||||||
|
try:
|
||||||
|
model.setControl(name, int(value))
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
model.setControl(name, float(value))
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
model.setControl(name, value)
|
||||||
|
# Setup warmstart information
|
||||||
|
if self.optionsDict.get("warmStart", False):
|
||||||
|
solval = list()
|
||||||
|
colind = list()
|
||||||
|
for v in sorted(lp.variables(), key=lambda x: x._xprs[0]):
|
||||||
|
if v.value() is not None:
|
||||||
|
solval.append(v.value())
|
||||||
|
colind.append(v._xprs[0])
|
||||||
|
if _ismip(lp) and self.mip:
|
||||||
|
# If we have a value for every variable then use
|
||||||
|
# loadmipsol(), which requires a dense solution. Otherwise
|
||||||
|
# use addmipsol() which allows sparse vectors.
|
||||||
|
if len(solval) == model.attributes.cols:
|
||||||
|
model.loadmipsol(solval)
|
||||||
|
else:
|
||||||
|
model.addmipsol(solval, colind, "warmstart")
|
||||||
|
else:
|
||||||
|
model.loadlpsol(solval, None, None, None)
|
||||||
|
# Setup message callback if output is requested
|
||||||
|
if self.msg:
|
||||||
|
|
||||||
|
def message(prob, data, msg, msgtype):
|
||||||
|
if msgtype > 0:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
model.addcbmessage(message)
|
||||||
|
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
|
||||||
|
raise PulpSolverError(str(err))
|
||||||
|
|
||||||
|
def actualResolve(self, lp, prepare=None):
|
||||||
|
"""Resolve a problem that was previously solved by actualSolve()."""
|
||||||
|
try:
|
||||||
|
rhsind = list()
|
||||||
|
rhsval = list()
|
||||||
|
for name in sorted(lp.constraints):
|
||||||
|
con = lp.constraints[name]
|
||||||
|
if not con.modified:
|
||||||
|
continue
|
||||||
|
if not hasattr(con, "_xprs"):
|
||||||
|
# Adding constraints is not implemented at the moment
|
||||||
|
raise PulpSolverError("Cannot add new constraints")
|
||||||
|
# At the moment only RHS can change in pulp.py
|
||||||
|
rhsind.append(con._xprs[0])
|
||||||
|
rhsval.append(-con.constant)
|
||||||
|
if len(rhsind) > 0:
|
||||||
|
lp.solverModel.chgrhs(rhsind, rhsval)
|
||||||
|
|
||||||
|
bndind = list()
|
||||||
|
bndtype = list()
|
||||||
|
bndval = list()
|
||||||
|
for v in lp.variables():
|
||||||
|
if not v.modified:
|
||||||
|
continue
|
||||||
|
if not hasattr(v, "_xprs"):
|
||||||
|
# Adding variables is not implemented at the moment
|
||||||
|
raise PulpSolverError("Cannot add new variables")
|
||||||
|
# At the moment only bounds can change in pulp.py
|
||||||
|
bndind.append(v._xprs[0])
|
||||||
|
bndtype.append("L")
|
||||||
|
bndval.append(-xpress.infinity if v.lowBound is None else v.lowBound)
|
||||||
|
bndind.append(v._xprs[0])
|
||||||
|
bndtype.append("G")
|
||||||
|
bndval.append(xpress.infinity if v.upBound is None else v.upBound)
|
||||||
|
if len(bndtype) > 0:
|
||||||
|
lp.solverModel.chgbounds(bndind, bndtype, bndval)
|
||||||
|
|
||||||
|
self.callSolver(lp, prepare)
|
||||||
|
return self.findSolutionValues(lp)
|
||||||
|
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
|
||||||
|
raise PulpSolverError(str(err))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _reset(lp):
|
||||||
|
"""Reset any XPRESS specific information in lp."""
|
||||||
|
if hasattr(lp, "solverModel"):
|
||||||
|
delattr(lp, "solverModel")
|
||||||
|
for v in lp.variables():
|
||||||
|
if hasattr(v, "_xprs"):
|
||||||
|
delattr(v, "_xprs")
|
||||||
|
for c in lp.constraints.values():
|
||||||
|
if hasattr(c, "_xprs"):
|
||||||
|
delattr(c, "_xprs")
|
||||||
|
|
||||||
|
def _extract(self, lp):
|
||||||
|
"""Extract a given model to an XPRESS Python API instance.
|
||||||
|
|
||||||
|
The function stores XPRESS specific information in the `solverModel` property
|
||||||
|
of `lp` and each variable and constraint. These information can be
|
||||||
|
removed by calling `_reset`.
|
||||||
|
"""
|
||||||
|
self._reset(lp)
|
||||||
|
try:
|
||||||
|
model = xpress.problem()
|
||||||
|
if lp.sense == constants.LpMaximize:
|
||||||
|
model.chgobjsense(xpress.maximize)
|
||||||
|
|
||||||
|
# Create variables. We first collect the info for all variables
|
||||||
|
# and then create all of them in one shot. This is supposed to
|
||||||
|
# be faster in case we have to create a lot of variables.
|
||||||
|
obj = list()
|
||||||
|
lb = list()
|
||||||
|
ub = list()
|
||||||
|
ctype = list()
|
||||||
|
names = list()
|
||||||
|
for v in lp.variables():
|
||||||
|
lb.append(-xpress.infinity if v.lowBound is None else v.lowBound)
|
||||||
|
ub.append(xpress.infinity if v.upBound is None else v.upBound)
|
||||||
|
obj.append(lp.objective.get(v, 0.0))
|
||||||
|
if v.cat == constants.LpInteger:
|
||||||
|
ctype.append("I")
|
||||||
|
elif v.cat == constants.LpBinary:
|
||||||
|
ctype.append("B")
|
||||||
|
else:
|
||||||
|
ctype.append("C")
|
||||||
|
names.append(v.name)
|
||||||
|
model.addcols(obj, [0] * (len(obj) + 1), [], [], lb, ub, names, ctype)
|
||||||
|
for j, (v, x) in enumerate(zip(lp.variables(), model.getVariable())):
|
||||||
|
v._xprs = (j, x)
|
||||||
|
|
||||||
|
# Generate constraints. Sort by name to get deterministic
|
||||||
|
# ordering of constraints.
|
||||||
|
# Constraints are generated in blocks of 100 constraints to speed
|
||||||
|
# up things a bit but still keep memory usage small.
|
||||||
|
cons = list()
|
||||||
|
for i, name in enumerate(sorted(lp.constraints)):
|
||||||
|
con = lp.constraints[name]
|
||||||
|
# Sort the variables by index to get deterministic
|
||||||
|
# ordering of variables in the row.
|
||||||
|
lhs = xpress.Sum(
|
||||||
|
a * x._xprs[1]
|
||||||
|
for x, a in sorted(con.items(), key=lambda x: x[0]._xprs[0])
|
||||||
|
)
|
||||||
|
rhs = -con.constant
|
||||||
|
if con.sense == constants.LpConstraintLE:
|
||||||
|
c = xpress.constraint(body=lhs, sense=xpress.leq, rhs=rhs)
|
||||||
|
elif con.sense == constants.LpConstraintGE:
|
||||||
|
c = xpress.constraint(body=lhs, sense=xpress.geq, rhs=rhs)
|
||||||
|
elif con.sense == constants.LpConstraintEQ:
|
||||||
|
c = xpress.constraint(body=lhs, sense=xpress.eq, rhs=rhs)
|
||||||
|
else:
|
||||||
|
raise PulpSolverError(
|
||||||
|
"Unsupprted constraint type " + str(con.sense)
|
||||||
|
)
|
||||||
|
cons.append((i, c, con))
|
||||||
|
if len(cons) > 100:
|
||||||
|
model.addConstraint([c for _, c, _ in cons])
|
||||||
|
for i, c, con in cons:
|
||||||
|
con._xprs = (i, c)
|
||||||
|
cons = list()
|
||||||
|
if len(cons) > 0:
|
||||||
|
model.addConstraint([c for _, c, _ in cons])
|
||||||
|
for i, c, con in cons:
|
||||||
|
con._xprs = (i, c)
|
||||||
|
|
||||||
|
# SOS constraints
|
||||||
|
def addsos(m, sosdict, sostype):
|
||||||
|
"""Extract sos constraints from PuLP."""
|
||||||
|
soslist = []
|
||||||
|
# Sort by name to get deterministic ordering. Note that
|
||||||
|
# names may be plain integers, that is why we use str(name)
|
||||||
|
# to pass them to the SOS constructor.
|
||||||
|
for name in sorted(sosdict):
|
||||||
|
indices = []
|
||||||
|
weights = []
|
||||||
|
for v, val in sosdict[name].items():
|
||||||
|
indices.append(v._xprs[0])
|
||||||
|
weights.append(val)
|
||||||
|
soslist.append(xpress.sos(indices, weights, sostype, str(name)))
|
||||||
|
if len(soslist):
|
||||||
|
m.addSOS(soslist)
|
||||||
|
|
||||||
|
addsos(model, lp.sos1, 1)
|
||||||
|
addsos(model, lp.sos2, 2)
|
||||||
|
|
||||||
|
lp.solverModel = model
|
||||||
|
except (xpress.ModelError, xpress.InterfaceError, xpress.SolverError) as err:
|
||||||
|
# Undo everything
|
||||||
|
self._reset(lp)
|
||||||
|
raise PulpSolverError(str(err))
|
||||||
|
|
||||||
|
def getAttribute(self, lp, which):
|
||||||
|
"""Get an arbitrary attribute for the model that was previously
|
||||||
|
solved using actualSolve()."""
|
||||||
|
return lp.solverModel.getAttrib(which)
|
||||||
104
utils/pulp/constants.py
Normal file
104
utils/pulp/constants.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# PuLP : Python LP Modeler
|
||||||
|
|
||||||
|
# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org)
|
||||||
|
# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id:constants.py 1791 2008-04-23 22:54:34Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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."""
|
||||||
|
|
||||||
|
"""
|
||||||
|
This file contains the constant definitions for PuLP
|
||||||
|
Note that hopefully these will be changed into something more pythonic
|
||||||
|
"""
|
||||||
|
VERSION = "2.7.0"
|
||||||
|
EPS = 1e-7
|
||||||
|
|
||||||
|
# variable categories
|
||||||
|
LpContinuous = "Continuous"
|
||||||
|
LpInteger = "Integer"
|
||||||
|
LpBinary = "Binary"
|
||||||
|
LpCategories = {LpContinuous: "Continuous", LpInteger: "Integer", LpBinary: "Binary"}
|
||||||
|
|
||||||
|
# objective sense
|
||||||
|
LpMinimize = 1
|
||||||
|
LpMaximize = -1
|
||||||
|
LpSenses = {LpMaximize: "Maximize", LpMinimize: "Minimize"}
|
||||||
|
LpSensesMPS = {LpMaximize: "MAX", LpMinimize: "MIN"}
|
||||||
|
|
||||||
|
# problem status
|
||||||
|
LpStatusNotSolved = 0
|
||||||
|
LpStatusOptimal = 1
|
||||||
|
LpStatusInfeasible = -1
|
||||||
|
LpStatusUnbounded = -2
|
||||||
|
LpStatusUndefined = -3
|
||||||
|
LpStatus = {
|
||||||
|
LpStatusNotSolved: "Not Solved",
|
||||||
|
LpStatusOptimal: "Optimal",
|
||||||
|
LpStatusInfeasible: "Infeasible",
|
||||||
|
LpStatusUnbounded: "Unbounded",
|
||||||
|
LpStatusUndefined: "Undefined",
|
||||||
|
}
|
||||||
|
|
||||||
|
# solution status
|
||||||
|
LpSolutionNoSolutionFound = 0
|
||||||
|
LpSolutionOptimal = 1
|
||||||
|
LpSolutionIntegerFeasible = 2
|
||||||
|
LpSolutionInfeasible = -1
|
||||||
|
LpSolutionUnbounded = -2
|
||||||
|
LpSolution = {
|
||||||
|
LpSolutionNoSolutionFound: "No Solution Found",
|
||||||
|
LpSolutionOptimal: "Optimal Solution Found",
|
||||||
|
LpSolutionIntegerFeasible: "Solution Found",
|
||||||
|
LpSolutionInfeasible: "No Solution Exists",
|
||||||
|
LpSolutionUnbounded: "Solution is Unbounded",
|
||||||
|
}
|
||||||
|
LpStatusToSolution = {
|
||||||
|
LpStatusNotSolved: LpSolutionInfeasible,
|
||||||
|
LpStatusOptimal: LpSolutionOptimal,
|
||||||
|
LpStatusInfeasible: LpSolutionInfeasible,
|
||||||
|
LpStatusUnbounded: LpSolutionUnbounded,
|
||||||
|
LpStatusUndefined: LpSolutionInfeasible,
|
||||||
|
}
|
||||||
|
|
||||||
|
# constraint sense
|
||||||
|
LpConstraintLE = -1
|
||||||
|
LpConstraintEQ = 0
|
||||||
|
LpConstraintGE = 1
|
||||||
|
LpConstraintTypeToMps = {LpConstraintLE: "L", LpConstraintEQ: "E", LpConstraintGE: "G"}
|
||||||
|
LpConstraintSenses = {LpConstraintEQ: "=", LpConstraintLE: "<=", LpConstraintGE: ">="}
|
||||||
|
# LP line size
|
||||||
|
LpCplexLPLineSize = 78
|
||||||
|
|
||||||
|
|
||||||
|
def isiterable(obj):
|
||||||
|
try:
|
||||||
|
obj = iter(obj)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class PulpError(Exception):
|
||||||
|
"""
|
||||||
|
Pulp Exception Class
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
401
utils/pulp/mps_lp.py
Normal file
401
utils/pulp/mps_lp.py
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
@author: Franco Peschiera
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from . import constants as const
|
||||||
|
|
||||||
|
CORE_FILE_ROW_MODE = "ROWS"
|
||||||
|
CORE_FILE_COL_MODE = "COLUMNS"
|
||||||
|
CORE_FILE_RHS_MODE = "RHS"
|
||||||
|
CORE_FILE_BOUNDS_MODE = "BOUNDS"
|
||||||
|
|
||||||
|
CORE_FILE_BOUNDS_MODE_NAME_GIVEN = "BOUNDS_NAME"
|
||||||
|
CORE_FILE_BOUNDS_MODE_NO_NAME = "BOUNDS_NO_NAME"
|
||||||
|
CORE_FILE_RHS_MODE_NAME_GIVEN = "RHS_NAME"
|
||||||
|
CORE_FILE_RHS_MODE_NO_NAME = "RHS_NO_NAME"
|
||||||
|
|
||||||
|
ROW_MODE_OBJ = "N"
|
||||||
|
|
||||||
|
BOUNDS_EQUIV = dict(LO="lowBound", UP="upBound")
|
||||||
|
|
||||||
|
ROW_EQUIV = {v: k for k, v in const.LpConstraintTypeToMps.items()}
|
||||||
|
COL_EQUIV = {1: "Integer", 0: "Continuous"}
|
||||||
|
ROW_DEFAULT = dict(pi=None, constant=0)
|
||||||
|
COL_DEFAULT = dict(lowBound=0, upBound=None, varValue=None, dj=None)
|
||||||
|
|
||||||
|
|
||||||
|
def readMPS(path, sense, dropConsNames=False):
|
||||||
|
"""
|
||||||
|
adapted from Julian Märte (https://github.com/pchtsp/pysmps)
|
||||||
|
returns a dictionary with the contents of the model.
|
||||||
|
This dictionary can be used to generate an LpProblem
|
||||||
|
|
||||||
|
:param path: path of mps file
|
||||||
|
:param sense: 1 for minimize, -1 for maximize
|
||||||
|
:param dropConsNames: if True, do not store the names of constraints
|
||||||
|
:return: a dictionary with all the problem data
|
||||||
|
"""
|
||||||
|
|
||||||
|
mode = ""
|
||||||
|
parameters = dict(name="", sense=sense, status=0, sol_status=0)
|
||||||
|
variable_info = {}
|
||||||
|
constraints = {}
|
||||||
|
objective = dict(name="", coefficients=[])
|
||||||
|
sos1 = []
|
||||||
|
sos2 = []
|
||||||
|
# TODO: maybe take out rhs_names and bnd_names? not sure if they're useful
|
||||||
|
rhs_names = []
|
||||||
|
bnd_names = []
|
||||||
|
integral_marker = False
|
||||||
|
|
||||||
|
with open(path) as reader:
|
||||||
|
for line in reader:
|
||||||
|
line = re.split(" |\t", line)
|
||||||
|
line = [x.strip() for x in line]
|
||||||
|
line = list(filter(None, line))
|
||||||
|
|
||||||
|
if line[0] == "ENDATA":
|
||||||
|
break
|
||||||
|
if line[0] == "*":
|
||||||
|
continue
|
||||||
|
if line[0] == "NAME":
|
||||||
|
if len(line) > 1:
|
||||||
|
parameters["name"] = line[1]
|
||||||
|
else:
|
||||||
|
parameters["name"] = ""
|
||||||
|
continue
|
||||||
|
|
||||||
|
# here we get the mode
|
||||||
|
if line[0] in [CORE_FILE_ROW_MODE, CORE_FILE_COL_MODE]:
|
||||||
|
mode = line[0]
|
||||||
|
elif line[0] == CORE_FILE_RHS_MODE and len(line) <= 2:
|
||||||
|
if len(line) > 1:
|
||||||
|
rhs_names.append(line[1])
|
||||||
|
mode = CORE_FILE_RHS_MODE_NAME_GIVEN
|
||||||
|
else:
|
||||||
|
mode = CORE_FILE_RHS_MODE_NO_NAME
|
||||||
|
elif line[0] == CORE_FILE_BOUNDS_MODE and len(line) <= 2:
|
||||||
|
if len(line) > 1:
|
||||||
|
bnd_names.append(line[1])
|
||||||
|
mode = CORE_FILE_BOUNDS_MODE_NAME_GIVEN
|
||||||
|
else:
|
||||||
|
mode = CORE_FILE_BOUNDS_MODE_NO_NAME
|
||||||
|
|
||||||
|
# here we query the mode variable
|
||||||
|
elif mode == CORE_FILE_ROW_MODE:
|
||||||
|
row_type = line[0]
|
||||||
|
row_name = line[1]
|
||||||
|
if row_type == ROW_MODE_OBJ:
|
||||||
|
objective["name"] = row_name
|
||||||
|
else:
|
||||||
|
constraints[row_name] = dict(
|
||||||
|
sense=ROW_EQUIV[row_type],
|
||||||
|
name=row_name,
|
||||||
|
coefficients=[],
|
||||||
|
**ROW_DEFAULT,
|
||||||
|
)
|
||||||
|
elif mode == CORE_FILE_COL_MODE:
|
||||||
|
var_name = line[0]
|
||||||
|
if len(line) > 1 and line[1] == "'MARKER'":
|
||||||
|
if line[2] == "'INTORG'":
|
||||||
|
integral_marker = True
|
||||||
|
elif line[2] == "'INTEND'":
|
||||||
|
integral_marker = False
|
||||||
|
continue
|
||||||
|
if var_name not in variable_info:
|
||||||
|
variable_info[var_name] = dict(
|
||||||
|
cat=COL_EQUIV[integral_marker], name=var_name, **COL_DEFAULT
|
||||||
|
)
|
||||||
|
j = 1
|
||||||
|
while j < len(line) - 1:
|
||||||
|
if line[j] == objective["name"]:
|
||||||
|
# we store the variable objective coefficient
|
||||||
|
objective["coefficients"].append(
|
||||||
|
dict(name=var_name, value=float(line[j + 1]))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# we store the variable coefficient
|
||||||
|
constraints[line[j]]["coefficients"].append(
|
||||||
|
dict(name=var_name, value=float(line[j + 1]))
|
||||||
|
)
|
||||||
|
j = j + 2
|
||||||
|
elif mode == CORE_FILE_RHS_MODE_NAME_GIVEN:
|
||||||
|
if line[0] != rhs_names[-1]:
|
||||||
|
raise Exception(
|
||||||
|
"Other RHS name was given even though name was set after RHS tag."
|
||||||
|
)
|
||||||
|
readMPSSetRhs(line, constraints)
|
||||||
|
elif mode == CORE_FILE_RHS_MODE_NO_NAME:
|
||||||
|
readMPSSetRhs(line, constraints)
|
||||||
|
if line[0] not in rhs_names:
|
||||||
|
rhs_names.append(line[0])
|
||||||
|
elif mode == CORE_FILE_BOUNDS_MODE_NAME_GIVEN:
|
||||||
|
if line[1] != bnd_names[-1]:
|
||||||
|
raise Exception(
|
||||||
|
"Other BOUNDS name was given even though name was set after BOUNDS tag."
|
||||||
|
)
|
||||||
|
readMPSSetBounds(line, variable_info)
|
||||||
|
elif mode == CORE_FILE_BOUNDS_MODE_NO_NAME:
|
||||||
|
readMPSSetBounds(line, variable_info)
|
||||||
|
if line[1] not in bnd_names:
|
||||||
|
bnd_names.append(line[1])
|
||||||
|
constraints = list(constraints.values())
|
||||||
|
if dropConsNames:
|
||||||
|
for c in constraints:
|
||||||
|
c["name"] = None
|
||||||
|
objective["name"] = None
|
||||||
|
variable_info = list(variable_info.values())
|
||||||
|
return dict(
|
||||||
|
parameters=parameters,
|
||||||
|
objective=objective,
|
||||||
|
variables=variable_info,
|
||||||
|
constraints=constraints,
|
||||||
|
sos1=sos1,
|
||||||
|
sos2=sos2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def readMPSSetBounds(line, variable_dict):
|
||||||
|
bound = line[0]
|
||||||
|
var_name = line[2]
|
||||||
|
|
||||||
|
def set_one_bound(bound_type, value):
|
||||||
|
variable_dict[var_name][BOUNDS_EQUIV[bound_type]] = value
|
||||||
|
|
||||||
|
def set_both_bounds(value_low, value_up):
|
||||||
|
set_one_bound("LO", value_low)
|
||||||
|
set_one_bound("UP", value_up)
|
||||||
|
|
||||||
|
if bound == "FR":
|
||||||
|
set_both_bounds(None, None)
|
||||||
|
return
|
||||||
|
elif bound == "BV":
|
||||||
|
set_both_bounds(0, 1)
|
||||||
|
return
|
||||||
|
value = float(line[3])
|
||||||
|
if bound in ["LO", "UP"]:
|
||||||
|
set_one_bound(bound, value)
|
||||||
|
elif bound == "FX":
|
||||||
|
set_both_bounds(value, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def readMPSSetRhs(line, constraintsDict):
|
||||||
|
constraintsDict[line[1]]["constant"] = -float(line[2])
|
||||||
|
if len(line) == 5: # read fields 5, 6
|
||||||
|
constraintsDict[line[3]]["constant"] = -float(line[4])
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def writeMPS(
|
||||||
|
LpProblem, filename, mpsSense=0, rename=0, mip=1, with_objsense: bool = False
|
||||||
|
):
|
||||||
|
wasNone, dummyVar = LpProblem.fixObjective()
|
||||||
|
if mpsSense == 0:
|
||||||
|
mpsSense = LpProblem.sense
|
||||||
|
cobj = LpProblem.objective
|
||||||
|
if mpsSense != LpProblem.sense:
|
||||||
|
n = cobj.name
|
||||||
|
cobj = -cobj
|
||||||
|
cobj.name = n
|
||||||
|
if rename:
|
||||||
|
constrNames, varNames, cobj.name = LpProblem.normalisedNames()
|
||||||
|
# No need to call self.variables() again, we have just filled self._variables:
|
||||||
|
vs = LpProblem._variables
|
||||||
|
else:
|
||||||
|
vs = LpProblem.variables()
|
||||||
|
varNames = {v.name: v.name for v in vs}
|
||||||
|
constrNames = {c: c for c in LpProblem.constraints}
|
||||||
|
model_name = LpProblem.name
|
||||||
|
if rename:
|
||||||
|
model_name = "MODEL"
|
||||||
|
objName = cobj.name
|
||||||
|
if not objName:
|
||||||
|
objName = "OBJ"
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
row_lines = [
|
||||||
|
" " + const.LpConstraintTypeToMps[c.sense] + " " + constrNames[k] + "\n"
|
||||||
|
for k, c in LpProblem.constraints.items()
|
||||||
|
]
|
||||||
|
# Creation of a dict of dict:
|
||||||
|
# coefs[variable_name][constraint_name] = coefficient
|
||||||
|
coefs = {varNames[v.name]: {} for v in vs}
|
||||||
|
for k, c in LpProblem.constraints.items():
|
||||||
|
k = constrNames[k]
|
||||||
|
for v, value in c.items():
|
||||||
|
coefs[varNames[v.name]][k] = value
|
||||||
|
|
||||||
|
# matrix
|
||||||
|
columns_lines = []
|
||||||
|
for v in vs:
|
||||||
|
name = varNames[v.name]
|
||||||
|
columns_lines.extend(
|
||||||
|
writeMPSColumnLines(coefs[name], v, mip, name, cobj, objName)
|
||||||
|
)
|
||||||
|
|
||||||
|
# right hand side
|
||||||
|
rhs_lines = [
|
||||||
|
" RHS %-8s % .12e\n"
|
||||||
|
% (constrNames[k], -c.constant if c.constant != 0 else 0)
|
||||||
|
for k, c in LpProblem.constraints.items()
|
||||||
|
]
|
||||||
|
# bounds
|
||||||
|
bound_lines = []
|
||||||
|
for v in vs:
|
||||||
|
bound_lines.extend(writeMPSBoundLines(varNames[v.name], v, mip))
|
||||||
|
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
if with_objsense:
|
||||||
|
f.write("OBJSENSE\n")
|
||||||
|
f.write(f" {const.LpSensesMPS[mpsSense]}\n")
|
||||||
|
else:
|
||||||
|
f.write(f"*SENSE:{const.LpSenses[mpsSense]}\n")
|
||||||
|
f.write(f"NAME {model_name}\n")
|
||||||
|
f.write("ROWS\n")
|
||||||
|
f.write(f" N {objName}\n")
|
||||||
|
f.write("".join(row_lines))
|
||||||
|
f.write("COLUMNS\n")
|
||||||
|
f.write("".join(columns_lines))
|
||||||
|
f.write("RHS\n")
|
||||||
|
f.write("".join(rhs_lines))
|
||||||
|
f.write("BOUNDS\n")
|
||||||
|
f.write("".join(bound_lines))
|
||||||
|
f.write("ENDATA\n")
|
||||||
|
LpProblem.restoreObjective(wasNone, dummyVar)
|
||||||
|
# returns the variables, in writing order
|
||||||
|
if rename == 0:
|
||||||
|
return vs
|
||||||
|
else:
|
||||||
|
return vs, varNames, constrNames, cobj.name
|
||||||
|
|
||||||
|
|
||||||
|
def writeMPSColumnLines(cv, variable, mip, name, cobj, objName):
|
||||||
|
columns_lines = []
|
||||||
|
if mip and variable.cat == const.LpInteger:
|
||||||
|
columns_lines.append(" MARK 'MARKER' 'INTORG'\n")
|
||||||
|
# Most of the work is done here
|
||||||
|
_tmp = [" %-8s %-8s % .12e\n" % (name, k, v) for k, v in cv.items()]
|
||||||
|
columns_lines.extend(_tmp)
|
||||||
|
|
||||||
|
# objective function
|
||||||
|
if variable in cobj:
|
||||||
|
columns_lines.append(
|
||||||
|
" %-8s %-8s % .12e\n" % (name, objName, cobj[variable])
|
||||||
|
)
|
||||||
|
if mip and variable.cat == const.LpInteger:
|
||||||
|
columns_lines.append(" MARK 'MARKER' 'INTEND'\n")
|
||||||
|
return columns_lines
|
||||||
|
|
||||||
|
|
||||||
|
def writeMPSBoundLines(name, variable, mip):
|
||||||
|
if variable.lowBound is not None and variable.lowBound == variable.upBound:
|
||||||
|
return [" FX BND %-8s % .12e\n" % (name, variable.lowBound)]
|
||||||
|
elif (
|
||||||
|
variable.lowBound == 0
|
||||||
|
and variable.upBound == 1
|
||||||
|
and mip
|
||||||
|
and variable.cat == const.LpInteger
|
||||||
|
):
|
||||||
|
return [" BV BND %-8s\n" % name]
|
||||||
|
bound_lines = []
|
||||||
|
if variable.lowBound is not None:
|
||||||
|
# In MPS files, variables with no bounds (i.e. >= 0)
|
||||||
|
# are assumed BV by COIN and CPLEX.
|
||||||
|
# So we explicitly write a 0 lower bound in this case.
|
||||||
|
if variable.lowBound != 0 or (
|
||||||
|
mip and variable.cat == const.LpInteger and variable.upBound is None
|
||||||
|
):
|
||||||
|
bound_lines.append(
|
||||||
|
" LO BND %-8s % .12e\n" % (name, variable.lowBound)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if variable.upBound is not None:
|
||||||
|
bound_lines.append(" MI BND %-8s\n" % name)
|
||||||
|
else:
|
||||||
|
bound_lines.append(" FR BND %-8s\n" % name)
|
||||||
|
if variable.upBound is not None:
|
||||||
|
bound_lines.append(" UP BND %-8s % .12e\n" % (name, variable.upBound))
|
||||||
|
return bound_lines
|
||||||
|
|
||||||
|
|
||||||
|
def writeLP(LpProblem, filename, writeSOS=1, mip=1, max_length=100):
|
||||||
|
f = open(filename, "w")
|
||||||
|
f.write("\\* " + LpProblem.name + " *\\\n")
|
||||||
|
if LpProblem.sense == 1:
|
||||||
|
f.write("Minimize\n")
|
||||||
|
else:
|
||||||
|
f.write("Maximize\n")
|
||||||
|
wasNone, objectiveDummyVar = LpProblem.fixObjective()
|
||||||
|
objName = LpProblem.objective.name
|
||||||
|
if not objName:
|
||||||
|
objName = "OBJ"
|
||||||
|
f.write(LpProblem.objective.asCplexLpAffineExpression(objName, constant=0))
|
||||||
|
f.write("Subject To\n")
|
||||||
|
ks = list(LpProblem.constraints.keys())
|
||||||
|
ks.sort()
|
||||||
|
dummyWritten = False
|
||||||
|
for k in ks:
|
||||||
|
constraint = LpProblem.constraints[k]
|
||||||
|
if not list(constraint.keys()):
|
||||||
|
# empty constraint add the dummyVar
|
||||||
|
dummyVar = LpProblem.get_dummyVar()
|
||||||
|
constraint += dummyVar
|
||||||
|
# set this dummyvar to zero so infeasible problems are not made feasible
|
||||||
|
if not dummyWritten:
|
||||||
|
f.write((dummyVar == 0.0).asCplexLpConstraint("_dummy"))
|
||||||
|
dummyWritten = True
|
||||||
|
f.write(constraint.asCplexLpConstraint(k))
|
||||||
|
# check if any names are longer than 100 characters
|
||||||
|
LpProblem.checkLengthVars(max_length)
|
||||||
|
vs = LpProblem.variables()
|
||||||
|
# check for repeated names
|
||||||
|
LpProblem.checkDuplicateVars()
|
||||||
|
# Bounds on non-"positive" variables
|
||||||
|
# Note: XPRESS and CPLEX do not interpret integer variables without
|
||||||
|
# explicit bounds
|
||||||
|
if mip:
|
||||||
|
vg = [
|
||||||
|
v
|
||||||
|
for v in vs
|
||||||
|
if not (v.isPositive() and v.cat == const.LpContinuous) and not v.isBinary()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
vg = [v for v in vs if not v.isPositive()]
|
||||||
|
if vg:
|
||||||
|
f.write("Bounds\n")
|
||||||
|
for v in vg:
|
||||||
|
f.write(f" {v.asCplexLpVariable()}\n")
|
||||||
|
# Integer non-binary variables
|
||||||
|
if mip:
|
||||||
|
vg = [v for v in vs if v.cat == const.LpInteger and not v.isBinary()]
|
||||||
|
if vg:
|
||||||
|
f.write("Generals\n")
|
||||||
|
for v in vg:
|
||||||
|
f.write(f"{v.name}\n")
|
||||||
|
# Binary variables
|
||||||
|
vg = [v for v in vs if v.isBinary()]
|
||||||
|
if vg:
|
||||||
|
f.write("Binaries\n")
|
||||||
|
for v in vg:
|
||||||
|
f.write(f"{v.name}\n")
|
||||||
|
# Special Ordered Sets
|
||||||
|
if writeSOS and (LpProblem.sos1 or LpProblem.sos2):
|
||||||
|
f.write("SOS\n")
|
||||||
|
if LpProblem.sos1:
|
||||||
|
for sos in LpProblem.sos1.values():
|
||||||
|
f.write("S1:: \n")
|
||||||
|
for v, val in sos.items():
|
||||||
|
f.write(f" {v.name}: {val:.12g}\n")
|
||||||
|
if LpProblem.sos2:
|
||||||
|
for sos in LpProblem.sos2.values():
|
||||||
|
f.write("S2:: \n")
|
||||||
|
for v, val in sos.items():
|
||||||
|
f.write(f" {v.name}: {val:.12g}\n")
|
||||||
|
f.write("End\n")
|
||||||
|
f.close()
|
||||||
|
LpProblem.restoreObjective(wasNone, objectiveDummyVar)
|
||||||
|
return vs
|
||||||
15
utils/pulp/pulp.cfg.buildout
Normal file
15
utils/pulp/pulp.cfg.buildout
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# The configuration file that holds locations for 3rd party solvers
|
||||||
|
# This is an appropriate configuration file for linux uses and in this case is assuming that the
|
||||||
|
# needed libraries are in the same directory as the config file (note this is not ideal and
|
||||||
|
# may change in later versions)
|
||||||
|
# Libraries are ordered in the needed order to resolve dependancies and CoinMP is loaded last
|
||||||
|
# a windows specific configuation file is pulp.cfg.win
|
||||||
|
#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $
|
||||||
|
[locations]
|
||||||
|
CoinMPPath = %(here)s/../../parts/coinMP/lib/libCoinUtils.so, %(here)s/../../parts/coinMP/lib/libClp.so, %(here)s/../../parts/coinMP/lib/libOsi.so, %(here)s/../../parts/coinMP/lib/libOsiClp.so, %(here)s/../../parts/coinMP/lib/libCgl.so, %(here)s/../../parts/coinMP/lib/libCbc.so, %(here)s/../../parts/coinMP/lib/libOsiCbc.so, %(here)s/../../parts/coinMP/lib/libCbcSolver.so, %(here)s/../../parts/coinMP/lib/libCoinMP.so
|
||||||
|
CplexPath = /usr/ilog/cplex/bin/x86_rhel4.0_3.4/libcplex110.so
|
||||||
|
#note the recommended gurobi changes must be made to your PATH
|
||||||
|
#and LD_LIBRARY_PATH enviroment variables
|
||||||
|
GurobiPath = /opt/gurobi201/linux32/lib/python2.5
|
||||||
|
CbcPath = %(here)s/../../parts/cbc/bin/cbc
|
||||||
|
GlpkPath = %(here)s/../../parts/glpk/bin/glpsol
|
||||||
18
utils/pulp/pulp.cfg.win
Normal file
18
utils/pulp/pulp.cfg.win
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# The configuration file that holds locations for 3rd party solvers
|
||||||
|
# for CoinMp.dll it is assumed that no location will place the file in the
|
||||||
|
# same directory as the configuration file. (note this is not ideal and
|
||||||
|
# may change in later versions)
|
||||||
|
# a linux file is provided in pulp.cfg.linux
|
||||||
|
#$Id: pulp.cfg 1704 2007-12-20 21:56:14Z smit023 $
|
||||||
|
[locations]
|
||||||
|
CoinMPPath = %(here)s\solverdir\coinMP\%(os)s\%(arch)s\CoinMP.dll
|
||||||
|
CplexPath = cplex100.dll
|
||||||
|
#must have gurobi.dll somewhere on the search path
|
||||||
|
GurobiPath = C:\gurobi10\win32\python2.5\lib
|
||||||
|
CbcPath = cbc
|
||||||
|
GlpkPath = glpsol
|
||||||
|
PulpCbcPath = %(here)s\solverdir\cbc\%(os)s\%(arch)s\cbc.exe
|
||||||
|
PulpChocoPath = %(here)s/solverdir/choco/choco-parsers-with-dependencies.jar
|
||||||
|
[licenses]
|
||||||
|
ilm_cplex_license = "LICENSE your-enterprise\nRUNTIME NEVER ..."
|
||||||
|
ilm_cplex_license_signature = 0
|
||||||
2298
utils/pulp/pulp.py
Normal file
2298
utils/pulp/pulp.py
Normal file
File diff suppressed because it is too large
Load Diff
88
utils/pulp/sparse.py
Normal file
88
utils/pulp/sparse.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Sparse : Python basic dictionary sparse matrix
|
||||||
|
|
||||||
|
# Copyright (c) 2007, Stuart Mitchell (s.mitchell@auckland.ac.nz)
|
||||||
|
# $Id: sparse.py 1704 2007-12-20 21:56:14Z smit023 $
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# The above copyright notice and this permission notice shall be included
|
||||||
|
# in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sparse this module provides basic pure python sparse matrix implementation
|
||||||
|
notably this allows the sparse matrix to be output in various formats
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Matrix(dict):
|
||||||
|
"""This is a dictionary based sparse matrix class"""
|
||||||
|
|
||||||
|
def __init__(self, rows, cols):
|
||||||
|
"""initialises the class by creating a matrix that will have the given
|
||||||
|
rows and columns
|
||||||
|
"""
|
||||||
|
self.rows = rows
|
||||||
|
self.cols = cols
|
||||||
|
self.rowdict = {row: {} for row in rows}
|
||||||
|
self.coldict = {col: {} for col in cols}
|
||||||
|
|
||||||
|
def add(self, row, col, item, colcheck=False, rowcheck=False):
|
||||||
|
if not (rowcheck and row not in self.rows):
|
||||||
|
if not (colcheck and col not in self.cols):
|
||||||
|
dict.__setitem__(self, (row, col), item)
|
||||||
|
self.rowdict[row][col] = item
|
||||||
|
self.coldict[col][row] = item
|
||||||
|
else:
|
||||||
|
print(self.cols)
|
||||||
|
raise RuntimeError(f"col {col} is not in the matrix columns")
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"row {row} is not in the matrix rows")
|
||||||
|
|
||||||
|
def addcol(self, col, rowitems):
|
||||||
|
"""adds a column"""
|
||||||
|
if col in self.cols:
|
||||||
|
for row, item in rowitems.items():
|
||||||
|
self.add(row, col, item, colcheck=False)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("col is not in the matrix columns")
|
||||||
|
|
||||||
|
def get(self, k, d=0):
|
||||||
|
return dict.get(self, k, d)
|
||||||
|
|
||||||
|
def col_based_arrays(self):
|
||||||
|
numEls = len(self)
|
||||||
|
elemBase = []
|
||||||
|
startsBase = []
|
||||||
|
indBase = []
|
||||||
|
lenBase = []
|
||||||
|
for i, col in enumerate(self.cols):
|
||||||
|
startsBase.append(len(elemBase))
|
||||||
|
elemBase.extend(list(self.coldict[col].values()))
|
||||||
|
indBase.extend(list(self.coldict[col].keys()))
|
||||||
|
lenBase.append(len(elemBase) - startsBase[-1])
|
||||||
|
startsBase.append(len(elemBase))
|
||||||
|
return numEls, startsBase, lenBase, indBase, elemBase
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
"""unit test"""
|
||||||
|
rows = list(range(10))
|
||||||
|
cols = list(range(50, 60))
|
||||||
|
mat = Matrix(rows, cols)
|
||||||
|
mat.add(1, 52, "item")
|
||||||
|
mat.add(2, 54, "stuff")
|
||||||
|
print(mat.col_based_arrays())
|
||||||
1
utils/pulp/tests/__init__.py
Normal file
1
utils/pulp/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .run_tests import pulpTestAll
|
||||||
51
utils/pulp/tests/bin_packing_problem.py
Normal file
51
utils/pulp/tests/bin_packing_problem.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
from pulp import *
|
||||||
|
import random
|
||||||
|
from itertools import product
|
||||||
|
|
||||||
|
|
||||||
|
def _bin_packing_instance(bins, seed=0):
|
||||||
|
packed_bins = [[] for _ in range(bins)]
|
||||||
|
bin_size = bins * 100
|
||||||
|
random.seed(seed)
|
||||||
|
for i in range(len(packed_bins)):
|
||||||
|
remaining_size = bin_size
|
||||||
|
while remaining_size >= 1:
|
||||||
|
item = random.randrange(1, remaining_size + 10)
|
||||||
|
packed_bins[i].append(item)
|
||||||
|
remaining_size -= item
|
||||||
|
packed_bins[i][-1] += remaining_size
|
||||||
|
all_items_with_bin = [(n, i) for i, l in enumerate(packed_bins) for n in l]
|
||||||
|
|
||||||
|
random.shuffle(all_items_with_bin)
|
||||||
|
items, packing = zip(*all_items_with_bin)
|
||||||
|
return items, packing, bin_size
|
||||||
|
|
||||||
|
|
||||||
|
def create_bin_packing_problem(bins, seed=0):
|
||||||
|
items, packing, bin_size = _bin_packing_instance(bins=bins, seed=seed)
|
||||||
|
|
||||||
|
prob = LpProblem("bin_packing", LpMinimize)
|
||||||
|
|
||||||
|
bin_indices = [i for i in range(len(items))]
|
||||||
|
item_indices = [i for i in range(len(items))]
|
||||||
|
|
||||||
|
using_bin = LpVariable.dicts("y", bin_indices, cat=LpBinary)
|
||||||
|
items_packed = LpVariable.dicts(
|
||||||
|
"x", indices=product(item_indices, bin_indices), cat=LpBinary
|
||||||
|
)
|
||||||
|
|
||||||
|
prob += lpSum(using_bin), "objective"
|
||||||
|
|
||||||
|
# pack every item
|
||||||
|
for i in item_indices:
|
||||||
|
prob += lpSum(items_packed[i, b] for b in bin_indices) == 1, f"pack_item_{i}"
|
||||||
|
|
||||||
|
# no bin overfilled
|
||||||
|
for b in bin_indices:
|
||||||
|
expr = (
|
||||||
|
lpSum([items[i] * items_packed[i, b] for i in item_indices])
|
||||||
|
<= bin_size * using_bin[b]
|
||||||
|
)
|
||||||
|
prob += expr, f"respect_bin_size_{b}"
|
||||||
|
|
||||||
|
return prob
|
||||||
33
utils/pulp/tests/run_tests.py
Normal file
33
utils/pulp/tests/run_tests.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import unittest
|
||||||
|
import pulp
|
||||||
|
from pulp.tests import test_pulp, test_examples, test_gurobipy_env
|
||||||
|
|
||||||
|
|
||||||
|
def pulpTestAll(test_docs=False):
|
||||||
|
runner = unittest.TextTestRunner()
|
||||||
|
suite_all = get_test_suite(test_docs)
|
||||||
|
# we run all tests at the same time
|
||||||
|
ret = runner.run(suite_all)
|
||||||
|
if not ret.wasSuccessful():
|
||||||
|
raise pulp.PulpError("Tests Failed")
|
||||||
|
|
||||||
|
|
||||||
|
def get_test_suite(test_docs=False):
|
||||||
|
# Tests
|
||||||
|
loader = unittest.TestLoader()
|
||||||
|
suite_all = unittest.TestSuite()
|
||||||
|
# we get suite with all PuLP tests
|
||||||
|
pulp_solver_tests = loader.loadTestsFromModule(test_pulp)
|
||||||
|
suite_all.addTests(pulp_solver_tests)
|
||||||
|
# Add tests for gurobipy env
|
||||||
|
gurobipy_env = loader.loadTestsFromModule(test_gurobipy_env)
|
||||||
|
suite_all.addTests(gurobipy_env)
|
||||||
|
# We add examples and docs tests
|
||||||
|
if test_docs:
|
||||||
|
docs_examples = loader.loadTestsFromTestCase(test_examples.Examples_DocsTests)
|
||||||
|
suite_all.addTests(docs_examples)
|
||||||
|
return suite_all
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pulpTestAll(test_docs=False)
|
||||||
36
utils/pulp/tests/test_examples.py
Normal file
36
utils/pulp/tests/test_examples.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import pulp
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
|
class Examples_DocsTests(unittest.TestCase):
|
||||||
|
def test_examples(self, examples_dir="../../examples"):
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
this_file = os.path.realpath(__file__)
|
||||||
|
parent_dir = os.path.dirname(this_file)
|
||||||
|
files = os.listdir(os.path.join(parent_dir, examples_dir))
|
||||||
|
TMP_dir = "_tmp/"
|
||||||
|
if not os.path.exists(TMP_dir):
|
||||||
|
os.mkdir(TMP_dir)
|
||||||
|
for f_name in files:
|
||||||
|
if os.path.isdir(f_name):
|
||||||
|
continue
|
||||||
|
_f_name = "examples." + os.path.splitext(f_name)[0]
|
||||||
|
os.chdir(TMP_dir)
|
||||||
|
importlib.import_module(_f_name)
|
||||||
|
os.chdir("../")
|
||||||
|
shutil.rmtree(TMP_dir)
|
||||||
|
|
||||||
|
def test_doctest(self):
|
||||||
|
"""
|
||||||
|
runs all doctests
|
||||||
|
"""
|
||||||
|
import doctest
|
||||||
|
|
||||||
|
doctest.testmod(pulp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
122
utils/pulp/tests/test_gurobipy_env.py
Normal file
122
utils/pulp/tests/test_gurobipy_env.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from pulp import GUROBI, LpProblem, LpVariable, const
|
||||||
|
|
||||||
|
try:
|
||||||
|
import gurobipy as gp
|
||||||
|
from gurobipy import GRB
|
||||||
|
except ImportError:
|
||||||
|
gp = None
|
||||||
|
|
||||||
|
|
||||||
|
def check_dummy_env():
|
||||||
|
with gp.Env(params={"OutputFlag": 0}):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def generate_lp() -> LpProblem:
|
||||||
|
prob = LpProblem("test", const.LpMaximize)
|
||||||
|
x = LpVariable("x", 0, 1)
|
||||||
|
y = LpVariable("y", 0, 1)
|
||||||
|
z = LpVariable("z", 0, 1)
|
||||||
|
prob += x + y + z, "obj"
|
||||||
|
prob += x + y + z <= 1, "c1"
|
||||||
|
return prob
|
||||||
|
|
||||||
|
|
||||||
|
class GurobiEnvTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
if gp is None:
|
||||||
|
self.skipTest("Skipping all tests in test_gurobipy_env.py")
|
||||||
|
self.options = {"Method": 0}
|
||||||
|
self.env_options = {"MemLimit": 1, "OutputFlag": 0}
|
||||||
|
|
||||||
|
def test_gp_env(self):
|
||||||
|
# Using gp.Env within a context manager
|
||||||
|
with gp.Env(params=self.env_options) as env:
|
||||||
|
prob = generate_lp()
|
||||||
|
solver = GUROBI(msg=False, env=env, **self.options)
|
||||||
|
prob.solve(solver)
|
||||||
|
solver.close()
|
||||||
|
check_dummy_env()
|
||||||
|
|
||||||
|
@unittest.SkipTest
|
||||||
|
def test_gp_env_no_close(self):
|
||||||
|
# Not closing results in an error for a single use license.
|
||||||
|
with gp.Env(params=self.env_options) as env:
|
||||||
|
prob = generate_lp()
|
||||||
|
solver = GUROBI(msg=False, env=env, **self.options)
|
||||||
|
prob.solve(solver)
|
||||||
|
self.assertRaises(gp.GurobiError, check_dummy_env)
|
||||||
|
|
||||||
|
def test_multiple_gp_env(self):
|
||||||
|
# Using the same env multiple times
|
||||||
|
with gp.Env(params=self.env_options) as env:
|
||||||
|
solver = GUROBI(msg=False, env=env)
|
||||||
|
prob = generate_lp()
|
||||||
|
prob.solve(solver)
|
||||||
|
solver.close()
|
||||||
|
|
||||||
|
solver2 = GUROBI(msg=False, env=env)
|
||||||
|
prob2 = generate_lp()
|
||||||
|
prob2.solve(solver2)
|
||||||
|
solver2.close()
|
||||||
|
|
||||||
|
check_dummy_env()
|
||||||
|
|
||||||
|
@unittest.SkipTest
|
||||||
|
def test_backward_compatibility(self):
|
||||||
|
"""
|
||||||
|
Backward compatibility check as previously the environment was not being
|
||||||
|
freed. On a single-use license this passes (fails to initialise a dummy
|
||||||
|
env).
|
||||||
|
"""
|
||||||
|
solver = GUROBI(msg=False, **self.options)
|
||||||
|
prob = generate_lp()
|
||||||
|
prob.solve(solver)
|
||||||
|
|
||||||
|
self.assertRaises(gp.GurobiError, check_dummy_env)
|
||||||
|
gp.disposeDefaultEnv()
|
||||||
|
solver.close()
|
||||||
|
|
||||||
|
def test_manage_env(self):
|
||||||
|
solver = GUROBI(msg=False, manageEnv=True, **self.options)
|
||||||
|
prob = generate_lp()
|
||||||
|
prob.solve(solver)
|
||||||
|
|
||||||
|
solver.close()
|
||||||
|
check_dummy_env()
|
||||||
|
|
||||||
|
def test_multiple_solves(self):
|
||||||
|
solver = GUROBI(msg=False, manageEnv=True, **self.options)
|
||||||
|
prob = generate_lp()
|
||||||
|
prob.solve(solver)
|
||||||
|
|
||||||
|
solver.close()
|
||||||
|
check_dummy_env()
|
||||||
|
|
||||||
|
solver2 = GUROBI(msg=False, manageEnv=True, **self.options)
|
||||||
|
prob.solve(solver2)
|
||||||
|
|
||||||
|
solver2.close()
|
||||||
|
check_dummy_env()
|
||||||
|
|
||||||
|
@unittest.SkipTest
|
||||||
|
def test_leak(self):
|
||||||
|
"""
|
||||||
|
Check that we cannot initialise environments after a memory leak. On a
|
||||||
|
single-use license this passes (fails to initialise a dummy env with a
|
||||||
|
memory leak).
|
||||||
|
"""
|
||||||
|
solver = GUROBI(msg=False, **self.options)
|
||||||
|
prob = generate_lp()
|
||||||
|
prob.solve(solver)
|
||||||
|
|
||||||
|
tmp = solver.model
|
||||||
|
solver.close()
|
||||||
|
|
||||||
|
solver2 = GUROBI(msg=False, **self.options)
|
||||||
|
|
||||||
|
prob2 = generate_lp()
|
||||||
|
prob2.solve(solver2)
|
||||||
|
self.assertRaises(gp.GurobiError, check_dummy_env)
|
||||||
1673
utils/pulp/tests/test_pulp.py
Normal file
1673
utils/pulp/tests/test_pulp.py
Normal file
File diff suppressed because it is too large
Load Diff
232
utils/pulp/utilities.py
Normal file
232
utils/pulp/utilities.py
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# Utility functions
|
||||||
|
import itertools
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
def resource_clock():
|
||||||
|
import resource
|
||||||
|
|
||||||
|
return resource.getrusage(resource.RUSAGE_CHILDREN).ru_utime
|
||||||
|
|
||||||
|
|
||||||
|
def isNumber(x):
|
||||||
|
"""Returns true if x is an int or a float"""
|
||||||
|
return isinstance(x, (int, float))
|
||||||
|
|
||||||
|
|
||||||
|
def value(x):
|
||||||
|
"""Returns the value of the variable/expression x, or x if it is a number"""
|
||||||
|
if isNumber(x):
|
||||||
|
return x
|
||||||
|
else:
|
||||||
|
return x.value()
|
||||||
|
|
||||||
|
|
||||||
|
def valueOrDefault(x):
|
||||||
|
"""Returns the value of the variable/expression x, or x if it is a number
|
||||||
|
Variable without value (None) are affected a possible value (within their
|
||||||
|
bounds)."""
|
||||||
|
if isNumber(x):
|
||||||
|
return x
|
||||||
|
else:
|
||||||
|
return x.valueOrDefault()
|
||||||
|
|
||||||
|
|
||||||
|
def __combination(orgset, k):
|
||||||
|
"""
|
||||||
|
fall back if probstat is not installed note it is GPL so cannot
|
||||||
|
be included
|
||||||
|
"""
|
||||||
|
if k == 1:
|
||||||
|
for i in orgset:
|
||||||
|
yield (i,)
|
||||||
|
elif k > 1:
|
||||||
|
for i, x in enumerate(orgset):
|
||||||
|
# iterates though to near the end
|
||||||
|
for s in __combination(orgset[i + 1 :], k - 1):
|
||||||
|
yield (x,) + s
|
||||||
|
|
||||||
|
|
||||||
|
try: # python >= 3.4
|
||||||
|
from itertools import combinations as combination
|
||||||
|
except ImportError:
|
||||||
|
try: # python 2.7
|
||||||
|
from itertools import combination
|
||||||
|
except ImportError: # pulp's
|
||||||
|
combination = __combination
|
||||||
|
|
||||||
|
|
||||||
|
def __permutation(orgset, k):
|
||||||
|
"""
|
||||||
|
fall back if probstat is not installed note it is GPL so cannot
|
||||||
|
be included
|
||||||
|
"""
|
||||||
|
if k == 1:
|
||||||
|
for i in orgset:
|
||||||
|
yield (i,)
|
||||||
|
elif k > 1:
|
||||||
|
for i, x in enumerate(orgset):
|
||||||
|
# iterates though to near the end
|
||||||
|
for s in __permutation(orgset[:i] + orgset[i + 1 :], k - 1):
|
||||||
|
yield (x,) + s
|
||||||
|
|
||||||
|
|
||||||
|
try: # python >= 3.4
|
||||||
|
from itertools import permutations as permutation
|
||||||
|
except ImportError:
|
||||||
|
try: # python 2.7
|
||||||
|
from itertools import permutation
|
||||||
|
except ImportError: # pulp's
|
||||||
|
permutation = __permutation
|
||||||
|
|
||||||
|
|
||||||
|
def allpermutations(orgset, k):
|
||||||
|
"""
|
||||||
|
returns all permutations of orgset with up to k items
|
||||||
|
|
||||||
|
:param orgset: the list to be iterated
|
||||||
|
:param k: the maxcardinality of the subsets
|
||||||
|
|
||||||
|
:return: an iterator of the subsets
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
>>> c = allpermutations([1,2,3,4],2)
|
||||||
|
>>> for s in c:
|
||||||
|
... print(s)
|
||||||
|
(1,)
|
||||||
|
(2,)
|
||||||
|
(3,)
|
||||||
|
(4,)
|
||||||
|
(1, 2)
|
||||||
|
(1, 3)
|
||||||
|
(1, 4)
|
||||||
|
(2, 1)
|
||||||
|
(2, 3)
|
||||||
|
(2, 4)
|
||||||
|
(3, 1)
|
||||||
|
(3, 2)
|
||||||
|
(3, 4)
|
||||||
|
(4, 1)
|
||||||
|
(4, 2)
|
||||||
|
(4, 3)
|
||||||
|
"""
|
||||||
|
return itertools.chain(*[permutation(orgset, i) for i in range(1, k + 1)])
|
||||||
|
|
||||||
|
|
||||||
|
def allcombinations(orgset, k):
|
||||||
|
"""
|
||||||
|
returns all combinations of orgset with up to k items
|
||||||
|
|
||||||
|
:param orgset: the list to be iterated
|
||||||
|
:param k: the maxcardinality of the subsets
|
||||||
|
|
||||||
|
:return: an iterator of the subsets
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
>>> c = allcombinations([1,2,3,4],2)
|
||||||
|
>>> for s in c:
|
||||||
|
... print(s)
|
||||||
|
(1,)
|
||||||
|
(2,)
|
||||||
|
(3,)
|
||||||
|
(4,)
|
||||||
|
(1, 2)
|
||||||
|
(1, 3)
|
||||||
|
(1, 4)
|
||||||
|
(2, 3)
|
||||||
|
(2, 4)
|
||||||
|
(3, 4)
|
||||||
|
"""
|
||||||
|
return itertools.chain(*[combination(orgset, i) for i in range(1, k + 1)])
|
||||||
|
|
||||||
|
|
||||||
|
def makeDict(headers, array, default=None):
|
||||||
|
"""
|
||||||
|
makes a list into a dictionary with the headings given in headings
|
||||||
|
headers is a list of header lists
|
||||||
|
array is a list with the data
|
||||||
|
"""
|
||||||
|
result, defdict = __makeDict(headers, array, default)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def __makeDict(headers, array, default=None):
|
||||||
|
# this is a recursive function so end the recursion as follows
|
||||||
|
result = {}
|
||||||
|
returndefaultvalue = None
|
||||||
|
if len(headers) == 1:
|
||||||
|
result.update(dict(zip(headers[0], array)))
|
||||||
|
defaultvalue = default
|
||||||
|
else:
|
||||||
|
for i, h in enumerate(headers[0]):
|
||||||
|
result[h], defaultvalue = __makeDict(headers[1:], array[i], default)
|
||||||
|
if default is not None:
|
||||||
|
f = lambda: defaultvalue
|
||||||
|
defresult = collections.defaultdict(f)
|
||||||
|
defresult.update(result)
|
||||||
|
result = defresult
|
||||||
|
returndefaultvalue = collections.defaultdict(f)
|
||||||
|
return result, returndefaultvalue
|
||||||
|
|
||||||
|
|
||||||
|
def splitDict(data):
|
||||||
|
"""
|
||||||
|
Split a dictionary with lists as the data, into smaller dictionaries
|
||||||
|
|
||||||
|
:param data: A dictionary with lists as the values
|
||||||
|
|
||||||
|
:return: A tuple of dictionaries each containing the data separately,
|
||||||
|
with the same dictionary keys
|
||||||
|
"""
|
||||||
|
# find the maximum number of items in the dictionary
|
||||||
|
maxitems = max([len(values) for values in data.values()])
|
||||||
|
output = [dict() for _ in range(maxitems)]
|
||||||
|
for key, values in data.items():
|
||||||
|
for i, val in enumerate(values):
|
||||||
|
output[i][key] = val
|
||||||
|
|
||||||
|
return tuple(output)
|
||||||
|
|
||||||
|
|
||||||
|
def read_table(data, coerce_type, transpose=False):
|
||||||
|
"""
|
||||||
|
Reads in data from a simple table and forces it to be a particular type
|
||||||
|
|
||||||
|
This is a helper function that allows data to be easily constained in a
|
||||||
|
simple script
|
||||||
|
::return: a dictionary of with the keys being a tuple of the strings
|
||||||
|
in the first row and colum of the table
|
||||||
|
::param data: the multiline string containing the table data
|
||||||
|
::param coerce_type: the type that the table data is converted to
|
||||||
|
::param transpose: reverses the data if needed
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> table_data = '''
|
||||||
|
... L1 L2 L3 L4 L5 L6
|
||||||
|
... C1 6736 42658 70414 45170 184679 111569
|
||||||
|
... C2 217266 227190 249640 203029 153531 117487
|
||||||
|
... C3 35936 28768 126316 2498 130317 74034
|
||||||
|
... C4 73446 52077 108368 75011 49827 62850
|
||||||
|
... C5 174664 177461 151589 153300 59916 135162
|
||||||
|
... C6 186302 189099 147026 164938 149836 286307
|
||||||
|
... '''
|
||||||
|
>>> table = read_table(table_data, int)
|
||||||
|
>>> table[("C1","L1")]
|
||||||
|
6736
|
||||||
|
>>> table[("C6","L5")]
|
||||||
|
149836
|
||||||
|
"""
|
||||||
|
lines = data.splitlines()
|
||||||
|
headings = lines[1].split()
|
||||||
|
result = {}
|
||||||
|
for row in lines[2:]:
|
||||||
|
items = row.split()
|
||||||
|
for i, item in enumerate(items[1:]):
|
||||||
|
if transpose:
|
||||||
|
key = (headings[i], items[0])
|
||||||
|
else:
|
||||||
|
key = (items[0], headings[i])
|
||||||
|
result[key] = coerce_type(item)
|
||||||
|
return result
|
||||||
140
utils/trans.py
Normal file
140
utils/trans.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
def t(input_string):
|
||||||
|
user_lang = bpy.context.preferences.view.language
|
||||||
|
trans_result = trans_dict.get(input_string)
|
||||||
|
if trans_result is None:
|
||||||
|
return input_string
|
||||||
|
|
||||||
|
if "zh_" in user_lang:
|
||||||
|
return trans_result[1]
|
||||||
|
if "ja_" in user_lang:
|
||||||
|
return trans_result[2]
|
||||||
|
else:
|
||||||
|
return trans_result[0]
|
||||||
|
|
||||||
|
|
||||||
|
trans_dict = {
|
||||||
|
"start": ["Start", "开始", "スタート"],
|
||||||
|
"stop": ["Stop", "停止", "停止する"],
|
||||||
|
"quit": ["Quit", "退出", "終了"],
|
||||||
|
"apply_mesh": ["Apply Mesh", "应用网格", "メッシュを適用"],
|
||||||
|
"enable": ["Enable", "启用", "有効化"],
|
||||||
|
"disable": ["Disable", "禁用", "無効化"],
|
||||||
|
"key": ["Key", "键", "キー"],
|
||||||
|
"enter": ["Enter", "回车", "エンター"],
|
||||||
|
"current_status": ["Current Status", "当前状态", "現在の状態"],
|
||||||
|
"drawing": ["Drawing", "正在绘制", "描画中"],
|
||||||
|
"patching": ["Patching", "选择补丁", "パッチ適用"],
|
||||||
|
"click_drag_to_draw_line": [
|
||||||
|
"Click drag to draw line",
|
||||||
|
"点击并拖动鼠标绘制边缘",
|
||||||
|
"クリックしてドラッグし線を描く",
|
||||||
|
],
|
||||||
|
"snap_surface": ["Snap surface", "吸附表面", "サーフェスにスナップ"],
|
||||||
|
"hold_ctrl": ["Hold Ctrl", "按住Ctrl", "Ctrlを押し続ける"],
|
||||||
|
"hold_shift": ["hold Shift", "按住Shift", "Shiftを押し続ける"],
|
||||||
|
"scroll": ["Scroll", "滚轮", "スクロール"],
|
||||||
|
"change_segment": ["Change Segment", "改变分段数", "セグメントを変更"],
|
||||||
|
"snap_vert": ["Snap Vertex", "吸附顶点", "頂点にスナップ"],
|
||||||
|
"snap_exist_path": ["Snap Exist Path", "吸附已有路径", "既存のパスにスナップ"],
|
||||||
|
"solver_solution": ["Solver Solution", "求解器结果", "ソルバーの解"],
|
||||||
|
"pattern": ["Pattern", "形状", "パターン"],
|
||||||
|
"solver_constraint": ["Solver Constraint", "求解器约束", "ソルバーの制約"],
|
||||||
|
"change_padding": ["Change Padding", "改变填充数量", "パディングを変更"],
|
||||||
|
"rotate_pattern": ["Rotate Pattern", "旋转形状", "パターンを回転"],
|
||||||
|
"clear_constraint": ["Clear Constraint", "清除约束", "制約をクリア"],
|
||||||
|
"switch_pattern": ["Switch Pattern", "切换形状", "パターンを切り替え"],
|
||||||
|
"no_solution": ["No Solution", "无解", "解なし"],
|
||||||
|
"not_even_num_edges": ["Not Even Number Edges", "非偶数边", "偶数でないエッジ数"],
|
||||||
|
"no_match_pattern": [
|
||||||
|
"No Match Pattern",
|
||||||
|
"未找到匹配的形状",
|
||||||
|
"一致するパターンなし",
|
||||||
|
],
|
||||||
|
"rotation": ["Rotation", "旋转", "回転"],
|
||||||
|
"paddings": ["Paddings", "填充", "パディング"],
|
||||||
|
"var_constraint": ["Variable Constraint", "变量约束", "変数制約"],
|
||||||
|
"current_para": ["Current Parameters", "当前参数", "現在のパラメータ"],
|
||||||
|
"snap_obj_setting_desc": [
|
||||||
|
"snap target obj, if empty snap to whole collection",
|
||||||
|
"吸附目标物体,如果留空则吸附整个集合",
|
||||||
|
"ターゲットオブジェクトをスナップします。空の場合はコレクション全体にスナップします",
|
||||||
|
],
|
||||||
|
"flip_normal": ["Flip Normals", "反转法线", "法線を反転する"],
|
||||||
|
"smooth_mesh": ["Smooth Mesh", "平滑网格", "スムーズ メッシュ"],
|
||||||
|
"guide_mode": ["Guide Mode", "引导模式", "ガイドモード"],
|
||||||
|
"only_clear_rotation": ["Only clear rotation", "只清除旋转", "回転のみクリア"],
|
||||||
|
"only_clear_var": ["Only clear variable", "只清除变量", "変数のみクリア"],
|
||||||
|
"snap_opposite": [
|
||||||
|
"Snap to normals opposite surface",
|
||||||
|
"吸附法线方向相反的表面",
|
||||||
|
"サーフェスの反対側の法線にスナップします",
|
||||||
|
],
|
||||||
|
"skip_no_solution_pattern": [
|
||||||
|
"Skip no solution pattern",
|
||||||
|
"跳过无解形状",
|
||||||
|
"スキップなしの解決パターン",
|
||||||
|
],
|
||||||
|
"padding": ["Padding", "填充", "パディング"],
|
||||||
|
"snap_selected_to_surface": [
|
||||||
|
"Snap Selected Vertices to Surface",
|
||||||
|
"吸附选中顶点至表面",
|
||||||
|
"選択頂点スナップ",
|
||||||
|
],
|
||||||
|
"right_click": ["Right Click", "右键", "右クリック"],
|
||||||
|
"cancel_this_draw": ["Cancel this Darw", "取消绘制", "この図面をキャンセル"],
|
||||||
|
"left_click_release": ["Left Click Release", "释放左键", "左クリックリリース"],
|
||||||
|
"apply_this_draw": ["Apply this Draw", "应用绘制", "この図面を適用"],
|
||||||
|
"delete_highlight_edge": [
|
||||||
|
"Delete highlight Edge",
|
||||||
|
"删除高亮边",
|
||||||
|
"ハイライトエッジを削除",
|
||||||
|
],
|
||||||
|
"faces_normal_toward_viewport": [
|
||||||
|
"Change Faces Normal Toward Viewport",
|
||||||
|
"修改面法向朝向视图",
|
||||||
|
"ビューポートに向かって面の法線を変更",
|
||||||
|
],
|
||||||
|
"draw_on_axis_of_symmetry": [
|
||||||
|
"Draw on Axis of Symmetry",
|
||||||
|
"沿对称轴绘制",
|
||||||
|
"対称軸に沿って描く",
|
||||||
|
],
|
||||||
|
"you_cant_undo_here": [
|
||||||
|
"Undo is not allowed here, quit and retry",
|
||||||
|
"此處無法撤回,退出再試",
|
||||||
|
"ここでは元に戻すことはできません。終了して再試行してください",
|
||||||
|
],
|
||||||
|
"faces": [
|
||||||
|
"Faces",
|
||||||
|
"面",
|
||||||
|
"ここでは元に戻すことはできません。終了して再試行してください",
|
||||||
|
],
|
||||||
|
"verts": [
|
||||||
|
"verts",
|
||||||
|
"頂點",
|
||||||
|
"頂点",
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
"edges",
|
||||||
|
"边",
|
||||||
|
"エッジ",
|
||||||
|
],
|
||||||
|
"created": [
|
||||||
|
"created",
|
||||||
|
"創建",
|
||||||
|
"作成された",
|
||||||
|
],
|
||||||
|
"font_size": [
|
||||||
|
"Font Size",
|
||||||
|
"字體大小",
|
||||||
|
"フォントサイズ",
|
||||||
|
],
|
||||||
|
"hotkey_display_position": [
|
||||||
|
"Hotkey Display Position",
|
||||||
|
"熱鍵的展示位置",
|
||||||
|
"ホットキーの表示位置",
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user