Python Casadi 基本语法记录

Python Casadi 基本语法记录

lz因为主要关注优化求解,因此记的语法也偏这方面。
参考链接: CasADi Python API

Python 安装 Casadi 及使用

pip install casadi
import casadi as ca

变量定义

SX Symbolics

创建变量的基本语法

x = ca.SX.sym('x')   # 标量x, 'x'为其显示名称
x = ca.SX.sym('x', 5) #  列向量
x = ca.SX.sym('x', 4, 2) # 4*2的矩阵,注意元素的索引是按照列来的,即同一行上的index的差值为row值
f = x ** 2 + 10 # 形成表达式

# 创建没有符号原语的变量实例
B = ca.SX.zeros(4, 5)  # 一个全为0的密集4x5矩阵
B = ca.SX.ones(4, 5)  # 一个全为1的密集4x5矩阵
B= ca.SX(4, 5) # 全零的稀疏4x5矩阵
B = ca.SX.eye(4)  # 稀疏的对角4x4矩阵
B = ca.SX(scalar_type) # 用给定的参数创建标量
B = ca.SX(matrix_type) # 以 Numpy形式给定一个矩阵并完成创建
B = ca.repmat(v, n, m) # 重复表达式v垂直方向n次,水平方向 m次
B = ca.SX([1, 2, 3, 4]) # 创建列向量 
B = ca.SX([[1, 2, 3, 4]]) # 创建行向量 
B = ca.diag(ca.SX([3, 4, 5, 6])) # 对角矩阵

DM Symbolics

DM与SX类似,语法相同, 主要用于Casadi中存储矩阵以及作为函数的输入和输出,不适用与计算密集型计算

C = ca.DM(2, 3)
C_dense = C.full()
C_sparse = C.sparse()

MX Symbolics

MX与SX的一个很大的区别是MX采用矩阵形式来表达,SX会拆解为逐个元素进行,因此当使用具有许多元素的自然向量或矩阵值的操作时,MX 可以更经济。

x = ca.MX.sym('x', 2, 2)
print(x[0, 0]) #获取元素

然后是不能把SX对象和MX对象相乘,也不能执行任何其他操作以将两者混合在同一个表达式中,但是可以在MX中使用包含SX函数的调用,SX表达式定义的函数操作开销比较小,MX可以用来定义约束。

稀疏矩阵

稀疏矩阵允许高效执行线性代数运算,稀疏矩阵采用维度和两个向量进行解码,第一个向量包含每列的第一个结构非零元素的索引,第二个向量包含每个非零元素的行索引,Casadi中Sparsity实例的应用:

ca.Sparsity.dense(n, m)  # 创建一个密集的nxm稀疏jj矩阵
ca.Sparsity(n, m)  # 创建一个稀疏的nxm稀疏矩阵
ca.Sparsity.diag(n)  # 创建一个对角的nxn的稀疏矩阵
ca.Sparsity.upper(n) #  创建一个上三角的nxn的稀疏矩阵
ca.Sparsity.lower(n)  # 创建一个下三角的nxn的稀疏矩阵

获取和设置矩阵中的元素

获取元素如果用单个元素索引的话从左上角按列到右下角最后一个元素
然后切片索引和numpy以及list差不多,不做过多赘述
注意赋值的话如果对一行元素只赋值一个常量,那么即为该行的所有元素均为这个常量, 如

M = ca.SX([[3,7],[4,5]])
M[0, :] = 1 # 这一行全为0
M[0, :][0, 0] = 1  # 这样子不会改变元素,因为相当于先复制了M[0, :] 再改变复制后的内容

M = ca.SX([[3,7,8,9],[4,5,6,1]])
print(M[0,[0,3]], M[[5,-6]])  # [[3,  9]] [6, 7]

算术运算

* 逐元素相乘 @ 矩阵相乘 .T矩阵转置

ca.reshape(x, size1, size2)  # 重塑

x = ca.SX.sym('x',5)
y = ca.SX.sym('y',5)
print(ca.vertcat(x,y)) # [x_0, x_1, x_2, x_3, x_4, y_0, y_1, y_2, y_3, y_4], 列向量
print(ca.horzcat(x,y))
[[x_0, y_0], 
 [x_1, y_1], 
 [x_2, y_2], 
 [x_3, y_3], 
 [x_4, y_4]]

另外还有水平拆分和垂直拆分,horzsplit竖着切,vertsplit水平切

ca.dot(x, x) # 即为x每个元素的平方之和

查询运算

x.shape # (row, column)
x.size1()  # row
x.size2() #  column
x.numel() # 元素的总数量

函数对象

定义

函数对象通常使用以下语法创建:

f = ca.Function(name, arguments, ..., [options])

