Pyomo基础学习笔记:建模组成要素的编写方法

1、Pyomo 简介

pyomo文档【数学建模】优化模型建模语言 Pyomo 入门教程 - 知乎 (zhihu.com)

Pyomo 是基于 Python 的开源软件包,主要功能是建立数学规划模型,包括线性规划,二次规划,整数规划,随机规划,含有微分代数方程的优化问题。

这里需要注意两个问题:

  1. Pyomo 只负责建模,求解还需要调用求解器来求解
  2. Pyomo 是开源免费的,但是只能适用于Python环境

你可能会产生一个疑问,在Gurobi/Cplex之类的求解器中也可以直接写模型啊,为何还需要专门单独搞一个Pyomo来写模型呢?主要是因为如下原因:

  1. Pyomo 的建模功能比一般求解器中自带的建模功能强大,Pyomo 支持多种问题类型,包括:
  • 线性规划
  • 二次规划
  • 非线性规划
  • 整数线性规划
  • 混合整数非线性规划
  • 混合整数二次规划
  • 微分代数方程
  • 整数随机规划
  • 广义析取规划
  • 带平衡约束的数学规划

这些都是 Gurobi/Cplex 所不具备的。

  1. 采用 Pyomo 建模可以实现一次建模,通用求解。在 Pyomo 中建模后 可以非常方便地调用其它多种求解器来求解,例如 Gurobi,Cplex,SCIP,CBC等。

所以是否选用 Pyomo 大家可以根据以上两条对照自己的需求来决定。最后 Pyomo 的安装也非常方便,在Anaconda环境中直接采用pip即可安装

pip install pyomo

2、Pyomo 建模方法

可通过两种方式使用 Pyomo 创建优化模型:抽象(Abstract)和具体(Concrete)。这两种方式的关键区别在于是否将模型与数据分开引入:

  • 在抽象模型中,模型与数据是分开的,支持在模型建立后再引入数据赋值。
  • 在具体模型中,数据在模型本身中被定义。例如,在使用具体模型的 Pyomo 脚本中,所有内容都是在单个脚本文件中定义的。对于简单的问题,这可能是一种很高效的方法。但另一方面,对于规模比较大的问题(更大的数据集),这使得将数据和处理该数据所需的代码嵌入同一个Python脚本中变得不易实现且难以维护。

总体上讲,Pyomo 模型由变量(Variables)、参数(Parameters)、约束(Constraints)和目标(Objectives)组成。

(1)变量(Variables):

变量代表模型中待优化的决策变量,它是在优化求解中待计算的对象。求解优化模型后得到的变量的值通常称为解,它是优化求解过程的输出。

(2)参数(Parameters):

参数通常指优化问题中提供的数据(系数),以便为决策变量找到最佳(或良好)的值分配。参数值也可以通过定义验证函数进行检查。

(3)约束(Constraints):

约束是定义等式、不等式或其他数学表达式来指定变量的限制的一种方式。大多数约束是使用表达式规则创建的,这些规则往往是一个 Python 函数。

(4)优化目标(Objective):

优化目标是指被建模系统的目标的函数,一般有最大化或最小化两种定义。

注意:建模过程中表达式不能用怒 numpy 等科学计算包

3、基本案例

img

from pyomo.environ import *
model = ConcreteModel()
model.x = Var(RangeSet(1, 4), bounds=(1, 5))
model.cons1 = Constraint(rule=lambda model: 40==model.x[1]**2+model.x[2]**2+model.x[3]**2+model.x[4]**2)
model.cons2 = Constraint(rule=lambda model: 25<=model.x[1]*model.x[2]*model.x[3]*model.x[4])
model.obj = Objective(expr = model.x[1]*model.x[4]*(model.x[1] + model.x[2] + model.x[3]) + model.x[3], sense=minimize)
SolverFactory('ipopt').solve(model)
solutions = [model.x[i]() for i in range(1, 5)]
print("solutins is :"+str(solutions))

在这里我们只使用 model = ConcreteModel() 来定义模型,在 Pyomo 中还有另外一类抽象的 AbstractModel() 模型,相对来说该模型的使用更加高级。

4、建模六大要素

