제1부 수학적 기초
1.15 최적화 모델링과 코드 생성: CasADi·acados·CVXPY — 임베디드 실시간 솔버 생성
1.15.1 이 장에서 배우는 것 / 선수 지식 / 특히 중요한 독자
이 장은 최적 제어 문제를 기호식으로 기술하고, 도함수와 희소 구조를 자동으로 얻고, 이를 임베디드 대상에서 실행되는 C 코드로 바꾸는 모델링 계층을 다룬다. 해법기의 알고리즘은 1.12~1.14가 다루고, 이 장은 문제를 해법기에 전달하는 방법을 다룬다. 질문은 다섯 가지다. 기호 그래프에서 야코비안과 희소 패턴은 어떻게 얻는가, 최적 제어 문제의 단계 구조는 QP 해법의 비용을 어떻게 바꾸는가, acados가 생성하는 코드와 사전 컴파일된 라이브러리는 각각 무엇인가, CVXPY의 DCP 규칙과 표준형 변환은 무엇이며 반복 호출의 비용은 어디서 생기는가, 생성 코드는 동적 메모리·수치 재현성·형상 관리에서 무엇을 보증하는가.
배우는 것:
- 이산 NLP 형식 — 직접 다중 사격으로 얻는 단계 구조.
- 계산 그래프와 자동 미분 — 공통 부분식 공유, 순방향·역방향 모드, 구조적 0, CasADi의 SX·MX.
- 구조 이용 QP — 리카티 재귀, 응축(condensing), HPIPM.
- acados — SQP-RTI의 준비·피드백 단계, 생성 파일의 구성, 설정 선택지.
- CVXPY — DCP 규칙, 표준형 변환 사슬, 해법기 선택, DPP와 CVXPYgen.
- 코드 생성의 이점과 유의점 — 동적 메모리, 수치 재현성, 버전 결합, 검증.
선수 지식: 1.1(선형대수 I), 1.12~1.14(최적화 I~III), 1.18(자동 미분의 이론), 12.15(최적 제어 기초). 표기는 BK-05를 따른다.
특히 중요한 독자: MPC를 임베디드 제어기에 배포하는 엔지니어, 궤적 최적화를 구현하는 연구자, 생성 코드의 검증 항목을 정하는 소프트웨어 품질 담당자.
출처 방침과 기준 시점(2026-11-03). 서지는 Crossref·arXiv 응답으로 확인했다(목록은 이 장 끝). 직접 읽은 자료: ① CasADi 사용자 안내서(web.casadi.org/docs, 3.8.2 판)의 기호 틀·방향 도함수·코드 생성 절, ② acados v0.6.0 저장소의 README·docs(features·installation·embedded_workflow)와
acados_template소스(acados_ocp_options.py·acados_ocp.py·c_templates_tera)·예제·HPIPM 헤더, ③ arXiv 초록(acados·HPIPM). 직접 실행한 것: casadi 3.8.2, cvxpy 1.9.3(OSQP 1.1.3·Clarabel 0.11.1·SCS 3.3.1), cvxpygen 1.0.2, acados v0.6.0(PyPI 바이너리가 없어 소스에서 빌드). 읽지 못한 것: 인용한 논문의 본문(정리·절 번호는(확인 필요)), BLASFEO 소스, 상용 도구. 수치는 순수 파이썬 구현과 격리 venv에서 실행한 결과다.
1.15.2 개념 — 모델링 계층과 해법기의 분리
해법기는 목적·제약 함수의 값과 도함수, 변수 사이의 희소 구조를 숫자 배열로 요구한다. 도함수를 손으로 유도해 작성하면 모델이 바뀔 때마다 유도와 검증이 반복되고, 오류는 해법기의 수렴 실패로 나타난다. 모델링 계층은 식을 한 번 기술하면 도함수·희소 패턴·C 코드를 기계적으로 만든다. 도구의 역할은 다음과 같다.
- CasADi: 기호 식(SX·MX), 자동 미분, C 코드 생성, 여러 해법기의 연결을 제공하는 프레임워크다. 문제 클래스에 제한이 없는 NLP·OCP를 기술한다.
- acados: OCP 구조에 특화한 SQP·RTI와 구조 이용 QP 해법(HPIPM), 선형대수 라이브러리 BLASFEO로 구성된다. 모델은 CasADi 식으로 기술하고 Python 인터페이스가 모델 함수와 연결 코드를 C로 생성한다.
- CVXPY: 볼록 문제를 DCP 규칙으로 기술하고 원뿔·QP 표준형으로 변환해 OSQP·Clarabel 등을 호출한다. 임베디드 C 코드는 CVXPYgen이 생성한다.
"코드 생성"이 가리키는 범위는 도구마다 다르다. CasADi는 함수와 일부 해법기까지, acados는 모델 함수와 연결 코드만, CVXPYgen은 해법기 소스와 작업 영역까지 생성한다(1.15.5 ⑤에서 측정). MPC 소프트웨어 사이의 비교와 acados 외 도구는 12.20이 다룬다.
1.15.3 원리
1.15.3.1 최적 제어 문제의 이산 NLP 형식
결론: 직접 다중 사격은 최적 제어 문제를 단계 구조의 NLP로 바꾸며, 이 구조가 미분의 희소성과 QP의 블록 구조를 정한다.
구간 $[0,T]$를 $N$개로 나누고 각 구간의 입력을 상수로 두면 다음 NLP를 얻는다(다중 사격의 출처는 Bock·Plitt 1984, 서지만 확인).
$$\min_{x,u}\ \sum_{k=0}^{N-1}\ell_k(x_k,u_k)+E(x_N)\quad\text{s.t.}\quad x_0=\bar x,\ \ x_{k+1}=F(x_k,u_k),\ \ h(x_k,u_k)\le0$$
$F$는 한 구간의 적분기(예: RK4)이고 변수는 $z=(x_0,u_0,\dots,x_N)$이다. 등식 제약의 야코비안은 블록 이중대각, 라그랑주 함수의 헤시안은 블록 대각이므로 선형화한 QP도 같은 단계 구조를 갖는다. 직접 콜로케이션과 궤적 최적화 일반은 11.8가 다룬다.
1.15.3.2 계산 그래프와 자동 미분
결론: 식을 연산 노드의 비순환 그래프로 두면 같은 부분식이 한 번만 계산되고, 도함수 그래프를 기계적으로 만들 수 있으며, 구조적 0이 그래프에서 드러난다.
공통 부분식을 공유하면 이 장의 대차-진자 RK4 한 구간 $F$는 연산 노드 138개이고, 공유 없이 트리로 전개하면 3,686개다(1.15.5 ①). 도함수는 노드마다 국소 편미분을 정하고 연쇄 법칙으로 전파해 얻는다. 입력 방향 하나의 접선을 전파하는 순방향 모드는 $n$개 입력에 $n$회, 출력 하나의 수반을 전파하는 역방향 모드는 $m$개 출력에 $m$회가 필요하다. CasADi 안내서는 두 모드의 방향 도함수 비용이 함수 평가 비용에 비례한다고 설명한다. 모드 비용의 이론과 수치 안정성은 1.18이 다룬다.
CasADi의 기호 틀은 두 가지다. SX는 스칼라 단항·이항 연산으로 이루어진 식의 행렬이고, MX는 희소 행렬을 입출력으로 하는 일반 연산(함수 호출 노드 포함)으로 식을 구성한다(안내서 3.1·3.3절). 같은 RK4 한 구간은 SX에서 노드 176개, MX에서 24개로 측정되었다. 희소 야코비안은 희소 패턴 검출, 그래프 채색, 방향 도함수 계산, 조립의 순서로 구성된다고 안내서가 설명한다. 이 장의 $4\times5$ 야코비안은 구조적 비영 원소가 15개로, 구현과 CasADi가 같은 위치를 보고했다. 동역학 라이브러리의 해석적 도함수와 자동 미분의 비교는 3.6이 다룬다.
1.15.3.3 구조 이용 QP와 응축
결론: 단계 구조의 QP는 리카티 재귀로 $N$에 비례하는 연산으로 풀리고, 구조를 이용하지 않는 밀집 KKT 풀이는 $N^3$에 비례한다. 응축은 상태를 소거해 구조를 없애는 대신 조건수를 키울 수 있다.
선형화한 부문제 $\min\sum\tfrac12\bigl(\Delta x_k^\top Q\Delta x_k+\Delta u_k^\top R\Delta u_k\bigr)+\cdots$, $\Delta x_{k+1}=A_k\Delta x_k+B_k\Delta u_k+c_k$에서 후진 재귀는 가치 함수 $\tfrac12\Delta x^\top P_k\Delta x+p_k^\top\Delta x$를 갱신한다.
$$H_{uu}=R+B^\top P_{k+1}B,\quad H_{ux}=B^\top P_{k+1}A,\quad K_k=-H_{uu}^{-1}H_{ux},\quad P_k=Q+A^\top P_{k+1}A+H_{ux}^\top K_k$$
전진 단계는 $\Delta u_k=K_k\Delta x_k+k_k$와 동역학으로 해를 복원한다(선형항 $p_k$·$k_k$는 구현에 있다). 측정에서 단계 수 $N$이 2배가 되면 리카티 곱셈은 2배, 밀집 KKT의 LU는 약 8배가 되었다(1.15.5 ③). HPIPM 소스에서 OCP QP의 KKT 풀이 주석에 후진 리카티 재귀가 있고 고전·제곱근 알고리즘 선택 옵션(square_root_alg)이 있음을 읽었다.
응축은 $x_{k+1}$ 제약으로 상태를 소거해 입력 $N n_u$개의 밀집 QP로 바꾼다. 부분 응축은 인접한 $k$개 단계를 한 단계로 묶는다. acados는 qp_solver_cond_N으로 응축 후 단계 수를 정한다. 열린 루프가 불안정한 모델에서는 소거에 $A^{N-1}B$ 꼴의 항이 나타나 길어질수록 커진다.
1.15.3.4 acados의 SQP-RTI와 해법기 구성
결론: acados는 구조 이용 QP와 사전 컴파일된 선형대수 라이브러리를 핵으로 두고, 모델 함수와 연결 코드만 생성한다. RTI는 제어 주기당 SQP 반복을 1회만 수행하며 계산을 준비와 피드백으로 나눈다.
RTI(Diehl 외 2002)에서 준비 단계는 이전 해 근방에서 선형화해 QP 데이터를 만들고, 피드백 단계는 새 상태 $\hat x_0$를 삽입해 QP를 풀고 해를 갱신한다. acados 문서는 이 분할이 제어 지연을 줄인다고 설명한다. Python 인터페이스에서 rti_phase는 0(준비·피드백), 1(준비), 2(피드백)이다(소스). 소스에서 읽은 설정 선택지는 다음과 같다.
| 항목 | 선택지(acados_ocp_options.py, v0.6.0) |
|---|---|
| 비선형 해법 | SQP·SQP_RTI·DDP·SQP_WITH_FEASIBLE_QP |
| 헤시안 | GAUSS_NEWTON·EXACT |
| 적분기 | ERK·IRK·GNSF·DISCRETE·LIFTED_IRK |
| QP 해법기 | PARTIAL_CONDENSING_HPIPM·FULL_CONDENSING_HPIPM·FULL_CONDENSING_QPOASES·PARTIAL_CONDENSING_QPDUNES·PARTIAL_CONDENSING_OSQP·PARTIAL_CONDENSING_CLARABEL·FULL_CONDENSING_DAQP |
| 전역화 | FIXED_STEP·MERIT_BACKTRACKING·FUNNEL_L1PEN_LINESEARCH |
기본값은 소스의 AcadosOcpOptions 초기화에서 읽었다(GAUSS_NEWTON·ERK·SQP·FIXED_STEP, QP 해법기는 PARTIAL_CONDENSING_HPIPM). 비용에는 기본 스케일링이 적용된다. acados_ocp.py는 cost_scaling이 없으면 단계 비용에 구간 길이를, 종단 비용에 1을 곱한다. 다른 도구와 비용 값을 비교하려면 이 곱을 맞춰야 한다(1.15.5 ④에서 일치 확인).
1.15.3.5 CVXPY의 DCP 규칙과 표준형 변환
결론: DCP는 식의 곡률을 규칙으로 판정해 볼록성을 증명하는 문법이며, 판정에 실패한 문제는 해법기에 전달되지 않는다. 통과한 문제는 규칙 기반 변환 사슬을 거쳐 원뿔 또는 QP 형식이 된다.
식의 곡률은 상수·아핀·볼록·오목 중 하나이거나 판정 불가다. 합성 $f(g_1,\dots,g_k)$는 $f$가 볼록이고 각 인자에서 증가이면 $g_i$가 볼록, 감소이면 $g_i$가 오목일 때 볼록으로 판정된다(오목은 대칭). 문제는 볼록 식의 최소화(또는 오목 식의 최대화)이고 볼록 식 $\le$ 오목 식 형태의 부등식과 아핀 등식만 가질 때 DCP다(Grant·Boyd·Ye, 서지만 확인이며 규칙은 1.15.5 ⑤의 판정 사례와 함께 읽는다). 변환은 재작성 시스템으로 설명된다(Agrawal 외 2018). 실행에서 확인한 변환 사슬(reduction chain)은 Dcp2Cone → CvxAttr2Constr → EliminateZeroSized → ConeMatrixStuffing → 해법기 인터페이스였다(1.15.5 ⑤).
DPP는 파라미터가 아핀 위치에만 나타나는 문제로, 변환 결과를 저장해 두고 파라미터 값만 바꿔 재호출할 수 있다(is_dpp()). 변환 비용이 해법 비용보다 클 수 있으므로 반복 호출에서는 DPP 여부가 비용을 정한다. 기본 해법기는 설치된 해법기에 따른다. 이 설치(CLARABEL·SCS·SCIPY·HIGHS·OSQP)에서 LP·SOCP·지수 원뿔은 CLARABEL, QP는 OSQP가 선택되었다.
1.15.3.6 코드 생성의 이점과 유의점
결론: 생성 코드는 인터프리터 의존을 없애고 정적 메모리 실행을 가능하게 한다. 문제의 구조·크기·컴파일러 옵션이 코드에 고정되므로 재생성과 재검증 절차가 따른다.
| 항목 | 근거(1.15.5) | 유의점 |
|---|---|---|
| 의존 제거 | C 컴파일러만 있으면 컴파일된다(CasADi 안내서). sqpmethod+qrqp 생성 파일의 #include는 math.h·stdarg.h·stdio.h | 해법기가 라이브러리면 링크 대상과 ABI가 빌드 시 고정된다(BLASFEO 타깃 등) |
| 정적 메모리 | 호출 구간 힙 할당: acados 1,000회 0회, CVXPYgen 1,000회 0회, CasADi 생성 코드는 malloc 계열 문자열 0 | 생성(create) 구간에는 할당이 있다(acados 90회). 소스에 malloc이 있어도 실행 구간 할당과 구분해 계수한다 |
| 호출 시간 | 모델 함수 $F$와 $J$ 0.2~0.3 µs(-O2), RTI 준비 약 58 µs·피드백 약 111 µs | 최대 시간은 QP 반복 수와 OS에 따라 늘어난다(피드백 최대 349 µs) |
| 수치 재현성 | -O0 코드는 파이썬 해석기와 비트 단위 일치, -march=native는 상대 $4\times10^{-16}$ | 컴파일러 옵션이 마지막 자리를 바꾸므로 비트 일치를 검증 기준으로 삼지 않는다 |
| 형상 고정 | 차원·구조 변경 시 재생성 | 생성 코드가 커지면 컴파일 시간이 늘어난다(nlpsol 생성 16,720줄, gcc -O2 약 7 s). CasADi 안내서도 긴 코드는 함수 분할을 권한다 |
| 도구 버전 | acados가 CasADi 3.8.2에 "not tested" 경고를 출력, acados_template 패키지 문자열은 0.5.1이고 소스 태그는 v0.6.0 | 도구 버전 조합을 고정하고 기록한다 |
1.15.4 알고리즘
모델에서 코드까지(이 장의 구현)
build: 기호 모델 → 계산 그래프 G (공유, x*1 단순화)
differentiate: 입력 수 ≤ 출력 수이면 순방향, 아니면 역방향
(접선·수반이 없는 노드는 구조적 0으로 둔다)
emit: G의 도달 노드를 위상 순서로 지역 변수에 출력
compile: gcc -shared, 해법기 라이브러리와 링크
verify: 기준 구현·중심차분 대조, 호출 구간 할당 계수
구조 이용 SQP 한 반복과 RTI 주기
준비(x0_hat 입력 전): 이전 해 (X, U)에서 F, J 평가
A_k, B_k, c_k = F − x_{k+1}, 기울기
x0_hat 수신
피드백: Δx_0 = x0_hat − x_0 삽입, 리카티 풀이
(X, U) ← (X, U) + (ΔX, ΔU), u_0 출력
SQP: 스텝이 허용치 미만일 때까지 반복
(RTI는 제어 주기당 1회)
1.15.5 풀이 예제 — 그래프·코드 생성기, 구조 이용 QP, 세 라이브러리 확인
계산 그래프·순방향·역방향 미분·C 코드 생성기를 순수 파이썬으로 구현해 대차-진자의 RK4 이산 동역학 $F$와 야코비안 $J$를 생성하고, 같은 선형화를 사용하는 가우스-뉴턴 SQP를 리카티 재귀로 푼다. 결론부터 적으면 다음과 같다. ① 생성한 C 코드는 파이썬 해석기와 일치하고(-O0·-O2에서 0) $J$는 중심차분과 $5\times10^{-10}$ 안에서 일치한다. ② 리카티와 밀집 KKT의 해는 $7\times10^{-13}$ 안에서 같고 연산 횟수는 $N=20$에서 455배 차이가 난다. ③ 이 SQP의 해는 CasADi+IPOPT와 $3\times10^{-14}$, acados와 $2\times10^{-11}$ 안에서 같다. ④ CasADi·CVXPY·CVXPYgen·acados의 생성·변환 결과를 실행으로 확인했다. 스크립트는 robotics/fig-src/1.15-codegen.py(리카티 재귀 포함)이며 행렬 곱·LU는 1.1의 구현을 불러온다.
| 가정·결과 | 값 |
|---|---|
| 모델 | 대차-진자, $M=1.0$ kg, $m=0.1$ kg, $l=0.8$ m, $g=9.81$ m/s$^2$ (acados 예제 저장소의 값과 같다) |
| 이산화 | RK4 1스텝 $\Delta t=0.05$ s, $N=20$, $T=1.0$ s |
| 비용 | $\sum \tfrac12\Delta t(x^\top Qx+Ru^2)+\tfrac12x_N^\top Qx_N$, $Q=\mathrm{diag}(1.0, 10.0, 0.1, 0.1)$, $R=0.01$ |
| 초기 상태 | $x_0=(0,\ 0.3\ \mathrm{rad},\ 0,\ 0)$ (직립에서 17.2도 기운 상태), 입력 제약 없음 |
| 첫 QP(영 입력 궤적에서 선형화) | 변수 104개, 등식 84개, 밀집 KKT 크기 188 |
| 리카티 대 밀집 KKT 해의 차이 | $\Delta u$ 6.9e-13, $\Delta x$ 3.2e-14 |
| GN-SQP 수렴 | 12회 반복(스텝 $<10^{-12}$), $u_0=-19.504704770524$ N, 비용 $J=0.493992291420$ |
| 실행 환경 | 13th Gen Intel(R) Core(TM) i5-13500, gcc 13.3, 파이썬 3.12.3 |
그래프·미분·코드 생성의 전체 구현이다(1.15.4의 알고리즘에 대응한다).
G, IDX = [], {} # 계산 그래프와 노드 색인
def nd(op, a=None, b=None):
k = (op, a, b) # 같은 노드는 하나로 공유한다
if k not in IDX:
IDX[k] = len(G)
G.append(k)
return IDX[k]
cst = lambda v: nd('c', float(v))
def mk(op, a, b=None):
one = cst(1) # x*1은 x로 줄인다
if op == '*' and one in (a, b):
return b if a == one else a
return nd(op, a, b)
class S: # 모델을 파이썬 식으로 작성
def __init__(s, n):
s.n = n
lift = lambda o: o.n if isinstance(o, S) else cst(o)
for nm, op in (('add', '+'), ('sub', '-'), ('mul', '*'),
('truediv', '/')):
setattr(S, f'__{nm}__', lambda s, o, op=op:
S(mk(op, s.n, lift(o))))
setattr(S, f'__r{nm}__', lambda s, o, op=op:
S(mk(op, lift(o), s.n)))
sin = lambda x: S(nd('sin', x.n))
cos = lambda x: S(nd('cos', x.n))
def part(n):
"""노드 n의 인자에 대한 국소 편미분 [(인자, 노드)]"""
op, a, b = G[n]
one, m1 = cst(1), cst(-1)
if op == '+': return [(a, one), (b, one)]
if op == '-': return [(a, one), (b, m1)]
if op == '*': return [(a, b), (b, a)]
if op == '/':
ib = mk('/', one, b)
return [(a, ib), (b, mk('*', m1, mk('*', n, ib)))]
if op == 'sin': return [(a, nd('cos', a))]
if op == 'cos': return [(a, mk('*', m1, nd('sin', a)))]
return []
def reach(outs): # outs가 의존하는 노드
seen, st = set(), [o for o in outs if o is not None]
while st:
n = st.pop()
if n not in seen:
seen.add(n)
if G[n][0] not in ('c', 'v'):
st += [a for a in G[n][1:] if a is not None]
return sorted(seen)
acc = lambda s, x: x if s is None else mk('+', s, x)
def fwd(k, outs):
"""순방향 모드: 입력 k에 대한 접선. None은 구조적 0"""
t = {}
for n in reach(outs):
if G[n][0] == 'v':
t[n] = cst(1) if G[n][1] == k else None
elif G[n][0] != 'c':
for a, d in part(n):
if t.get(a) is not None:
t[n] = acc(t.get(n), mk('*', t[a], d))
return [t.get(o) for o in outs]
def rev(o, nin):
"""역방향 모드: 출력 o의 모든 입력에 대한 수반"""
bar = {o: cst(1)}
for n in reversed(reach([o])):
for a, d in part(n) if n in bar else []:
bar[a] = acc(bar.get(a), mk('*', bar[n], d))
return [bar.get(nd('v', i)) for i in range(nin)]
def emit(name, outs):
"""outs를 계산하는 C 함수. 지역 변수만 사용한다."""
def ref(n):
op, a, _ = G[n]
return (f'({a!r})' if op == 'c' else
f'x[{a}]' if op == 'v' else f'a{n}')
L = ['#include <math.h>',
f'void {name}(const double *x, double *y) {{']
for n in reach(outs):
op, a, b = G[n]
if op not in ('c', 'v'):
e = (f'{op}({ref(a)})' if b is None
else f'{ref(a)} {op} {ref(b)}')
L.append(f' double a{n} = {e};')
for j, o in enumerate(outs):
r = '0.0' if o is None else ref(o)
L.append(f' y[{j}] = {r};')
return '\n'.join(L + ['}'])
모델은 일반 파이썬 식으로 기술하며(RK4는 이 함수를 네 번 호출한다), 생성 함수는 입력 5개($x$ 4, $u$ 1)에서 $F$ 4개와 $J$ 20개를 출력한다.
def ode(x, u): # 대차-진자 연속 동역학
s, c, w = sin(x[1]), cos(x[1]), x[3]
d = MC + MB - MB * c * c
return [x[2], w,
(-MB * LP * s * w * w + MB * GR * c * s + u) / d,
(-MB * LP * c * s * w * w + u * c
+ (MC + MB) * GR * s) / (LP * d)]
① 그래프·미분·코드 크기.
| 항목 | 값 |
|---|---|
| 입력·출력 | 입력 5개(상태 4, 입력 1), 출력 4개($F$) 또는 24개($F$와 $J$) |
| $F$의 연산 노드(공통 부분식 공유 후) | 138 |
| 같은 식을 공유 없이 트리로 전개한 연산 수 | 3,686 |
| $F$와 $J$, 순방향 모드(접선 5회) 연산 노드 | 637 |
| $F$와 $J$, 역방향 모드(수반 4회) 연산 노드 | 667 |
| $J$의 구조적 비영 원소 | 15 / 20 (두 모드 동일) |
| 생성 C 줄 수: $F$ · $F$와 $J$ 순방향 · 역방향 | 145 · 664 · 694 |
| gcc 13.3 컴파일 시간 $F$와 $J$ 순방향: -O0 · -O2 | 177 ms · 623 ms |
② 값의 일치와 컴파일러 옵션.
| 비교 (무작위 1,000점) | 최대 차이 |
|---|---|
| 생성 코드 $F$ 대 독립 구현의 float RK4 (절대) | 0.0e+00 |
| 생성 코드($F$와 $J$) 대 파이썬 그래프 해석기 (상대, 분모 $\max(1,\lvert y\rvert)$) | 0.0e+00 |
| 생성 코드 $J$ 대 중심차분 $h=10^{-6}$ (상대) | 4.7e-10 |
| 순방향 모드 코드 대 역방향 모드 코드 (상대) | 4.4e-16 |
컴파일 옵션 -O0의 코드 대 파이썬 그래프 해석기 (상대) | 0.0e+00 |
컴파일 옵션 -O2 -march=native의 코드 대 파이썬 그래프 해석기 (상대) | 4.0e-16 |
컴파일 옵션 -O3 -ffast-math의 코드 대 파이썬 그래프 해석기 (상대) | 4.3e-16 |
③ 구조 이용: 연산 횟수. 밀집 KKT의 크기는 $9N+8$이다. 밀집 LU의 횟수는 1.1 lu의 반복 구조에서 센 값이고, 리카티의 횟수는 구현의 행렬 곱 계수기에서 센 값이다.
| 호라이즌 $N$ | 밀집 KKT 크기 | 밀집 LU 곱셈·나눗셈 | 리카티 곱셈 | 비 | $\lVert A^{N-1}B\rVert_\infty$ |
|---|---|---|---|---|---|
| 5 | 53 | 53,795 | 1,245 | 43 | 8.5e-02 |
| 10 | 98 | 328,055 | 2,490 | 132 | 1.8e-01 |
| 20 | 188 | 2,267,750 | 4,980 | 455 | 1.1e+00 |
| 40 | 368 | 16,814,840 | 9,960 | 1,688 | 4.4e+01 |
| 80 | 728 | 129,403,820 | 19,920 | 6,496 | 6.8e+04 |
| 160 | 1448 | 1,015,152,980 | 39,840 | 25,481 | 1.6e+11 |
$N=160$에서 직립 선형화의 $\lVert A^{N-1}B\rVert_\infty$는 $1.6\times10^{11}$이다. acados에서 같은 문제(입력 제약 $\lvert F\rvert\le15$ N)를 완전 응축 HPIPM으로 $N=160$에 풀면 첫 SQP 반복에서 QP 해법기가 상태 3(ACADOS_MINSTEP)을 반환했다. 같은 설정의 구조 이용(응축 없음)은 풀렸다. 이 실패의 원인이 응축의 조건수 증가인지는 확인하지 않았다 (추정). $N=10\sim80$에서는 두 설정 모두 SQP 8회 반복으로 끝났다(해의 비교는 하지 않았다). 시간은 실행마다 편차가 커서($N=20$의 한 번 풀이가 $0.5\sim0.9$ ms 사이에서 바뀌었다) 두 설정의 우열을 가리지 않는다.
④ SQP 수렴과 해의 일치.
| 반복 | 동역학 결함 $\max\lvert F(x_k,u_k)-x_{k+1}\rvert$ | 스텝 크기 $\max\lvert\Delta\rvert$ |
|---|---|---|
| 1 | 0.0e+00 | 3.7e+01 |
| 2 | 1.8e+00 | 3.7e+01 |
| 3 | 3.3e-02 | 2.7e-01 |
| 4 | 1.2e-05 | 9.5e-03 |
| 6 | 2.6e-11 | 3.0e-05 |
| 8 | 2.5e-16 | 8.7e-08 |
| 10 | 2.2e-16 | 2.5e-10 |
| 12 | 2.2e-16 | 7.1e-13 |
| 해 | $u_0$ (N) | 이 장의 GN-SQP와 $\max\lvert\Delta u\rvert$ |
|---|---|---|
| 이 장의 순수 파이썬 GN-SQP | $-19.504704770524$ | — |
| CasADi 3.8.2 + IPOPT(4회 반복, 정확한 헤시안) | $-19.504704770524$ | $2.9\times10^{-14}$ |
CasADi sqpmethod+qrqp(4회 반복) | — | IPOPT와 $1.2\times10^{-14}$ |
| acados v0.6.0 SQP(9회 반복, GN, HPIPM) | $-19.504704770546$ | $2.2\times10^{-11}$ |
acados의 비용 $0.493992291420$은 이 장의 값과 $2.8\times10^{-16}$ 안에서 같다. acados의 반복 수(9회)와 이 장(12회)은 수렴 판정 기준이 달라 직접 비교하지 않는다.
⑤ 라이브러리 확인(실행 2026-11-03). 스크립트는 robotics/fig-src/1.15-libcheck-casadi.py·-cvxpy.py·-cvxpygen.py·-acados.py, 출력은 robotics/fig-src/data/1.15-*-output.txt에 있다.
| CasADi 3.8.2 | 결과 |
|---|---|
| 노드 수 | SX: $F$ 176, $J$ 667, $F$와 $J$ 703. MX: $F$ 24, $J$ 142. 노드 정의가 이 장 구현(138·637·667)과 같은지 확인하지 않았다 |
| 값 | SX 대 이 장 C 코드: $F$ $1.1\times10^{-16}$, $J$ $4.4\times10^{-16}$(300점). MX 대 SX $5.6\times10^{-17}$ |
| 미분 모드 | forward(5)·reverse(4)로 만든 $J$가 SX jacobian과 $0$·$2.2\times10^{-16}$, 중심차분과 $1.3\times10^{-10}$ |
생성 코드(FJ) | 879줄, 17,184 B, malloc 계열 0, casadi_real은 double, 서명 int FJ(arg, res, iw, w, mem). external로 적재한 결과는 SX 가상기계와 일치(0) |
| 호출 시간 | $F$와 $J$, 2,000,000회 루프 중앙값: -O0에서 이 장 0.59 µs·CasADi 1.29 µs, -O2에서 0.28 µs·0.22 µs(-O2의 차이는 실행 간 변동 범위 안이다). SX 가상기계는 파이썬 호출을 포함해 약 30 µs, 이 장의 파이썬 해석기는 약 445 µs |
nlpsol 생성 | sqpmethod+qrqp: 16,720줄·515,764 B, 정적 메모리 배열, 생성한 해법기의 해는 내장과 0, IPOPT와 $1.2\times10^{-14}$ |
IPOPT의 generate() | 14,771줄 파일이 <coin-or/IpStdCInterface.h>를 포함하고 IpoptSolve를 호출한다. IPOPT 라이브러리 링크가 필요하다 |
| CVXPY 1.9.3·cvxpygen 1.0.2 | 결과 |
|---|---|
| DCP 판정 | norm(x,2) 볼록, -norm(x,2) 오목, x[0]*x[1]·x[0]/y 판정 불가, sqrt(square(y)) QUASICONVEX(DCP 아님), quad_over_lin(x[0],y) 볼록 |
| 문제 판정 | min norm(x) s.t. norm(x)<=1 통과, norm(x)>=1·min sqrt(y)는 DCPError, max sqrt(y)는 통과 |
| LQ 부문제(104변수) | 사슬 Dcp2Cone → … → ConeMatrixStuffing → OSQP. 변환 후 변수 208, 등식 188, $P$ 비영 104, $A$ $188\times208$(비영 592). 부분 인덱스에 이차 형식을 적용한 경우의 관찰이며 원인은 소스를 읽지 않았다 |
| 해의 일치 | 리카티와의 차이: CLARABEL(기본 허용치) $1.7\times10^{-8}$, OSQP(eps $10^{-10}$) $2.8\times10^{-13}$, SCS(eps $10^{-9}$) $1.7\times10^{-11}$ |
| 호출 시간 | OSQP 첫 호출(변환 포함) 약 64 ms, 같은 문제 둘째 호출 약 4 ms, $x_0$를 파라미터로 둔 DPP 재호출 중앙값 약 6 ms, 문제를 매번 다시 구성하면 중앙값 약 140 ms |
| CVXPYgen(OSQP) | 생성 0.26 s, C 파일 48개·21,896줄(OSQP 소스 포함). 1,000회 호출 힙 할당 0회. 기본 허용치에서 25회 반복·호출당 약 146 µs, 해 오차 $1.1\times10^{-4}$. eps $10^{-10}$에서 $2.5\times10^{-10}$. 파이썬 래퍼는 pip가 없는 venv에서 빌드되지 않아 C 코드 경로만 시험했다 |
| acados v0.6.0 | 결과 |
|---|---|
| 생성 파일 | C 7개, 2,701줄: 해법기 1,218줄, 모델 함수 3개(expl_ode_fun 190줄·expl_vde_forw 430줄·expl_vde_adj 249줄, CasADi가 생성), 예제 main 2개, 시뮬레이션 해법기 311줄 |
| 라이브러리 | 생성 해법기 .so 52,864 B. 사전 컴파일: libacados 794,976 B, libhpipm 1,807,328 B, libblasfeo 2,427,104 B |
| 할당 | 해법기 C에서 malloc·calloc 포함 줄 22개(생성 구간). 측정: create 90회, 호출 1,000회 구간 0회(SQP_RTI·SQP 해법기 모두), 비정상 상태 반환 0회 |
| QP 설정 4종 | PARTIAL_CONDENSING_HPIPM(cond_N 20·4)·FULL_CONDENSING_HPIPM·FULL_CONDENSING_QPOASES 모두 같은 해($2.2\times10^{-11}$). qpOASES는 단측 제약 미지원 안내가 출력된다 |
| 빌드 | BLASFEO_TARGET=X64_AUTOMATIC, HPIPM_TARGET=GENERIC로 빌드. tera_renderer는 rustc 1.85에서 최신 의존 크레이트를 컴파일하지 못해 일부를 낮춰 소스에서 빌드했다 |
⑥ RTI의 단계 시간. 입력 제약 $\lvert F\rvert\le15$ N, $N=20$, 120스텝 폐루프(스텝 60에서 $\theta$에 0.15 rad 외란)에서 준비·피드백 단계를 따로 호출해 time_tot을 기록했다(비실시간 리눅스, 한 기계).
준비 단계의 중앙값은 58 µs(최대 129 µs), 피드백 단계의 중앙값은 111 µs(최대 349 µs)다. 피드백 단계의 QP 반복 수는 중앙값 2, 최대 8이며, 스텝 1의 입력 포화 직후(스텝 2에서 8회)와 외란 직후(스텝 62에서 5회)에 늘었다. 반복 수가 3인 스텝의 피드백 중앙값은 152 µs, 2인 스텝은 108 µs였다. 준비 단계의 돌출(스텝 15·16)은 QP 반복 수와 무관하므로 OS 지터로 추정한다 (추정). 수렴할 때까지 푼 SQP와 비교한 폐루프 차이는 $\max\lvert\Delta u\rvert=0.476$ N, $\max\lvert\Delta\theta\rvert=0.0033$ rad였다. time_sim 통계는 time_tot보다 큰 값이 보고되어 사용하지 않았다.
⑦ 대표 소프트웨어와 현황(조회 2026-11-03).
| 소프트웨어 | 저장소 | 라이선스 | 최신 릴리스 | 마지막 푸시 |
|---|---|---|---|---|
| CasADi | casadi/casadi | LGPL-3.0 | 3.8.2·2026-10-27 | 2026-11-02 |
| acados | acados/acados | LICENSE 본문이 BSD 2-Clause(GitHub API는 NOASSERTION) | v0.6.0·2026-08-06 | 2026-10-29 |
| CVXPY | cvxpy/cvxpy | Apache-2.0 | v1.9.3·2026-09-19 | 2026-11-03 |
| CVXPYgen | PyPI cvxpygen | Apache-2.0(PyPI 표기) | 1.0.2·2026-10-22(PyPI) | — |
푸시 날짜는 유지의 대리 지표일 뿐이다(0.18). 제품 추천·순위가 아니며 시간 측정은 한 문제·한 기계의 값이다.
한계. 수치는 대차-진자 한 모델이고 입력 제약은 RTI 폐루프에만 적용했다. 호출 시간은 비실시간 리눅스의 값이다. 순수 파이썬 코드 생성기는 사칙연산과 sin·cos만 지원한다.
견해: 생성 코드의 검증은 값의 일치보다 호출 구간 할당 계수와 최악 시간 측정을 우선해야 한다고 본다. 근거는 이 장에서 값은 세 구현이 $10^{-11}$ 안에서 일치해 변별력이 없었고, 힙 할당은 생성 구간 90회 대 호출 구간 0회로 구간에 따라 달랐으며, 피드백 시간은 QP 반복 수에 따라 중앙값의 3배(111 µs 대 349 µs)까지 달라졌다는 점이다. 단일 모델·단일 기계의 결과이므로 다른 조건에서는 다를 수 있다.
1.15.6 설계 트레이드오프 — 모델링·해법 경로의 선택
공개 문헌과 이 장의 실행에서 확인되는 경로는 세 가지다.
- ① CasADi 기술 + 범용 NLP 해법기(런타임 호출). 기호 식에서 도함수를 얻어 IPOPT 등 NLP 해법기에 전달한다. 개발 속도가 빠르고 제약 종류에 제한이 없다. 해법기 라이브러리와 파이썬 환경이 실행 시점에 필요하다.
- ② acados 생성 해법기. OCP 구조를 이용하는 SQP·RTI와 HPIPM을 사용하며 호출 구간 할당이 0이었다. 문제 클래스는 OCP 구조로 제한되고 빌드 환경(컴파일러·BLASFEO 타깃)을 관리해야 한다.
- ③ CVXPY 기술(볼록 문제) + CVXPYgen. DCP 판정으로 볼록성이 보증되고 해법기를 교체할 수 있다. 비볼록 동역학은 기술할 수 없고, 변환 비용 때문에 반복 호출은 DPP 문제로 제한된다.
| 경로 | 문제 클래스 | 산출물 | 호출 구간 할당(측정) | 주로 선택하는 쪽 |
|---|---|---|---|---|
| ① | 일반 NLP·OCP | 파이썬 환경 또는 생성 함수 | 해법기에 따름(IPOPT는 확인하지 않음) | 연구·오프라인 궤적 최적화 |
| ② | OCP 구조의 NLP | C 해법기 + 라이브러리 | 0 (acados 1,000회) | 임베디드 NMPC |
| ③ | 볼록 QP·SOCP 등 | 변환 사슬 또는 생성 C | 0 (CVXPYgen 1,000회) | 선형·볼록 MPC, 설계 단계 검증 |
견해: 처음에는 ①로 모델과 비용을 확정하고 같은 CasADi 식을 ②에 넘겨 해를 대조한 뒤 배포하는 순서가 검증 비용이 낮다고 본다. 근거는 이 장에서 CasADi+IPOPT 해와 acados 해가 $10^{-11}$ 안에서 일치해 ①이 ②의 기준 해로 사용될 수 있었다는 점이다. 비용 스케일링 같은 기본값이 도구마다 다르므로 대조 전에 맞춰야 한다.
1.15.7 응용 축 비교
| 응용 축 | 최적화 모델링·코드 생성의 사용 | 요구 | 관련 장 |
|---|---|---|---|
| 산업·제조 | 서보 주기 궤적 최적화·진동 억제 MPC를 제어기에 배포 | 결정적 주기, 형상 고정, PLC 연동 | 8.28, 12.16 |
| 물류·이동 | 이동 로봇 경로 추종 MPC | 속도·가속도 제약, 임베디드 리눅스 | 12.24 |
| 도로 주행 | 궤적 최적화·추종 | 재현성, 소프트웨어 도구의 신뢰 근거 | 30.6, 27.15 |
| 서비스 | 팔·접촉 제어의 QP | 안전 속도 한계, 저비용 연산 자원 | 12.9, 21.20 |
| 의료·헬스케어 | 힘·속도 한계 아래 도구 끝 제어 | 검증 가능한 단순 구조 | 26.5 |
| 휴머노이드·범용 | 전신 제어 QP·MPC | 최악 시간 상한, 다접촉 | 12.7, 14.17 |
| 필드 | 무인 장기 운용에서 코드 갱신 | 형상 관리·원격 갱신 | 15.19 |
| 국방·안보 | 원리 수준에서 고유 요구 없음 | — | — |
이 표는 이 책의 정리이며 장별 요구는 다르다.
1.15.8 관련 규격·프로토콜
| 문서 | 발행 | 무엇을 정하나 | 상세 장 |
|---|---|---|---|
| ISO/IEC 9899 | ISO·IEC | C 언어. acados 생성 Makefile은 -std=c99로 컴파일(빌드 로그에서 확인). 최신 판 (확인 필요) | 8.10 |
| IEEE 754-2019 | IEEE | 부동소수점 산술의 형식·반올림. 컴파일러 옵션과 FMA 사용은 별도 | 1.18 |
| MISRA C | MISRA | 안전 관련 C 코딩 지침. 생성 코드의 준수 여부는 확인하지 않았다 (확인 필요) | 26.20 |
| IEC 61131-3 | IEC | PLC 프로그래밍 언어. 생성 C 코드의 연결 경로는 제조사별 | 8.28 |
| IEC 61508 | IEC | 기능안전과 소프트웨어 요구. 도구 관련 조항 번호 (확인 필요) | 26.5 |
| FMI | Modelica Association | 모델 교환·코시뮬레이션 인터페이스(판 (확인 필요)) | 16.23 |
| CasADi 생성 코드 API | CasADi 프로젝트 | fname_work·fname의 작업 배열 규약(안내서 5.3절) | — |
1.15.9 관련 규제·인증 요구와의 연결
직접 적용되는 규제는 없다. 생성 코드가 안전 기능(힘·속도 한계 감시 등)의 일부로 사용되면 소프트웨어 안전 표준의 도구·형상 요구가 적용될 수 있다. 기능안전은 26.5, 자동차는 27.15, 소프트웨어 안전과 품질은 26.20, CI·시험은 15.18가 다룬다.
견해: MPC 해법기는 성능 기능으로 두고 안전 기능은 해법기 출력과 독립한 감시기로 구성하는 편이 인증 부담을 줄인다고 본다. 근거는 이 장에서 해법기 호출 시간이 QP 반복 수에 따라 달라졌고 QP 해법기가 실패 상태(
ACADOS_MINSTEP)를 반환한 조건이 관찰되었다는 점이다. 구조는 설계에 따라 다르며 법률·인증 자문이 아니다.
1.15.10 한계·유의점·미해결
- 측정. 단일 모델·단일 기계·비실시간 리눅스이며 최악 실행 시간(WCET) 분석은 하지 않았다. 시간은 실행마다 달라 한 자리 유효숫자 수준으로 읽는다.
- 빌드. acados는
X64_AUTOMATIC·GENERIC타깃으로만 빌드했다. 교차 컴파일(ARM 등)과 다른 BLASFEO 타깃은 문서만 읽었다. - 범위. 상용 해법기와 acados 외 도구(Crocoddyl·OCS2 등)는 다루지 않았다(12.20). CasADi MX의 코드 생성과 2차 도함수 코드는 시험하지 않았다.
- 읽지 못한 것. CasADi·acados·HPIPM·BLASFEO·RTI·DCP 논문의 본문, BLASFEO 소스. CVXPY의 보조 변수 추가 원인은 소스를 읽지 않았다.
- 원인 미확인. 완전 응축 $N=160$의 QP 실패가 조건수 때문인지
(추정).time_sim통계가time_tot보다 큰 이유.acados_template패키지 문자열(0.5.1)과 태그(v0.6.0)의 불일치. - 코딩 표준. 생성 코드의 MISRA C 준수와 정적 분석 결과는 확인하지 않았다.
1.15.11 체크포인트·연구 질문
체크포인트:
- 직접 다중 사격으로 OCP를 단계 구조의 NLP로 기술하고 변수·제약의 블록 구조를 설명할 수 있는가.
- 계산 그래프에서 공통 부분식 공유와 구조적 0이 도함수 코드의 크기를 어떻게 바꾸는지 설명할 수 있는가.
- 순방향·역방향 모드의 실행 횟수를 입력·출력 차원에서 정할 수 있는가.
- 리카티 재귀와 밀집 KKT의 연산 횟수 증가율이 다른 이유를 설명할 수 있는가.
- SQP-RTI의 준비·피드백 단계가 각각 무엇을 계산하는지 말할 수 있는가.
- 식이 DCP인지 판정하고 위반을 DCP를 만족하는 형태로 고칠 수 있는가.
- 생성 코드의 호출 구간 할당과 생성 구간 할당을 구분해 계수할 수 있는가.
- 두 도구의 해를 비교하기 전에 비용 스케일링·적분기·정확도 설정을 맞출 수 있는가.
연구 질문(47.12에 수록한다):
- 생성된 임베디드 해법기의 최악 실행 시간을 정적으로 상한하는 방법은 QP 반복 수를 어떻게 다루는가.
- 열린 루프 불안정 모델에서 응축의 조건수 증가를 사전에 판정하는 기준은 무엇인가.
- 생성 코드의 컴파일러·플랫폼별 수치 차이가 폐루프 거동에 미치는 영향은 어느 수준인가.
1차 출처
- J. A. E. Andersson, J. Gillis, G. Horn, J. B. Rawlings, M. Diehl, 「CasADi: a software framework for nonlinear optimization and optimal control」, Math. Program. Comput. 11(1), 1–36(Crossref 기재 연도 2018), doi:10.1007/s12532-018-0139-4 — 서지만 확인.
- R. Verschueren 외, 「acados—a modular open-source framework for fast embedded optimal control」, Math. Program. Comput. 14(1), 147–183(Crossref 기재 연도 2021), doi:10.1007/s12532-021-00208-8; arXiv:1910.13753(초록 확인).
- G. Frison, M. Diehl, 「HPIPM: a high-performance quadratic programming framework for model predictive control」, IFAC-PapersOnLine 53(2), 6563–6569, 2020, doi:10.1016/j.ifacol.2020.12.073; arXiv:2003.02547(초록 확인).
- G. Frison, D. Kouzoupis, T. Sartor, A. Zanelli, M. Diehl, 「BLASFEO」, ACM Trans. Math. Softw. 44(4), 1–30, 2018, doi:10.1145/3210754 — 서지만 확인.
- M. Diehl, H. G. Bock, J. P. Schlöder 외, 「Real-time optimization and nonlinear model predictive control of processes governed by differential-algebraic equations」, J. Process Control 12(4), 577–585, 2002, doi:10.1016/s0959-1524(01)00023-3 — 서지만 확인.
- S. Gros 외, 「From linear to nonlinear MPC: bridging the gap via the real-time iteration」, Int. J. Control 93(1), 62–80, doi:10.1080/00207179.2016.1222553 — 서지만 확인.
- H. G. Bock, K. J. Plitt, 「A multiple shooting algorithm for direct solution of optimal control problems」, IFAC Proc. Vol. 17(2), 1603–1608, 1984, doi:10.1016/s1474-6670(17)61205-9 — 서지만 확인.
- M. Grant, S. Boyd, Y. Ye, 「Disciplined convex programming」, Nonconvex Optimization and Its Applications 시리즈, 155–210, Springer, doi:10.1007/0-387-30528-9_7 — 서지만 확인.
- A. Agrawal, R. Verschueren, S. Diamond, S. Boyd, 「A rewriting system for convex optimization problems」, J. Control Decis. 5(1), 42–60, 2018, doi:10.1080/23307706.2017.1397554 — 서지만 확인.
- S. Diamond, S. Boyd, 「CVXPY: a Python-embedded modeling language for convex optimization」, arXiv:1603.00943 — 서지만 확인(학술지 게재 사항은
(확인 필요)). - A. Agrawal 외, 「Differentiable convex optimization layers」, arXiv:1910.12430 — 서지만 확인. DPP 규칙과의 관계는
(확인 필요). - B. Stellato 외, 「OSQP: an operator splitting solver for quadratic programs」, Math. Program. Comput. 12(4), 637–672, 2020, doi:10.1007/s12532-020-00179-2 — 서지만 확인.
- M. Schaller, G. Banjac, S. Diamond, A. Agrawal 외, 「Embedded code generation with CVXPY」, IEEE Control Syst. Lett. 6, 2653–2658, 2022, doi:10.1109/lcsys.2022.3173209 — 서지만 확인.
- A. Griewank, A. Walther, 『Evaluating Derivatives』, SIAM, 2008, doi:10.1137/1.9780898717761 — 서지만 확인
(확인 필요: 장·정리 번호). - 소스·문서(조회·실행 2026-11-03): CasADi 사용자 안내서(web.casadi.org/docs),
acados/acadosv0.6.0(README·docs·acados_template·예제·HPIPM 헤더),casadi3.8.2·cvxpy1.9.3·cvxpygen1.0.2(PyPI, 실행).
다음 장. 1.16는 최적화 IV로, 도함수를 사용할 수 없거나 비용이 큰 문제를 위한 무미분·표본 기반 최적화를 다룬다. 베이지안 최적화·CMA-ES·교차 엔트로피 방법과 MPPI의 수학적 기초를 정리하고, 이 장의 기울기 기반 해법과의 적용 조건을 구분한다.