name主要是一个显示名称,arguments主要是类相关的参数,options则为一个选项结构来定义类的行为,为字典类型

x = ca.SX.sym('x', 2)
y = ca.SX.sym('y', 2)
f = ca.Function('f', [x, y], [x + y, ca.sin(y) * x])
print(f) #  f:(i0[2],i1[2])->(o0[2],o1[2]) SXFunction

相当于定义了 R 2 × R 2 → R 2 × R 2 R^2 \times R^2 \rightarrow R^2 \times R^2 R2×R2R2×R2,多输入多输出,MX工作方式相同;[2]代表是二维的列向量,另外建议将命名如下:

f = ca.Function('f', [x, y], [x + y, ca.sin(y) * x], ['x', 'y'], ['r', 'q'])
print(f)  # f:(x[2],y[2])->(r[2],q[2]) SXFunction

调用

r, q = f(1.1, 3.3)   # r, q 即为两个输出
[4.4, 4.4] [-0.17352, -0.17352]

res = f(1.1, 3.3)
print(res)  # 返回一个字典,(DM([4.4, 4.4]), DM([-0.17352, -0.17352]))

res = f(x=1.1, y=3.3)
print(res)  # 返回一个字典, {'q': DM([-0.17352, -0.17352]), 'r': DM([4.4, 4.4])}

调用函数对象时,一般要求传递到函数的参数必须与函数输入的维度相匹配,但是有两个例外

  • 传入标量参数,即为输入矩阵的所有元素设为该值
  • 可以传递行向量而不是列向量,反之亦然(lz没太理解这一点)

当输入参数变化较大时,可以使用list or dict 来调用函数的参数

arg = [1.1,3.3]
res = f.call(arg)
print('res:', res)  # [DM([4.4, 4.4]), DM([-0.17352, -0.17352])]

arg = {'x':1.1,'y':3.3}
res = f.call(arg)
print('res:', res) # {'q': DM([-0.17352, -0.17352]), 'r': DM([4.4, 4.4])}

转换:由MX转为SX

sx_function = mx_function.expand()
可以加快计算速度,但是也会带来额外的内存开销

非线性规划

min ⁡ x , y    f ( x , p ) s . t .    l b x ≤ x ≤ u b x l b g ≤ g ( x , p ) ≤ u b g \min_{x, y} \; f(x, p)\\ s.t. \; lbx \leq x \leq ubx\\ lbg \leq g(x, p) \leq ubg x,yminf(x,p)s.t.lbxxubxlbgg(x,p)ubg
其中 x ∈ R x x \in \mathbb{R}^x xRx是决策变量, p ∈ R p p \in \mathbb{R}^p pRp是参数向量
Casadi中的NLP求解器接受参数p,以及边界参数(lbx, ubx, lbg, ubg)和初始解 x 0 x_0 x0来返回最优解,一般使用IPOPT方法

创建NLP求解器

NLP求解器是基于Casadi中的nlpsol来创建的,不同的求解器和借口被实现为插件,考虑如下形式的Rosenbrock问题:
min ⁡ x , y , z    x 2 + 100 z 2 s . t .    z + ( 1 − x ) 2 − y = 0 \min_{x, y, z} \; x^2 + 100z^2 \\ s.t. \; z + (1-x)^2-y=0 x,y,zminx2+100z2s.t.z+(1x)2y=0
这个非线性问题,基于’ipopt’插件 的求解过程如下所示:

x = ca.SX.sym('x')
y = ca.SX.sym('y')
z = ca.SX.sym('z')

nlp = {'x': ca.vertcat(x, y, z), 'f': x ** 2 + 100 * z ** 2, 'g': z + (1 + x) ** 2 - y}
nlp_solver = ca.nlpsol('nlp_solver', 'ipopt', nlp)
print(nlp_solver)
Print  result:  nlp_solver:(x0[3],p[],lbx[3],ubx[3],lbg,ubg,lam_x0[3],lam_g0)->(x[3],f,g,lam_x[3],lam_g,lam_p[]) IpoptInterface
# [3] 即为变量的维数3*1
result = nlp_solver(x0=[2.0, 3.0, 0.75], lbg=0, ubg=0) #设置初值以及将不等式约束变为等式约束,即为两端都是0,result以字典的形式存储
print(result)
{'f': DM(8.01189e-66), 'g': DM(0), 'lam_g': DM(0), 'lam_p': DM([]), 'lam_x': DM([0, 0, 0]), 'x': DM([-2.83053e-33, 1, 0])}

x_opt = result['x']
print(x_opt)

求解器相关设置

opts_setting = {
            'ipopt.max_iter': 200,  # 最大迭代次数
            'ipopt.print_level': 0,  #  用于优化计算的日志输出详细级别,数字越高,越详细,0即为不输出相关信息
            'print_time': 0, # 不输出最后的求解时间
            'ipopt.acceptable_tol': 1e-8,
            'ipopt.acceptable_obj_change_tol': 1e-6
        }