从运筹优化的理论来讲,一般优化模型的构成是三大要素 决策变量,约束,目标函数。但是在用代码实现优化模型的时候就不得不考虑另外三个不可或缺的要素:索引集合、参数和表达式。因此一共合成六大要素,

  • 索引集合
  • 参数
  • 决策变量
  • 目标函数
  • 约束条件
  • 表达式

在 Pyomo 中我们只要定义好了以上六大要素,也就基本完成了对优化问题的建模。下面以优化问题中常见的指派问题为例来说明这六大要素:

img

4.1 索引集合

Pyomo 中常用的两类集合

4.1.1 Set(initialize, filter, validate)

  • initialize:表示初始化参数,可以采用列表,元组和集合来给一个Set初始化;
  • filter:表示一个过滤掉集合的条件,一般是一个函数表达式;
  • validate:表示对集合进行验证条件,一般是一个函数表达式;

如下所示是采用多种方式来初始化集合的方法:

from pyomo.environ import *
model = ConcreteModel()
model.I = Set(initialize = [i for i in range(6)]) # 采用列表初始化集合
model.J = Set(initialize=('red', 'green', 'blue')) # 采用元组初始化集合 并且用字符串作为元素引。  
model.N = Set([i for i in range(3)]) # 等价于用 initialize
model.M = Set([(i,j) for i in range(2) for j in range(2)])
def even_init(model):
    return (i for i in model.I if i % 2 == 0)
model.K = Set(initialize=even_init) # 可以通过一个函数even_init 来定义initialize,本例中就是选取偶数的索引。

采用filter参数可以对集合的元素进行筛选,如下例子中就是通过定义 filter_rule 来实现让model.P集合中只含有集合model.I 没有的元素

def filter_rule(model, x):
    return x not in model.I

model.P = Set(initialize = [i for i in range(6)], filter = filter_rule)

4.1.2 RangeSet(start, end, slice)

如果只需要纯数字作为索引的元素采用RangeSet会更加直接和方便一些,当然前面介绍的 Set 的功能可以覆盖掉 RangeSet。RangeSet 的用法如下所示:

model.D = RangeSet(1,10,2) # 表示 [1,3,5,7,9]
# model.D = RangeSet(10) # 表示
# model.D = RangeSet(1,10,4) 表示 [1,5,9]  
# model.D = RangeSet(1,10) 表示 [1,2,3,4,5,6,7,8,9,10]

4.1.3 集合的运算

model.I = [i for i in range(5)] # 给集合赋值
print([v for v in model.I]) # 遍历集合中所有元素
print(len(model.J)) # 3 表示集合元素个数
print(model.I.data()) # (0, 1, 2, 3, 4) 表示 集合元素
print(model.J.data()) # ('red', 'green', 'blue') 表示 集合元素
print(1 in model.I) # True 判断元素是否在集合内
print(12 in model.I) # False 判断元素是否在集合内
print([1,2] == model.I) # False 判断集合是否相等
print([i for i in range(5)] == model.I) # True 判断集合是否相等

Pyomo 提供了几种集合之间的运算,这些运算与Python自带的集合类型十分相似。我这里列举在运筹优化中最常见的四种运算:并交差,笛卡尔积。

model.I = [i for i in range(6)]
model.J = [i for i in range(6) if i % 2 == 0]
model.K = [1,2]
model.E = model.I & model.J # 交集 (0, 2, 4)
model.F = model.I - model.J # 差集 (1, 3, 5)
model.G = model.I | model.J # 并集 (0, 1, 2, 3, 4, 5)
model.H = model.J * model.K # 笛卡尔积 ((0, 1), (0, 2), (2, 1), (2, 2), (4, 1), (4, 2))

4.2 参数

Param(initialize, default, validate)

  • initialize:表示初始化参数,可以采用列表,元组和字典来做参数初始化;
  • default:表示初始化参数的默认值,initialize没有覆盖的索引default才会起作用;
  • validate: 表示对集合进行验证条件,一般是一个函数表达式

4.2.1 简单赋值法初始化参数

from pyomo.environ import *
model = ConcreteModel()
model.I = Set(initialize = [i for i in range(4)])
model.J = Set(initialize = [i for i in range(4)])
model.c = Param(model.I, model.J, default = 1) # 参数c是一个4*4的矩阵 并且初始值全部为1

4.2.2 字典初始化参数