solver = ca.nlpsol('nlp_solver', 'ipopt', nlp,  opts_setting)  

二次规划

Casadi中 提供了二次规划(QP)的接口,支持的求解器为qpOASES以及OOQP以及商业求解器CPLEX和GUROBI,在Casadi中求解QP具备两种不同的接口 ,分别是高级接口和低级接口。

高级接口

高级接口反映了非线性规划的一个接口,要求目标函数的限制 f ( x , p ) f(x, p) f(x,p)必须关于x为凸的二次函数以及约束函数g(x, p)必须关于x是线性的,如果函数不是二次函数和线性函数,那么会在当前初始值处完成线性化后再进行求解。当然可能也会找不到解,或者解不唯一。
考虑以下QP问题:
min ⁡ x , y    x 2 + y 2 s . t .    x + y − 10 ≥ 0 \min_{x, y} \; x^2 + y^2 \\ s.t. \; x+y-10 \geq 0 x,yminx2+y2s.t.x+y100
基于高级接口只需要把nlpsol替换为qpsol,然后使用QP的求解器

x = ca.SX.sym('x')
y = ca.SX.sym('y')

qp = {'x': ca.vertcat(x, y), 'f': x ** 2 + y ** 2, 'g': x + y - 10}
qp_solver = ca.qpsol('qp_solver', 'qpoases', qp)
print(qp_solver)

result = qp_solver(x0=[0, 0], lbg=0)
print(result)
x_opt = result['x']
print(x_opt)

qp_solver:(x0[2],p[],lbx[2],ubx[2],lbg,ubg,lam_x0[2],lam_g0)->(x[2],f,g,lam_x[2],lam_g,lam_p[]) MXFunction
{'f': DM(50), 'g': DM(-1.77636e-15), 'lam_g': DM(-10), 'lam_p': DM([]), 'lam_x': DM([-0, -0]), 'x': DM([5, 5])}
[5, 5]

#  另外一个较好的建议,把函数放在外面定义
f = x**2+y**2    #定义目标函数
qp = {'x': ca.vertcat(x, y), 'f': f, 'g': x+y-10}

低级接口

低级接口主要解决以下形式的 QP:
min ⁡ x    1 2 x T H x + g T x s . t .    x l b ≤ x ≤ x u b   a l b ≤ A x ≤ a u b \min_{x} \; \frac{1}{2}x^THx + g^Tx \\ s.t. \; x_{lb} \leq x \leq x_{ub} \\ \qquad \ a_{lb} \leq Ax \leq a_{ub} xmin21xTHx+gTxs.t.xlbxxub albAxaub

这种形式的Qp为了创建求解器实例,需要传递矩阵的稀疏模式,而不是直接传递矩阵

H = 2 * ca.DM.eye(2)
A = ca.DM.ones(1, 2)
g = ca.DM.zeros(2)
lba = 10

# construct the solver
qp = {'h': H.sparsity(), 'a': A.sparsity()}
qp_solver = ca.conic('qp_solver', 'qpoases', qp)
print(qp_solver)
result = qp_solver(h=H, g=g, a=A, lba=lba)
print(result)
x_opt = result['x']
print(x_opt)

qp_solver:(h[2x2,2nz],g[2],a[1x2],lba,uba,lbx[2],ubx[2],x0[2],lam_x0[2],lam_a0,q[],p[])->(x[2],cost,lam_a,lam_x[2]) QpoasesInterface
{'cost': DM(50), 'lam_a': DM(-10), 'lam_x': DM([-0, -0]), 'x': DM([5, 5])}
[5, 5]

Optimal Stack

Optimal Stack是Casadi中辅助类的集合,它提供了与NLP表示法之间密切的对应关系,考虑如下问题:
min ⁡ x , y   ( y − x 2 ) 2 s . t .    x 2 + y 2 = 1 x + y ≥ 1 \min_{x, y} \ (y-x^2)^2 \\ s.t.\; x^2 + y^2=1 \\ \quad x + y \geq 1 x,ymin (yx2)2s.t.x2+y2=1x+y1

opti = ca.Opti()

x = opti.variable()
y = opti.variable()

opti.minimize((y - x ** 2) ** 2)
opti.subject_to(x ** 2 + y ** 2 == 1)
opti.subject_to(x + y >= 1)

opti.solver('ipopt')
sol = opti.solve()
print('sol: ', sol)
print('result_x_opt:', sol.value(x))
print('result_x_opt:', sol.value(x))