c_data = {(0,0): 1, (1,2): 33, (3,3): 20} # 采用字典初始化 表示 c[0,0]=1,c[1,2]=33,c[3,3]=20
model.c1 = Param(model.I, model.J, initialize=c_data, default = 0) # default = 0 表示其余元素都为 0

4.2.3 函数初始化参数

采用函数初始化参数相比前两种方法可以非常的灵活。

def c_init(model, i, j):
    if i == j:
        return i*i
    else:
        return 0.0

model.c2 = Param(model.I, model.J, initialize=c_init)

4.2.4 validate 验证参数取值正确性

借用 c_validate 函数可以方便验证参数选取是否满足一些硬性条件,如果不满足条件则会直接报错提示。这是一个非常好的预防bug提升代码鲁棒性的功能,同时validate并不影响我们对数学模型的实现。

c_data = {(1,1): 4, (1,2): 33, (3,3): 20}
def c_validate(model, v, i, j): # 保证所有c_{ij}的值都要大于3
    return v > 3.

model.c3 = Param(model.I, model.J, initialize=c_data, validate=c_validate)

在上面的代码中,如果\(c_{ij}\)的值小于3,validate函数会报错提示我们。

4.2.5 参数常用操作

print(len(model.c)) # 9 参数元素个数
print(1 in model.c) # False
print((0,0) in model.c) # True
print([v for v in model.c]) # [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2

4.3 决策变量

Var(index_set, within, bounds, initialize)

  • index_set: 表示决策变量的索引;
  • bounds:表示决策变量上下界;
  • within :表示决策变量类型;
  • initialize :表示决策变量的初值;

4.3.1 一维决策变量定义

from pyomo.environ import *
model = ConcreteModel()
# Reals 表示实数变量,上下界为0-6,初始值设为1.5
model.x = Var(within=Reals, bounds=(0,6), initialize=1.5) 

4.3.2 多维决策变量定义

def fb(model, i, j):
    return (0, 1) # 下界为0,上界为1

model.I = Set(initialize = [i for i in range(4)])
model.J = Set(initialize = [i for i in range(4)])
model.y= Var(model.I, model.J, within=PositiveIntegers, bounds=fb) 
# 表示决策变量 y 维数为4*4,正整数变量,上下通过函数fb()定义
def fb1(model, i, j):
    return (10, None) # None表示没有上界

model.z= Var(model.I, model.J, within=PositiveIntegers, bounds=fb1)

within 可以选择的类型有:

Reals: 实数

PositiveReals:正实数

NonPositiveReals: 非正实数

NegativeReals:负实数

NonNegativeReals:非负实数

PercentFraction: 0到1之间实数

UnitInterval: 同PercentFraction

Integers:整数

PositiveIntegers:正整数

NonPositiveIntegers:非正整数

NegativeIntegers:负整数

NonNegativeIntegers:非负整数

Boolean: 布尔变量

Binary: 0,1变量

4.3.3 给决策变量赋初值

给决策变量赋初值是一个很重要的功能,因为很多算法都涉及到 warm start 的问题,给决策变量赋一个好的初值会加速整个求解过程。在 pyomo 中就是通过initialize参数来实现给决策变量赋初值的功能,相关代码如下所示:

model.x_init1 = Var(model.I, model.J, initialize = {(0,0):1.2, (1,2):30, (2,2): 21}) # 字典赋初值给决策变量
model.x_init2 = Var(model.I, model.J, initialize = 2.0) # 初值全为 2.0
def g_init(model, i, j):
    return i+j

model.x_init3 = Var(model.I, model.J, initialize = g_init) # 通过函数g_init 给定初值
print(value(model.x_init1[0,0])) # 1.2
print(value(model.x_init1[1,2])) # 30
print(value(model.x_init2[0,0])) # 2.0
print(value(model.x_init2[3,3])) # 2.0
print(value(model.x_init3[3,3])) # 6.0

4.3.4 决策变量的固定

在优化算法中我们为了降低问题的规模,经常会采用固定住一部分决策变量,只优化另外一部分决策变量的策略。这种直观的方法也被称为 fixed and relaxed 。在 Pyomo中也给我们提供了这样固定住决策变量的方法,一旦变量被固定住之后 在优化求解过程中该变量均被视为常数 不会被改变,代码如下所示:

model.x_init1.fix(1) # 表示把 x_init1 决策变量固定为1
print(model.x_init1[0,0].fixed) # True 表示该变量是固定变量
print(value(model.x_init1[0,0])) # 1

4.4 目标函数

Objective(index_set, obj_rule, sense)

  • index_set:表示目标函数索引集合;
  • obj_rule:表示目标函数定义规则;
  • sense:maximize代表是极大化问题,minimize代表极小化问题;

4.4.1 简单表达式定义目标函数

model = ConcreteModel()
model.I = Set(initialize = [i for i in range(2)])
model.J = Set(initialize = [i for i in range(3)])
model.x = Var(model.I, model.J, domain = Reals)

def obj_rule(model):
    return 2*model.x[1,1] + 3*model.x[1,2]

model.obj1 = Objective(rule = obj_rule, sense=minimize)

4.4.2 借助summation函数定义目标函数

summation 函数是 pyomo 中可以实现两个向量快速求内积的操作,如下代码是实现目标函数img

model.p = Param(model.I, model.J, default = 2)
def obj_rule(model):
    return summation(model.p, model.x)
model.obj2 = Objective(rule = obj_rule, sense=maximize)

4.4.3 借助各种复杂表达式定义目标函数

Pyomo 中可以建模各种各样的目标函数,如下的目标函数中包括了三角函数,多项式函数,指数函数和对数函数。

def obj_rule(model):
    expr = 0
    for i in model.I:
        for j in model.J:
            expr += sin(model.x[i,j]) + model.x[i,j]**2 + 2**(model.x[i,j]) + log(model.x[i,j])
    return expr

model.obj3 = Objective(rule = obj_rule, sense=minimize)

4.4.4 带有索引的目标函数定义

如下所示是实现img为目标函数。

def e_rule(model, i, j):
    return model.x[i,j]**2
model.e1 = Objective(model.I, model.J, rule=e_rule)

4.4.5 借助 Skip 实现目标函数灵活定义

某些索引需要跳过的,可以采用 Objective.Skip,如下所示就是跳过 i 和 j相等的情况,就可以在对变矩阵选择性的执行函数。

def e_rule(model, i, j):
    if i == j:
        return Objective.Skip
    return model.x[i,j]**2

model.e2 = Objective(model.I, model.J, rule=e_rule)

3.4.6 访问目标函数的性质

print(model.obj1.expr) # 2*x[1,1] + 3*x[2,2] 返回目标函数的表达式
print(model.obj1.sense) # -1 表示极大化 1 表示极小化
print(value(model.obj1)) # 返回目标函数值

4.5 约束

Objective(index_set, expr, rule)

  • index_set:表示约束索引集合;
  • rule:表示目约束定义规则;
  • expr:表示约束表达式;

4.5.1 借助表达式定义单条约束

借助表达式可以非常方便的定义简单的单条约束,如下所示:

model = ConcreteModel()
model.I = Set(initialize = [i for i in range(3)])
model.J = Set(initialize = [i for i in range(3)])
model.x = Var(model.I, model.J, initialize = 1.0, within = Reals)
model.c1 = Constraint(expr = 2.0 <= model.x[0,0] + 2*model.x[0,2]) # 不等式约束 2 <= x[0,0] + 2*x[0,2]
model.c2 = Constraint(expr = 3*model.x[0,0] + 5*model.x[1,1] == 10.0) # 等式约束 3*x[0,0] + 5*x[1,1] = 10.0

print(value(model.c1.body)) # 3 返回c1表达式的值
print(model.c1.lslack()) # 1.0 返回c1约束和下限的差(lower bound slack variable)
print(model.c1.uslack()) # +inf 返回c1约束和上限的差(upper bound slack variable)
print(model.c2.lslack()) # -2.0 返回c2约束和下限的差(lower bound slack variable)
print(model.c2.uslack()) # 2.0 返回c1约束和下限的差(upper bound slack variable)

注意:约束与下限差的计算是约束减下限,相反约束与上限的差是上限减约束。

4.5.2 借用函数定义单个约束

对于表达式比较复杂的约束,可以采用一个函数来定义这个约束,如下所示:

model.p = Param(model.I, model.J, default = 2)
def c3_rule(model):
    return summation(model.p, model.x) + model.x[0,0]**2 <= 2

model.c3 = Constraint(rule = c3_rule)

4.5.3 借用函数定义多条约束

一次性定义多条约束,例如\sum_{j\in J}{p_{ij}x_{ij}}\le 2,\ \ \forall i\in I 。如果需要跳过某些不需要定义约束的索引下标,同理也可以采用 Constraint.Skip 就可以跳过相对应的下标。如下所示就是跳过了\(i=0\)的情况:

def c4_rule(model, i):
    if i == 0:
        return Constraint.Skip
    return sum([model.p[i,j]*model.x[i,j] for j in model.J]) <= 2

model.c4= Constraint(model.I, rule = c4_rule)

c4_rule 函数会对 moel.I 的 i 循环执行,这样就构建了 I 个约束。

4.5.4 激活约束和停用约束

很多时候我们需要暂时让一些约束失效,那么 pyomo 也有这样的命令,如下所示:

model.c2.deactivate() # c2约束失效
model.c2.activate() # 重新激活c2约束

调用 deactivate() 后可以让约束c2不起作用了,如果想再添加回c2约束,再调用activate()即可。

4.6 表达式

Expression(expr, rule)

  • expr:表达式;
  • rule:定义表达式的函数;

4.6.1 定义单个表达式

定义单个表达式,如下所示是定义表达式x_0 + 3x_1,然后使用表达式的 set_value() 可以对表达式再赋值,同样借助 value() 函数可以得到表达式的值(在使用value函数返回表达式值之前确保所有表达式中的变量已经有值),代码如下所示。

from pyomo.environ import *
model = ConcreteModel()
model.I = Set(initialize = [i for i in range(3)])
model.x = Var(model.I, within = Reals, initialize = 1.0)
model.e = Expression(expr = model.x[0] + 3*model.x[1]) # 建立表达式e=x[0]+3x[1]
print(value(model.e)) # 4.0
model.e.set_value(model.x[1]) # 重新给表达式赋值 e = x[1]
print(value(model.e)) # 1.0

4.6.2 定义多个表达式

定义多个表达式,如下所示是定义表达式 i×x_i,i∈I

def expr_rule(model,i):
    if i == 1:
        return Expression.Skip
    return model.x[i]*i

model.e1 = Expression(model.I, rule=expr_rule)

定义多个表达式,如下所示是定义表达式x_i×x_i+e^x),i∈I

def gen_expr_rule(model,i):
    return model.x[i]* model.x[i] + exp(model.x[i])

model.e2 = Expression(model.I, rule=gen_expr_rule)

4.6.3 采用单表达式辅助定义约束和目标函数

事实上定义表达式的一个很重要的目的是帮助我们定义约束和目标函数,例如下面的例子就是借助刚才定义的表达式来定义目标和约束。

model.obj = Objective(expr=0.1*model.e + model.x[0])
model.constr = Constraint(expr=model.e <= 1.0)

4.6.4 采用多表达式辅助定义约束和目标函数

同上一小节,只不过换成更为复杂的多个约束。

def constrs1_rule(model, i):
    return model.e2[i] + model.x[i] <= 2
model.constrs1 = Constraint(model.I, rule = constrs1_rule) # model.I约束索引集合

5、求解

前面讲过 Pyomo 只能完成建模的任务,求解的还是需要将 Pyomo 的模型导入到专业的优化求解器来实现。如下代码就是调用 Gurobi 求解其来实现求解:

5.1 调用求解器

img

model = ConcreteModel()
model.I = Set(initialize = [i for i in range(4)])
model.J = Set(initialize = [i for i in range(4)])
model.x = Var(model.I, model.J, within = Reals, bounds = (0,1))
model.c = Param(model.I, model.J, initialize = {(1,1):2, (2,2):3}, default = 1)
def obj_rule(model):
    return summation(model.c, model.x) # 定义目标函数
def constrs_rule1(model, i):
    return sum([model.x[i,j] for j in model.J]) == 1 # 定义约束5.3
def constrs_rule2(model, j):
    return sum([model.x[i,j] for i in model.I]) == 1 # 定义约束5.2