sol:  Opti {
  instance #0
  #variables: 2 (nx = 2)
  #parameters: 0 (np = 0)
  #constraints: 2 (ng = 2)
  CasADi solver allocated.
  CasADi solver was called: Solve_Succeeded
}
result_x_opt: 0.7861513776531158
result_y_opt: 0.6180339888825889

Optimal Stack的主要特点:约束的语法比较自然,决策变量的索引是隐藏的,映射更加紧密

Optimal Stack 问题规范

声明任意数量的决策变量:

opti = ca.Opti()
x = opti.variable()  # 标量
x = opti.variable(2) # 列向量
x = opti.variable(2, 3) # 矩阵
x = opti.variable(5, 5, 'symmetric') # 对称矩阵

求解器求解时遵循声明变量的顺序,变量实际上都是普通的MX符号;另外可以声明任意数量的参数,必须在求解之前将其固定为特定的数值,并且可以随时覆盖该值:

opti = ca.Opti()
p = opti.parameter()
opti.set_value(p, 3)

下面则是目标函数的设计,使用所有可能涉及到的变量和参数来声明目标函数,再次调用则会覆盖原先的旧目标函数:

opti.minimize(ca.sin(x * (y - p)))

下面则是声明该优化问题的约束:

opti.subject_to(sqrt(x +  y) >= 1)   # 不等式约束
opti.subject_to(sqrt(x +  y) == 1)   # 等式约束
opti.subject_to([sqrt(x +  y) == 1, x == 0.2])   # 一次声明多个约束 
opti.subject_to(opti.bounded(0, x, 1))  # x的上下界,且两边没有变量时,约束将有效地传给求解器

x = opti.variable(5, 1) # 
opti.subject_to(x * p <= 3) # 使用向量进行元素等式or非等式
对于矩阵的需要注意下,因为矩阵之间为正定关系等等,一般是先矢量化然后再比较(半正定约束存在歧义)
opti.subject_to() # 清空约束

声明该问题的求解器solver,后面的参数由字典给出:

p_opts = {"expand":True}
s_opts = {"max_iter": 100}
opti.solver("ipopt",p_opts, s_opts) # 指定求解器以及其它的一些参数

还可以设置决策变量的初值,如果没有设置,那么假设初值为0:

opti.set_initial(x, 2)

对问题进行求解

sol = opti.solve()

如果求解器无法收敛,那么调用将失败并显示错误,可以考虑对该问题进行处理,因为连续地调用solve()无助于问题的收敛,另外还可以热启动求解器,表示将一个问题的解决方案转换为下一个问题的初始值:

sol = opti.solve()
print(sol.stats()['iter_count'])   # 输出迭代次数
15

# solve  again makes no  difference
sol = opti.solve()
print(sol.stats()['iter_count'])   # 输出迭代次数
15

# Passing initial makes a difference
opti.set_initial(sol.value_variables())
sol1 = opti.solve()
print(sol1.stats()["iter_count"]) # 4

初始化对偶变量暂时没这需求,等用到再补充

下面是介绍求解完问题后如何在解中检索变量的数值:

sol.value(x)  # 决策变量的值
sol.value(p)  # 参数的值
sol.value(sin(x + p))  # 获取表达式的值,任意的表达式均可

# 还可以返回仅用原问题的部分最优解来得到一个新问题解:
obj = (y - x ** 2) **2
opti.minimize(obj)
print(sol.value(obj, [y == 2]))  # 即为在新的目标函数下使用原问题的x,y设定为2,但是注意原问题的y也是没有变换的

# 另外还可以在初始值处来获取表达式的值:
print(sol.value(x ** 2 + y, opti.inital()))

Mixed Integer NLP

opti = ca.Opti()
x1 = opti.variable(2)
x2 = opti.variable(2)

obj_function = (x1[0] - 0.8) **2 + (x1[1] - 0.8) **2 + (x2[0] - 0.2) **2 + (x2[1] - 1.1) **2
opti.minimize(obj_function)

p_opts = {'discrete': [False, False, True, True]} # 指定哪些变量为integer变量,指定了为True即为Integer变量。
s_opts = {'time_limit': 100}
opti.solver('bonmin', p_opts, s_opts) 
sol = opti.solve() 

print(sol.value(obj_function))
print(sol.value(x1))
print(sol.value(x2))

附加功能

查看优化问题的相关概述

sol.disp()
sol.stats()

求解器可能找不到最优解,在这种情况下可以通过调试模式来访问融合解决方案 (有点没搞明白)

opti.debug.value(x) 
opti.debug.value(x, opti.initial())
opti.debug.show_infeasibilities() # 识别有问题的约束

# 指定一个回调函数,在求解器的每次迭代中被调用,选取迭代次数作为参数
opti.callback(lambda i: plot(opti.debug.value(x)))



你可能感兴趣的:(python,人工智能)