model.obj = Objective(rule=obj_rule, sense = minimize) # 设置优化目标,最小化目标函数
model.constrs1 = Constraint(model.I, rule = constrs_rule1)
model.constrs2 = Constraint(model.J, rule = constrs_rule2)
model.dual = Suffix(direction=Suffix.IMPORT) # 对偶变量定义
opt = SolverFactory('gurobi') # 指定 gurobi 作为求解器
# opt = SolverFactory('cplex') # 指定 cplex 作为求解器
# opt = SolverFactory('scip') # 指定 scip 作为求解器
solution = opt.solve(model) # 调用求解器求解

注意在调用求解器之前要确保求解器已经成功安装。Pyomo 目前支持的求解器种类也有很多种主要包括:Gurobi,Cplex,SCIP, CBC, Ipopt, BARON, Moske, Xpress, GLPK等等。调用方式和上面例子中的Gurobi 调用方式类似,只需要替换相关求解器的名字即可。

5.2 优化结果导出

在成功求解之后我们最常用的是将一些结果输出出来。一般最常用的是需要输出最优的目标函数值和最优的决策变量值,如下代码所示:

solution.write() # 写入求解结果
x_opt = np.array([value(model.x[i,j]) for i in model.I for j in model.J]).reshape((len(model.I), len(model.J))) # 提取最优解
obj_values = value(model.obj) # 提取目标函数
print("optimum point: \n {} ".format(x_opt))
print("optimal objective: {}".format(obj_values))

5.3 对偶变量值导出

记得如果需要使用对偶变量的话,需要在求解之前定义好对偶变量,例如 model.dual = Suffix(direction=Suffix.IMPORT) ,这样才能在求解完备之后采用下面的例子来遍历所需要的对偶变量的值:

for c in model.constrs1:
    print(model.dual[model.constrs1[c]]) #遍历约束constrs1的对偶变量

5.4 求解状态查看

在求解完成之后,我们也可能需要查看求解的状态信息,可以通过如下变量体现:

print(solution.solver.termination_condition) # 终止条件 一般包括三种 optimal, feasible, infeasible
print(solution.solver.termination_message) # 求解得到的信息 一般为一行字符串信息
print(solution.solver.status) # 表示求解的状态 包括 ok, warning, error, aborted, or unknown

6、微电网日前优化调度实践

7、其他

pyo.Piecewise的离散约束方式

import numpy as np
import pyomo.environ as pyo
import matplotlib.pyplot as plt

#Create macc
# logistic sigmoid function
def logistic(x, L=1, x_0=0, k=1):
    return L / (1 + np.exp(-k * (x - x_0)))

c = np.linspace(0,400,400)
macc = 2000*logistic(c,L=0.5,x_0 = 60,k=0.02)
macc = macc -macc[0]

plt.rcParams['font.family'] = ['SimHei']  #
plt.figure(figsize=(10, 5))
plt.plot(c,macc,color='b')
plt.legend()
plt.show()



s0 = 800
b0 = 1000
tnac0 = 100

cp0 = 10
ab0 = 100

model = pyo.ConcreteModel()

#Declare variable
model.x = pyo.Var(domain=pyo.NonNegativeReals, bounds=(5,395), initialize = cp0)
model.y = pyo.Var()
model.piecewise = pyo.Piecewise(model.y, model.x, pw_pts=list(c), f_rule=list(macc), pw_constr_type='EQ', pw_repn='DCC')

#Declare parameters
model.s = pyo.Param(domain=pyo.NonNegativeReals,initialize=s0)
model.b = pyo.Param(domain=pyo.NonNegativeReals,initialize=b0)
model.tnac = pyo.Param(domain=pyo.NonNegativeReals,initialize=tnac0)


model.Obj = pyo.Objective(expr= model.b*model.x - model.y*model.x, sense=pyo.minimize)

model.con1 = pyo.Constraint(expr=model.b - model.y <= model.tnac + model.s)
model.con2 = pyo.Constraint(expr=model.b - model.y >= 1)
model.con3 = pyo.Constraint(expr= model.y >= 0)
solver = pyo.SolverFactory('ipopt')
solver.solve(model, tee=True)

参考文献

【1】Pyomo 6.3.0 Documentation: https://pyomo.readthedocs.io/en/stable/

【2】Hart W E, Laird C D, Watson J P, et al. Pyomo-optimization modeling in python[M]. Berlin: Springer, 2017.

【3】https://jckantor.github.io/ND-P

posted @ 2022-11-18 08:51  薄书  阅读(1501)  评论(0编辑  收藏  举报