<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://jiharangrang.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jiharangrang.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-04-04T19:05:04+09:00</updated><id>https://jiharangrang.github.io/feed.xml</id><title type="html">Harang Robotics Log</title><subtitle>Control · Dynamics · Robotics · Vision</subtitle><author><name>Harang Ji</name></author><entry><title type="html">[기초] Action, Target, Reward 비교 실험</title><link href="https://jiharangrang.github.io/robotics/2026/04/04/modern-robotics-rl-action-target-reward-comparison.html" rel="alternate" type="text/html" title="[기초] Action, Target, Reward 비교 실험" /><published>2026-04-04T00:00:00+09:00</published><updated>2026-04-04T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/2026/04/04/modern-robotics-rl-action-target-reward-comparison</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/2026/04/04/modern-robotics-rl-action-target-reward-comparison.html"><![CDATA[<p>코드베이스는 <a href="https://github.com/jiharangrang/Modern_Robotics/tree/main/week6_extended">여기</a>에 정리해 두었다.</p>

<p>이전 포스팅에서는 custom env를 만들고, baseline을 두고, PPO가 실제로 학습되는지까지는 확인했다. 그 과정만으로도 강화학습을 처음 붙여보는 경험은 충분히 할 수 있었지만, 실험을 더 해보려고 하니 아쉬운 점도 분명히 보였다.</p>

<p>예를 들면 이런 것들이었다.</p>

<ul>
  <li>PPO를 한 가지 설정으로만 돌려서, 왜 그 설정을 썼는지 비교하기 어려웠다.</li>
  <li>target을 다르게 주면 어떤 차이가 나는지 더 체계적으로 보고 싶었다.</li>
  <li>결과가 한 번 잘 나온 것인지, 여러 seed에서도 비슷한지 보고 싶었다.</li>
  <li>학습 때 보던 target과 안 보던 target을 나눠서 평가해 보고 싶었다.</li>
</ul>

<p>그래서 이번 포스팅은 이전 구조를 지우거나 고치는 대신, 그 위에 조금 더 연구처럼 실험할 수 있는 구조를 새로 만든 버전이라고 생각하고 시작했다.</p>

<h2 id="목표">목표</h2>

<p>이번 확장판에서 먼저 해보고 싶은 목표는 아래 네 가지였다.</p>

<ol>
  <li>이전 포스팅에서 만들었던 2-link leg RL 문제를 그대로 유지하면서, 실험 구조만 더 엄밀하게 바꿔 보기</li>
  <li>목표 각도 변화량 방식과 직접 토크 방식을 같은 환경에서 비교할 수 있게 만들기</li>
  <li><code class="language-plaintext highlighter-rouge">fixed target</code>, <code class="language-plaintext highlighter-rouge">randomized target</code>, <code class="language-plaintext highlighter-rouge">held-out target</code>을 나눠서 정책이 어디까지 대응하는지 보기</li>
  <li>seed를 여러 개 돌리고 결과를 한 폴더 구조 안에서 정리해서, 한 번 잘 나온 실험과 반복해서 비슷하게 나오는 실험을 구분해 보기</li>
</ol>

<p>즉 이번 실험는 “PPO가 되느냐”만 보는 기록이 아니라, <strong>어떤 실험 조건을 추가로 확인하고 싶어서 확장했는지</strong>를 적어 가는 노트로 쓰려고 한다.</p>

<h2 id="왜-action-방식부터-다시-보려고-했나">왜 action 방식부터 다시 보려고 했나?</h2>

<p>이전 포스팅에서는 action을 목표 각도 변화량 방식 한 가지로만 두고 실험했다. 그 방식 자체는 그때는 합리적이었다. 직접 토크를 내게 하는 것보다 안정적일 것 같았고, 내가 이미 알고 있던 PD 제어와도 자연스럽게 이어졌기 때문이다.</p>

<p>그런데 확장판을 생각하면서 보니, 사실 그때는 “왜 이 action 방식이 더 낫다고 생각했는지”를 결과로 직접 비교해 본 적은 없었다. 그냥 첫 실험용으로 그렇게 정하고 들어간 셈이었다. 그래서 이번에는 같은 2-link 문제를 유지한 상태에서, action만 바꿨을 때 어떤 차이가 나는지 따로 보고 싶어졌다.</p>

<p>이번 확장판에서 비교하려는 action 방식은 두 가지다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">목표 각도 변화량 방식</code> (<code class="language-plaintext highlighter-rouge">pd_target_offset</code>)
현재 자세를 기준으로 목표 관절각을 조금 움직이게 한 뒤, 실제 토크는 PD 제어기가 만들게 하는 방식</li>
  <li><code class="language-plaintext highlighter-rouge">직접 토크 방식</code> (<code class="language-plaintext highlighter-rouge">direct_torque</code>)
policy가 낸 action을 거의 바로 토크로 해석하는 방식</li>
</ul>

<p>내가 먼저 확인하고 싶은 것은 아주 단순하다. 정말 목표 각도 변화량 방식이 학습하기 더 쉬운지, 아니면 생각보다 직접 토크 방식도 해볼 만한지 보는 것이다. 즉 이번 확장판에서는 action 설계도 그냥 받아들이는 것이 아니라, <strong>비교해 볼 수 있는 대상</strong>으로 두기로 했다.</p>

<h2 id="직접-토크-방식은-action이-어떻게-쓰이나">직접 토크 방식은 action이 어떻게 쓰이나?</h2>

<p>이전 포스팅에서는 action이 바로 토크가 되지 않았다. 먼저 목표 관절각을 조금 움직이는 명령으로 바꾸고, 실제 토크는 PD 제어기가 계산했다. 그래서 policy가 내는 값과 실제 물리에 들어가는 값 사이에 한 단계가 더 있었다.</p>

<p>그런데 직접 토크 방식에서는 그 중간 단계가 없다. policy가 낸 action을 거의 바로 토크로 바꿔서 동역학 계산에 넣는다.</p>

<p>예를 들어 policy가</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">action</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.6</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.4</span><span class="p">]</span>
</code></pre></div></div>

<p>를 냈고, 최대 토크가</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tau_limit</span> <span class="o">=</span> <span class="mf">40.0</span>
</code></pre></div></div>

<p>이라면 실제 토크는</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tau</span> <span class="o">=</span> <span class="mf">40.0</span> <span class="o">*</span> <span class="p">[</span><span class="mf">0.6</span><span class="p">,</span> <span class="o">-</span><span class="mf">0.4</span><span class="p">]</span>
    <span class="o">=</span> <span class="p">[</span><span class="mf">24.0</span><span class="p">,</span> <span class="o">-</span><span class="mf">16.0</span><span class="p">]</span>
</code></pre></div></div>

<p>처럼 바로 계산된다.</p>

<p>즉 이 방식에서는</p>

<ul>
  <li>첫 번째 관절에는 <code class="language-plaintext highlighter-rouge">24.0</code></li>
  <li>두 번째 관절에는 <code class="language-plaintext highlighter-rouge">-16.0</code></li>
</ul>

<p>의 토크가 바로 들어간다고 생각하면 된다.</p>

<p>내가 이해한 바로는, 이 방식은 중간 완충 장치가 없는 느낌에 가깝다. 목표 각도 변화량 방식에서는 action이 먼저 목표 자세 쪽으로 바뀌고 PD 제어기가 토크를 만들어 줬는데, 직접 토크 방식에서는 policy 출력이 거의 바로 물리계로 들어간다. 그래서 학습 초반에 이상한 action이 나오면 더 거칠게 움직일 수도 있을 것 같았다.</p>

<h2 id="액션-방식-비교-실험-결과">액션 방식 비교 실험 결과</h2>

<p>이번에는 target 조건은 둘 다 <code class="language-plaintext highlighter-rouge">randomized_train</code>으로 고정하고, action 방식만 바꿔서 비교했다.</p>

<ul>
  <li>목표 각도 변화량 방식 (<code class="language-plaintext highlighter-rouge">pd_target__randomized_train</code>)</li>
  <li>직접 토크 방식 (<code class="language-plaintext highlighter-rouge">direct_torque__randomized_train</code>)</li>
</ul>

<p>각 실험은 seed <code class="language-plaintext highlighter-rouge">7</code>, <code class="language-plaintext highlighter-rouge">11</code>, <code class="language-plaintext highlighter-rouge">21</code>로 돌렸고, 각 seed마다 <code class="language-plaintext highlighter-rouge">100000</code> step 학습했다. 그 뒤 run별 평가를 돌리고, 마지막에 aggregate 결과를 모아서 비교했다.</p>

<p>여기서 내가 처음 헷갈렸던 건 <code class="language-plaintext highlighter-rouge">primary</code>와 <code class="language-plaintext highlighter-rouge">held-out</code>이라는 말이었다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">primary</code> 평가는 학습 때와 같은 방식으로 목표를 다시 랜덤하게 뽑아서 보는 평가다(이전에 사용한 방식).</li>
  <li><code class="language-plaintext highlighter-rouge">held-out</code> 평가는 학습 때 일부러 따로 쓰지 않은 고정 목표 25개에서 따로 보는 평가다.</li>
</ul>

<p>즉 <code class="language-plaintext highlighter-rouge">primary</code>는 “학습하던 방식의 문제에서는 잘 되나?”를 보는 것이고, <code class="language-plaintext highlighter-rouge">held-out</code>은 “처음 보는 평가용 목표들에서도 어느 정도 되나?”를 보는 것이다.</p>

<h3 id="aggregate-숫자부터-보면">aggregate 숫자부터 보면</h3>

<table>
  <thead>
    <tr>
      <th>실험</th>
      <th style="text-align: right">primary best success rate</th>
      <th style="text-align: right">held-out success rate</th>
      <th style="text-align: right">primary best mean final distance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목표 각도 변화량 방식</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.538</code></td>
    </tr>
    <tr>
      <td>직접 토크 방식</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.635</code></td>
    </tr>
  </tbody>
</table>

<p>결론은 아주 단순했다. 같은 target 조건에서는 직접 토크 방식보다 목표 각도 변화량 방식이 훨씬 잘 학습됐다.<br />
직접 토크 방식은 seed를 세 개 돌렸는데도 success가 한 번도 나오지 않았다.</p>

<h3 id="성공률-그래프를-보면">성공률 그래프를 보면</h3>

<p>성공률 그래프를 보면 차이가 바로 보였다.</p>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/success_rate_bar.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/direct_torque__randomized_train/aggregate_plots/success_rate_bar.png" width="48%" />
</p>

<ul>
  <li>목표 각도 변화량 방식은 primary와 held-out 둘 다 성공률이 대략 <code class="language-plaintext highlighter-rouge">30%</code> 근처였다.</li>
  <li>직접 토크 방식은 primary도 <code class="language-plaintext highlighter-rouge">0%</code>, held-out도 <code class="language-plaintext highlighter-rouge">0%</code>였다.</li>
</ul>

<p>즉 직접 토크 방식은 학습 때와 같은 방식으로 랜덤 목표를 다시 뽑아도 잘 못 갔고, 학습 때 따로 쓰지 않은 held-out 목표들에서도 마찬가지였다.</p>

<h3 id="최종-거리-그래프를-보면">최종 거리 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/final_distance_boxplot.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/direct_torque__randomized_train/aggregate_plots/final_distance_boxplot.png" width="48%" />
</p>

<p>이 그래프는 박스플롯이라서 처음에는 조금 낯설었다. 나는 이걸 이렇게 읽었다.</p>

<ul>
  <li>네모 박스: 값들이 많이 모여 있는 구간</li>
  <li>가운데 주황선: 중앙값</li>
  <li>위아래 선: 크게 튀지 않은 범위</li>
  <li>바깥 동그라미: 다른 값들보다 유난히 크게 튀는 값</li>
</ul>

<p>그래서 여기서는 주황선이 낮을수록 보통 목표에 더 가깝게 끝났다고 볼 수 있다. 또 위쪽 동그라미가 많으면 어떤 episode들은 목표에서 훨씬 멀리 끝났다고 이해했다.</p>

<p>이번 결과에서는 목표 각도 변화량 방식 쪽이 전반적으로 더 낮은 거리 쪽에 분포했고, 중앙값도 직접 토크 방식보다 더 낮았다. 즉 보통의 episode를 놓고 보면 목표 각도 변화량 방식이 더 가까이 가는 편이었다.</p>

<p>다만 목표 각도 변화량 방식 쪽은 분포가 더 넓고, 위쪽으로 튀는 값도 보였다. 잘되는 episode에서는 확실히 더 잘 가지만, 어떤 경우에는 멀리서 끝나는 경우도 남아 있었다는 뜻이다.</p>

<p>반대로 직접 토크 방식은 분포가 조금 더 모여 있기는 했지만, 전체적으로 목표에서 더 먼 쪽에 머물렀다. 즉 아주 크게 망한다기보다, 애초에 목표까지 충분히 가까이 가지 못한 채 끝나는 경우가 많았다고 느꼈다.</p>

<h3 id="제어-effort-그래프를-보면">제어 effort 그래프를 보면</h3>

<p>제어 effort 그래프는 조금 흥미로웠다.</p>

<p>여기서 제어 effort는 각 episode 동안 나온 토크의 크기를 계속 더해서 만든 값이다.<br />
코드에서는</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">control_effort</span> <span class="o">+=</span> <span class="o">||</span><span class="n">tau</span><span class="o">||^</span><span class="mi">2</span> <span class="o">*</span> <span class="n">dt</span>
</code></pre></div></div>

<p>처럼 계산한다. 즉 토크를 얼마나 많이, 얼마나 오래 썼는지를 같이 반영한 값이다. 그래서 값이 크면 그 episode에서는 제어 입력을 더 많이 쓴 것으로 볼 수 있다.</p>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/control_effort_boxplot.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/direct_torque__randomized_train/aggregate_plots/control_effort_boxplot.png" width="48%" />
</p>

<ul>
  <li>목표 각도 변화량 방식이 control effort는 더 컸다.</li>
  <li>직접 토크 방식은 control effort가 더 낮았다.</li>
</ul>

<p>처음에는 힘을 덜 쓰는 쪽이 더 좋은 것 같기도 했지만, 이번 결과에서는 직접 토크 방식이 힘을 덜 쓴 대신 목표에 잘 도달하지도 못했다. 그래서 이번 비교에서는 “힘을 적게 썼다”보다 “실제로 목표에 갔는가”가 더 중요하게 느껴졌다.</p>

<h3 id="held-out-성공-heatmap을-보면">held-out 성공 heatmap을 보면</h3>

<p>held-out 성공 heatmap은 두 실험 차이를 더 직관적으로 보여줬다.</p>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/held_out_success_heatmap.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/direct_torque__randomized_train/aggregate_plots/held_out_success_heatmap.png" width="48%" />
</p>

<ul>
  <li>목표 각도 변화량 방식은 25개 고정 목표 중 일부에서는 분명히 성공했다.</li>
  <li>특히 몇몇 영역은 절반 이상 성공한 칸도 있었고, 어떤 칸은 거의 항상 실패하는 식으로 차이가 보였다.</li>
  <li>직접 토크 방식은 heatmap 전체가 전부 실패였다.</li>
</ul>

<p>즉 직접 토크 방식은 안 본 목표에서 약한 정도가 아니라, 이번 설정에서는 아예 제대로 대응하지 못한 쪽에 더 가까워 보였다.</p>

<h3 id="failure-reason도-같이-보면">failure reason도 같이 보면</h3>

<p>episode 종료 이유를 같이 세어 보니 차이가 더 분명했다.</p>

<ul>
  <li>목표 각도 변화량 방식
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 18</code>, <code class="language-plaintext highlighter-rouge">joint_limit 37</code>, <code class="language-plaintext highlighter-rouge">time_limit 5</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 23</code>, <code class="language-plaintext highlighter-rouge">joint_limit 45</code>, <code class="language-plaintext highlighter-rouge">time_limit 7</code></li>
    </ul>
  </li>
  <li>직접 토크 방식
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">joint_limit 21</code>, <code class="language-plaintext highlighter-rouge">velocity_limit 39</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">joint_limit 27</code>, <code class="language-plaintext highlighter-rouge">velocity_limit 48</code></li>
    </ul>
  </li>
</ul>

<p>내가 보기에는 이 차이가 꽤 중요했다. 뒤의 숫자들은 실패 횟수인데, 목표 각도 변화량 방식은 적어도 성공으로 끝난 episode가 있었고, 실패하더라도 주로 joint limit 쪽으로 끝났다. 반면 직접 토크 방식은 성공이 없었고, 특히 velocity limit으로 끝나는 경우가 많았다. 즉 직접 토크 방식 쪽이 더 거칠고 불안정하게 움직였다고 해석할 수 있었다.</p>

<h3 id="액션-방식-비교에-대한-결론">액션 방식 비교에 대한 결론</h3>

<p>이번 비교만 놓고 보면, 같은 <code class="language-plaintext highlighter-rouge">randomized_train</code> 조건에서는 직접 토크 방식보다 목표 각도 변화량 방식이 PPO 학습에 훨씬 더 유리했다. 성공률, final distance, held-out heatmap, failure reason을 같이 보면 직접 토크 방식은 아직 “더 어렵지만 해볼 만하다” 수준보다, 이번 설정에서는 거의 제대로 학습이 안 된 쪽에 더 가까웠다.</p>

<p>다만 여기서 바로 “직접 토크 방식은 항상 안 된다”라고까지 말할 수는 없다고 느꼈다. reward나 토크 제한, 학습 step, PPO 설정을 더 조정하면 달라질 수도 있기 때문이다. 그래도 적어도 이번 비교에서는, 예전에 내가 목표 각도 변화량 방식을 먼저 고른 이유가 결과로도 어느 정도 확인됐다고 볼 수 있었다.</p>

<h2 id="왜-target-조건도-따로-비교해-보려고-했나">왜 target 조건도 따로 비교해 보려고 했나?</h2>

<p>이전 포스팅에서도 목표 위치는 episode마다 계속 바뀌었지만, 그때는 “한 점만 배우는 정책”과 “여러 목표를 보면서 배우는 정책”을 따로 나눠서 본 적은 없었다. 그래서 학습이 되는지 정도는 볼 수 있었지만, target 조건이 바뀌면 정책 성격이 어떻게 달라지는지는 알 수 없었다.</p>

<p>이번 확장판에서는 이 부분을 조금 더 분리해서 보고 싶었다. 한 점만 반복해서 학습하면 그 한 점에서는 훨씬 잘 가게 되는지, 반대로 여러 목표를 랜덤하게 보면서 학습하면 안 본 목표들에서도 더 잘 대응하게 되는지를 확인하고 싶었다.</p>

<p>그래서 이번에는 action은 이미 더 잘 맞았던 목표 각도 변화량 방식으로 고정하고, target 조건만 바꿔 보기로 했다. 비교하려는 조건은 두 가지다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fixed_single</code>
학습 내내 사실상 한 점 목표만 보게 하는 조건</li>
  <li><code class="language-plaintext highlighter-rouge">randomized_train</code>
학습 중 목표가 계속 바뀌는 조건</li>
</ul>

<p>내가 특히 보고 싶은 것은 두 가지였다. 첫째, 한 점만 배운 정책이 자기 문제에서는 얼마나 강해지는지 보는 것이다. 둘째, 여러 목표를 보며 학습한 정책이 정말 held-out 목표들에서도 더 잘 되는지 확인하는 것이다. 즉 이번 target 비교는 “어떤 목표 분포로 학습시키는 게 더 좋은가?”를 보기 위한 실험이라고 정리할 수 있다.</p>

<h2 id="target-조건-비교-실험-결과">target 조건 비교 실험 결과</h2>

<p>이번에는 action은 둘 다 목표 각도 변화량 방식으로 고정하고, target 조건만 바꿔서 비교했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">pd_target__fixed_single</code></li>
  <li><code class="language-plaintext highlighter-rouge">pd_target__randomized_train</code></li>
</ul>

<p>각 실험은 seed <code class="language-plaintext highlighter-rouge">7</code>, <code class="language-plaintext highlighter-rouge">11</code>, <code class="language-plaintext highlighter-rouge">21</code>로 돌렸고, 각 seed마다 <code class="language-plaintext highlighter-rouge">100000</code> step 학습했다. 그 뒤 run별 평가를 돌리고, 마지막에 aggregate 결과를 모아서 비교했다.</p>

<p>여기서 <code class="language-plaintext highlighter-rouge">fixed_single</code>은 정말 한 점만 향하도록 학습시키는 조건이었다. 코드에서는 고정 관절각 <code class="language-plaintext highlighter-rouge">q_goal = [0.3, 0.8]</code>을 먼저 정하고, 그 자세에서의 발끝 위치를 목표점으로 썼다. 실제 목표 좌표는 대략 <code class="language-plaintext highlighter-rouge">(1.187, -1.409)</code>였다. 즉 <code class="language-plaintext highlighter-rouge">fixed_single</code> 실험은 학습 내내 이 한 점을 향해 가는 정책을 만든다고 이해하면 된다.</p>

<p>다만 이번 비교에서는 <code class="language-plaintext highlighter-rouge">primary</code>를 읽을 때 조금 조심해야 했다. <code class="language-plaintext highlighter-rouge">fixed_single</code>의 primary는 한 점 목표에서의 평가이고, <code class="language-plaintext highlighter-rouge">randomized_train</code>의 primary는 랜덤 목표들에서의 평가라서, primary 성능 차이에는 문제 난이도 차이도 함께 들어간다. 그래서 이번에는 primary는 “자기 문제에서 얼마나 잘했는가”를 보는 용도로 보고, 진짜 일반화 비교는 held-out 쪽을 더 중요하게 보기로 했다.</p>

<h3 id="aggregate-숫자부터-보면-1">aggregate 숫자부터 보면</h3>

<table>
  <thead>
    <tr>
      <th>실험</th>
      <th style="text-align: right">primary best success rate</th>
      <th style="text-align: right">held-out success rate</th>
      <th style="text-align: right">primary best mean final distance</th>
      <th style="text-align: right">held-out mean final distance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>fixed single</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">86.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">56.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.072</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.256</code></td>
    </tr>
    <tr>
      <td>randomized train</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.538</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.378</code></td>
    </tr>
  </tbody>
</table>

<p>처음에는 <code class="language-plaintext highlighter-rouge">randomized_train</code>이 여러 상황에 대해서 학습을 진행하니까 held-out에서 더 좋을 수도 있겠다고 생각했는데, 결과는 그렇지 않았다. <code class="language-plaintext highlighter-rouge">fixed_single</code>은 자기 문제에서는 당연히 훨씬 강했고, held-out에서도 오히려 더 높은 success rate와 더 낮은 final distance를 보였다.</p>

<h3 id="성공률-그래프를-보면-1">성공률 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__fixed_single/aggregate_plots/success_rate_bar.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/success_rate_bar.png" width="48%" />
</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fixed_single</code>은 primary에서 거의 <code class="language-plaintext highlighter-rouge">0.87</code>까지 올라갔다.</li>
  <li><code class="language-plaintext highlighter-rouge">randomized_train</code>은 primary가 <code class="language-plaintext highlighter-rouge">0.30</code> 정도였다.</li>
  <li>held-out에서도 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 <code class="language-plaintext highlighter-rouge">0.56</code>, <code class="language-plaintext highlighter-rouge">randomized_train</code>이 <code class="language-plaintext highlighter-rouge">0.31</code> 정도로 차이가 났다.</li>
</ul>

<p>primary는 문제 자체가 다르니 당연한 차이가 일부 섞여 있다고 봤다. 하지만 held-out에서도 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 더 높았다는 점은 예상과 조금 달랐다. 이번 설정에서는 여러 목표를 랜덤하게 보면서 학습한 것이 곧바로 더 좋은 일반화로 이어지지는 않았다.</p>

<h3 id="최종-거리-그래프를-보면-1">최종 거리 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__fixed_single/aggregate_plots/final_distance_boxplot.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/final_distance_boxplot.png" width="48%" />
</p>

<p>이 그래프도 박스플롯이라서 액션 비교 때와 같은 식으로 읽었다.</p>

<p>이번 결과에서는 <code class="language-plaintext highlighter-rouge">fixed_single</code> 쪽 박스와 주황선이 훨씬 아래에 있었다. 즉 대부분의 episode가 목표 근처에서 끝났고, 중앙값도 아주 낮았다. 한 점을 반복해서 학습한 만큼, 적어도 자기 문제에서는 훨씬 정확하게 가는 정책이 만들어졌다고 볼 수 있었다.</p>

<p><code class="language-plaintext highlighter-rouge">randomized_train</code>은 박스가 더 위쪽에 있고 분포도 넓었다. 어떤 경우에는 어느 정도 가까이 가기도 했지만, 전체적으로 보면 목표에서 더 먼 상태로 끝나는 episode가 훨씬 많았다. held-out 평균 final distance도 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 <code class="language-plaintext highlighter-rouge">0.256</code>, <code class="language-plaintext highlighter-rouge">randomized_train</code>이 <code class="language-plaintext highlighter-rouge">0.378</code>이라서, 이번 실험에서는 안 본 목표들에서도 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 더 유리했다.</p>

<h3 id="제어-effort-그래프를-보면-1">제어 effort 그래프를 보면</h3>

<p>제어 effort는 각 episode 동안 나온 토크의 크기를 계속 더해서 만든 값이다(액션과 동일).</p>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__fixed_single/aggregate_plots/control_effort_boxplot.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/control_effort_boxplot.png" width="48%" />
</p>

<p>이번에는 <code class="language-plaintext highlighter-rouge">fixed_single</code> 쪽이 control effort도 조금 더 낮았다. primary 기준 평균 control effort는 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 약 <code class="language-plaintext highlighter-rouge">201.1</code>, <code class="language-plaintext highlighter-rouge">randomized_train</code>이 약 <code class="language-plaintext highlighter-rouge">218.6</code>이었다. held-out에서도 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 약 <code class="language-plaintext highlighter-rouge">176.3</code>, <code class="language-plaintext highlighter-rouge">randomized_train</code>이 약 <code class="language-plaintext highlighter-rouge">200.3</code>으로 더 낮았다.</p>

<p>즉 이번 비교에서는 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 더 잘 갔을 뿐 아니라, 토크를 크게 낭비하지도 않았다. 여러 목표를 상대하는 쪽이 더 일반적일 것이라고 생각했지만, 현재 reward와 학습 step에서는 오히려 그만큼 더 어려운 문제를 풀게 되면서 제어 effort도 조금 더 커진 것으로 보였다.</p>

<h3 id="held-out-성공-heatmap을-보면-1">held-out 성공 heatmap을 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__fixed_single/aggregate_plots/held_out_success_heatmap.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train/aggregate_plots/held_out_success_heatmap.png" width="48%" />
</p>

<p>이 그래프는 두 실험 차이를 꽤 직관적으로 보여줬다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fixed_single</code>은 held-out 25개 목표 중 밝은 칸이 더 많았다.</li>
  <li><code class="language-plaintext highlighter-rouge">randomized_train</code>도 일부 칸에서는 성공했지만, 어두운 칸이 더 많고 전체적으로 덜 안정적이었다.</li>
</ul>

<p>조금 더 들여다보면 <code class="language-plaintext highlighter-rouge">fixed_single</code>의 밝은 칸들은 아무 데나 있는 것이 아니라, 학습 때 반복해서 보던 고정 목표와 발끝 위치가 비교적 가까운 쪽에서 더 많이 나타났다. 다만 여기서 주의할 점은, heatmap 축이 <code class="language-plaintext highlighter-rouge">(x, y)</code> 좌표가 아니라 <code class="language-plaintext highlighter-rouge">(q1, q2)</code> 관절각 grid라는 점이다. 그래서 “학습한 관절각 <code class="language-plaintext highlighter-rouge">q_goal = [0.3, 0.8]</code> 근처 칸만 밝다”처럼 단순하게 읽으면 맞지 않을 수 있다.</p>

<p>실제로는 같은 발끝 위치와 비슷한 Cartesian 목표를 여러 관절각 조합으로 만들 수 있기 때문에, 학습한 관절각과 joint-space에서 아주 가깝지 않아도 heatmap에서 밝게 나오는 칸이 있었다. 예를 들어 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 학습한 한 점 목표 <code class="language-plaintext highlighter-rouge">(1.187, -1.409)</code>와 Cartesian 거리상 가까운 held-out 목표들 중에는 <code class="language-plaintext highlighter-rouge">(q1, q2) = (0.5, 0.5)</code>, <code class="language-plaintext highlighter-rouge">(1.0, -0.5)</code>, <code class="language-plaintext highlighter-rouge">(0.0, 1.0)</code>, <code class="language-plaintext highlighter-rouge">(1.0, -1.0)</code> 같은 칸들이 있었고, 이런 칸들은 실제로 <code class="language-plaintext highlighter-rouge">1.0</code>으로 잘 성공했다.</p>

<p>즉 이번 heatmap은 “한 점만 학습한 정책이 안 본 모든 목표를 고르게 잘 간다”기보다, <strong>학습한 한 점과 위치상 비슷한 목표들에는 더 잘 퍼지고, 멀거나 joint limit에 불리한 영역에서는 여전히 약하다</strong>는 쪽으로 읽는 게 더 맞아 보였다. 이런 점을 보면, held-out 평가도 결국 학습한 목표와 완전히 무관한 것이 아니라, 학습했던 한 점의 영향이 남아 있는 상태에서 일반화를 보고 있다고 이해할 수 있었다.</p>

<h3 id="failure-reason도-같이-보면-1">failure reason도 같이 보면</h3>

<p>episode 종료 이유를 같이 세어 보니 차이가 더 분명했다.</p>

<ul>
  <li>fixed single
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 52</code>, <code class="language-plaintext highlighter-rouge">joint_limit 7</code>, <code class="language-plaintext highlighter-rouge">time_limit 1</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 42</code>, <code class="language-plaintext highlighter-rouge">joint_limit 23</code>, <code class="language-plaintext highlighter-rouge">time_limit 10</code></li>
    </ul>
  </li>
  <li>randomized train
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 18</code>, <code class="language-plaintext highlighter-rouge">joint_limit 37</code>, <code class="language-plaintext highlighter-rouge">time_limit 5</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 23</code>, <code class="language-plaintext highlighter-rouge">joint_limit 45</code>, <code class="language-plaintext highlighter-rouge">time_limit 7</code></li>
    </ul>
  </li>
</ul>

<p>이번 비교에서는 <code class="language-plaintext highlighter-rouge">randomized_train</code>이 더 자주 joint limit에 걸렸다. 즉 여러 목표를 동시에 상대하는 동안 아직 충분히 안정적인 움직임을 배우지 못한 경우가 더 많았다고 볼 수 있었다. 반대로 <code class="language-plaintext highlighter-rouge">fixed_single</code>은 한 점을 강하게 익힌 덕분인지 성공으로 끝나는 episode가 훨씬 많았다.</p>

<h3 id="target-조건-비교에-대한-결론">target 조건 비교에 대한 결론</h3>

<p>이번 결과만 놓고 보면, 현재 설정에서는 <code class="language-plaintext highlighter-rouge">randomized_train</code>이 <code class="language-plaintext highlighter-rouge">fixed_single</code>보다 더 좋은 일반화를 보여주지 못했다. 오히려 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 자기 문제에서는 훨씬 강했고, held-out에서도 더 높은 success rate와 더 낮은 final distance를 보였다.</p>

<p>다만 이걸 바로 “항상 한 점만 학습하는 것이 더 낫다”라고 해석할 수는 없다고 느꼈다. 지금은 success 정의도 단순하고, reward와 학습 step도 아직 초보적인 설정이다. 그래서 이번 결과는 “여러 목표를 랜덤하게 주면 무조건 더 좋은 일반화가 나온다”는 기대가 지금 설정에서는 성립하지 않았다는 정도로 이해하는 것이 더 맞아 보였다.</p>

<h2 id="왜-reward도-따로-비교해-보려고-했나">왜 reward도 따로 비교해 보려고 했나?</h2>

<p>액션과 target 비교까지 해 보니, 이제 남은 큰 축은 reward라고 느껴졌다. 이전 포스팅과 이번 확장 실험 초반에는 reward를 거의 그대로 두고 action 방식과 target 조건만 바꿔 봤다. 그 덕분에 “무엇을 바꿔서 어떤 차이가 났는지”는 보기 쉬웠지만, 반대로 reward 자체가 지금 결과를 얼마나 끌고 가고 있는지는 아직 제대로 본 적이 없었다.</p>

<p>내가 궁금했던 것은 이런 점이었다. 목표에 가까워지는 것만 강하게 밀어주는 reward를 쓰면 더 빨리 가기는 할지 몰라도 움직임이 거칠어질 수 있다. 반대로 속도 항이나 action 항까지 같이 두면 성공률은 조금 떨어져도 더 안정적이고 덜 과격한 움직임을 만들 수도 있다. 즉 reward를 어떻게 짜느냐에 따라, policy가 “무엇을 더 중요하게 배우는지”가 달라질 수 있다고 느꼈다.</p>

<p>그래서 다음에는 action은 이미 더 잘 맞았던 목표 각도 변화량 방식으로 고정하고, target도 비교 기준으로 쓰기 쉬운 조건으로 고정한 뒤, reward 구성만 바꿔서 보려고 한다. 내가 보고 싶은 것은 세 가지다.</p>

<ul>
  <li>위치 오차만 보는 reward가 정말 더 빨리 학습되는지</li>
  <li>속도 항을 넣으면 움직임이 얼마나 더 안정적으로 바뀌는지</li>
  <li>action 항까지 넣으면 제어 effort나 움직임 거칠기가 줄어드는지</li>
</ul>

<p>즉 이번 reward 비교는 “같은 문제를 풀더라도 policy가 어떤 성격의 움직임을 배우게 할지”를 reward가 어떻게 바꾸는지 확인하기 위한 실험이라고 정리할 수 있다.</p>

<h2 id="reward는-어떤-케이스로-나눴나">reward는 어떤 케이스로 나눴나?</h2>

<p>이번 reward 비교에서는 action과 target은 고정하고, reward 구성만 세 가지로 나눠 보기로 했다.</p>

<ul>
  <li>action: 목표 각도 변화량 방식</li>
  <li>target: <code class="language-plaintext highlighter-rouge">randomized_train</code></li>
</ul>

<p>즉 같은 문제를 풀게 두고, policy가 무엇을 더 중요하게 배우는지만 reward로 바꿔 보려는 실험이다.</p>

<p>이번에 나눈 reward 케이스는 아래 세 가지다.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">position only</code>
    <ul>
      <li>위치 오차 항만 남기고</li>
      <li>속도 항과 action 항은 <code class="language-plaintext highlighter-rouge">0</code>으로 둔다</li>
      <li>목표에 가까워지는 것만 가장 직접적으로 밀어주는 reward다</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>
    <ul>
      <li>위치 오차 항과 속도 항을 같이 둔다</li>
      <li>action 항은 <code class="language-plaintext highlighter-rouge">0</code>으로 둔다</li>
      <li>목표에 가는 것뿐 아니라 너무 빠르게 흔들리는 움직임도 조금 줄여 보려는 설정이다</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity + action</code>
    <ul>
      <li>위치 오차, 속도, action 항을 모두 둔다</li>
      <li>지금까지 기본으로 쓰던 reward와 같은 구성이다</li>
      <li>목표에 가되, 움직임과 입력이 너무 과격하지 않게 하려는 의도가 들어 있다</li>
    </ul>
  </li>
</ol>

<p>정리하면 이번 비교는 reward를 단순하게 할수록 더 빨리 가는 쪽으로 배울지, 아니면 속도와 action 항을 함께 둘 때 더 안정적이고 덜 거친 정책이 나오는지를 보려는 실험이다.</p>

<h2 id="reward-비교-실험-결과">reward 비교 실험 결과</h2>

<p>각 실험은 seed <code class="language-plaintext highlighter-rouge">7</code>, <code class="language-plaintext highlighter-rouge">11</code>, <code class="language-plaintext highlighter-rouge">21</code>로 돌렸고, 각 seed마다 <code class="language-plaintext highlighter-rouge">100000</code> step 학습했다. 그 뒤 run별 평가를 돌리고, 마지막에 aggregate 결과를 모아서 비교했다.</p>

<h3 id="aggregate-숫자부터-보면-2">aggregate 숫자부터 보면</h3>

<table>
  <thead>
    <tr>
      <th>실험</th>
      <th style="text-align: right">primary best success rate</th>
      <th style="text-align: right">held-out success rate</th>
      <th style="text-align: right">primary best mean final distance</th>
      <th style="text-align: right">held-out mean final distance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>position only</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">33.3%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">42.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.480</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.240</code></td>
    </tr>
    <tr>
      <td>position + velocity</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">23.3%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">18.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.560</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.485</code></td>
    </tr>
    <tr>
      <td>position + velocity + action</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30.7%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.538</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.378</code></td>
    </tr>
  </tbody>
</table>

<p>숫자만 먼저 보면 의외의 결과가 나왔다. 위치 오차만 보는 <code class="language-plaintext highlighter-rouge">position only</code>가 성공률과 held-out 최종 거리에서 제일 좋았다. 반대로 속도 항만 추가한 <code class="language-plaintext highlighter-rouge">position + velocity</code>는 세 실험 중 가장 성능이 낮았다. <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 그 중간쯤에 있었다.</p>

<h3 id="성공률-그래프를-보면-2">성공률 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_only/aggregate_plots/success_rate_bar.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel/aggregate_plots/success_rate_bar.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel_act/aggregate_plots/success_rate_bar.png" width="32%" />
</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">position only</code>는 primary보다 held-out success가 오히려 더 높았다.</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>는 primary와 held-out 둘 다 가장 낮았다.</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 둘 사이의 중간 정도였다.</li>
</ul>

<p>처음에는 reward를 단순하게 하면 거칠게 움직여서 성능이 낮아질 수도 있겠다고 생각했는데, 이번 설정에서는 적어도 success rate 기준으로는 그렇지 않았다. 오히려 위치 오차만 강하게 밀어준 쪽이 더 자주 목표에 도달했다. 반면 속도 항을 추가한 <code class="language-plaintext highlighter-rouge">position + velocity</code>는 기대했던 “조금 더 안정적인 성공”보다, 성공 자체가 줄어든 쪽에 더 가까워 보였다.</p>

<h3 id="최종-거리-그래프를-보면-2">최종 거리 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_only/aggregate_plots/final_distance_boxplot.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel/aggregate_plots/final_distance_boxplot.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel_act/aggregate_plots/final_distance_boxplot.png" width="32%" />
</p>

<p>이번 결과에서는 <code class="language-plaintext highlighter-rouge">position + velocity</code>의 중앙값이 가장 높아서, 보통의 episode를 놓고 보면 목표에서 가장 멀리 끝나는 편이었다. 즉 속도 항만 넣는다고 해서 이번에는 더 안정적으로 가까이 가는 정책이 만들어지지는 않았다.</p>

<p><code class="language-plaintext highlighter-rouge">position only</code>와 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 조금 흥미로웠다. 중앙값만 보면 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>이 더 낮아 보여서, 잘 되는 episode들만 놓고 보면 오히려 더 가까이 가는 경우도 있었다. 그런데 평균 final distance는 <code class="language-plaintext highlighter-rouge">position only</code>가 더 낮았다. 내가 보기에는 <code class="language-plaintext highlighter-rouge">position + velocity + action</code> 쪽이 보통은 잘 가더라도, 위쪽으로 크게 튀는 실패가 더 남아 있어서 평균이 올라간 것으로 보였다.</p>

<p>즉 최종 거리 그래프를 보면, <code class="language-plaintext highlighter-rouge">position only</code>는 성능이 좋은 episode와 아주 안 좋은 episode가 같이 있는 편이고, <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 중앙값은 낮지만 큰 실패가 아직 남아 있었다. <code class="language-plaintext highlighter-rouge">position + velocity</code>는 전체적으로 그 둘보다 더 멀리서 끝나는 경우가 많았다.</p>

<h3 id="제어-effort-그래프를-보면-2">제어 effort 그래프를 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_only/aggregate_plots/control_effort_boxplot.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel/aggregate_plots/control_effort_boxplot.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel_act/aggregate_plots/control_effort_boxplot.png" width="32%" />
</p>

<p>이 부분은 예상과 더 비슷했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">position only</code>는 control effort가 가장 컸다(y축 범위가 완전히 다르다).</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>는 control effort가 가장 낮았다.</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 그 중간이었다.</li>
</ul>

<p>primary 기준 평균 control effort는</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">position only</code>: 약 <code class="language-plaintext highlighter-rouge">293.3</code></li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>: 약 <code class="language-plaintext highlighter-rouge">185.5</code></li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity + action</code>: 약 <code class="language-plaintext highlighter-rouge">218.6</code></li>
</ul>

<p>였다. 여기서 내가 처음 헷갈렸던 점은, <code class="language-plaintext highlighter-rouge">action</code> 항으로 감점을 줬는데 왜 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>의 control effort가 <code class="language-plaintext highlighter-rouge">position + velocity</code>보다 더 큰가 하는 점이었다.</p>

<p>지금 코드에서는 이 둘이 같은 값을 보는 것이 아니었다. reward의 action 항은 정규화된 action 크기 <code class="language-plaintext highlighter-rouge">||a||^2</code>에 감점을 주고, control effort는 실제 토크의 누적량 <code class="language-plaintext highlighter-rouge">||tau||^2 * dt</code>를 더해서 계산한다. 그런데 목표 각도 변화량 방식에서는 action이 바로 토크가 아니라, 먼저 목표 각도 변화로 바뀐 뒤 PD 제어기가 실제 토크를 만든다. 그래서 action을 조금 더 얌전하게 써도, 실제 토크 누적량이 꼭 같이 줄어드는 것은 아니었다.</p>

<p>여기서 <code class="language-plaintext highlighter-rouge">action smoothness</code>는 매 step에서 action이 이전 step과 얼마나 달라졌는지를 제곱해서 계속 더한 값이다. 그래서 값이 작을수록 action이 덜 튀고 더 부드럽게 바뀐다고 이해하면 된다.</p>

<p>이번 결과를 보면 이 차이가 실제로 나타났다. <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 <code class="language-plaintext highlighter-rouge">position + velocity</code>보다 action smoothness는 더 낮았지만, episode 길이는 더 길고 success도 더 많았다. 즉 더 부드럽게 움직이기는 했지만, 목표에 가기 위해 토크를 더 오래 쓰면서 누적 control effort는 오히려 조금 올라간 것으로 보였다. 반대로 <code class="language-plaintext highlighter-rouge">position + velocity</code>는 control effort는 더 낮았지만, joint limit에 더 자주 걸리고 더 빨리 끝나는 경우가 많아서 누적 토크가 작게 나온 면도 있었다.</p>

<p>즉 이번 그래프는 “action 항을 넣었는데도 토크를 더 썼다”기보다, <strong>action penalty는 action 크기를 줄이는 항이고 control effort는 실제 토크 누적량이라서 서로 직접 같은 값이 아니며, 이번에는 더 오래 버티고 더 자주 성공한 쪽이 토크를 더 오래 써서 effort가 조금 커졌다</strong>고 해석하는 게 더 맞아 보였다.</p>

<h3 id="held-out-성공-heatmap을-보면-2">held-out 성공 heatmap을 보면</h3>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_only/aggregate_plots/held_out_success_heatmap.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel/aggregate_plots/held_out_success_heatmap.png" width="32%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-rl-action-target-reward-comparison/results/pd_target__randomized_train__reward_pos_vel_act/aggregate_plots/held_out_success_heatmap.png" width="32%" />
</p>

<p>heatmap을 보면 세 reward의 성격 차이가 더 분명했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">position only</code>는 밝은 칸과 어두운 칸이 강하게 갈렸다.</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>는 전체적으로 어두운 칸이 많았다.</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 <code class="language-plaintext highlighter-rouge">position only</code>보다 덜 극단적이지만, 더 넓게 퍼진 성공 칸들이 보였다.</li>
</ul>

<p>즉 <code class="language-plaintext highlighter-rouge">position only</code>는 몇몇 held-out 목표에서는 아주 잘 성공하지만, 안 되는 곳은 확실히 안 되는 식으로 조금 더 거칠고 불연속적인 느낌이었다. 반면 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 최고 성능 칸은 적더라도, 중간 정도로 성공하는 칸이 더 넓게 퍼져 있어서 전체적으로는 조금 더 고른 분포처럼 보였다.</p>

<p><code class="language-plaintext highlighter-rouge">position + velocity</code>는 이 둘 사이의 장점을 잘 못 살린 느낌이었다. 조심스럽게 움직이기는 하지만, 그렇다고 넓게 일반화된 성공 영역을 만들지도 못했다.</p>

<h3 id="failure-reason도-같이-보면-2">failure reason도 같이 보면</h3>

<p>episode 종료 이유를 같이 세어 보니 reward 차이가 또 다르게 보였다.</p>

<ul>
  <li>position only
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 20</code>, <code class="language-plaintext highlighter-rouge">joint_limit 29</code>, <code class="language-plaintext highlighter-rouge">time_limit 11</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 32</code>, <code class="language-plaintext highlighter-rouge">joint_limit 31</code>, <code class="language-plaintext highlighter-rouge">time_limit 12</code></li>
    </ul>
  </li>
  <li>position + velocity
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 14</code>, <code class="language-plaintext highlighter-rouge">joint_limit 44</code>, <code class="language-plaintext highlighter-rouge">time_limit 2</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 14</code>, <code class="language-plaintext highlighter-rouge">joint_limit 61</code></li>
    </ul>
  </li>
  <li>position + velocity + action
    <ul>
      <li>primary: <code class="language-plaintext highlighter-rouge">success 18</code>, <code class="language-plaintext highlighter-rouge">joint_limit 37</code>, <code class="language-plaintext highlighter-rouge">time_limit 5</code></li>
      <li>held-out: <code class="language-plaintext highlighter-rouge">success 23</code>, <code class="language-plaintext highlighter-rouge">joint_limit 45</code>, <code class="language-plaintext highlighter-rouge">time_limit 7</code></li>
    </ul>
  </li>
</ul>

<p>여기서 눈에 띈 건 <code class="language-plaintext highlighter-rouge">position + velocity</code>가 성공이 가장 적으면서도 joint limit 종료는 가장 많았다는 점이다. 원래는 속도 항을 넣으면 좀 더 안정적으로 움직일 수도 있겠다고 생각했는데, 이번 설정에서는 오히려 충분히 목표에 들어가기 전에 자세가 말려서 joint limit에 걸리는 경우가 더 많아진 것처럼 보였다.</p>

<p>반대로 <code class="language-plaintext highlighter-rouge">position only</code>는 time limit도 꽤 있었지만, 성공 수 자체는 가장 많았다. <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 success와 failure reason이 둘 사이의 중간쯤에 있었다.</p>

<h3 id="reward-비교에-대한-결론">reward 비교에 대한 결론</h3>

<p>이번 결과만 놓고 보면, 현재 설정에서는 reward를 단순하게 만든 <code class="language-plaintext highlighter-rouge">position only</code>가 가장 높은 success와 가장 좋은 held-out final distance를 보였다. 즉 적어도 이번 2-link reaching 문제에서는 “reward를 더 많이 다듬을수록 무조건 더 잘 된다”는 결과는 나오지 않았다.</p>

<p>다만 제어 effort까지 같이 보면 얘기가 조금 달라진다. <code class="language-plaintext highlighter-rouge">position only</code>는 가장 잘 가는 대신 토크를 가장 많이 썼고, heatmap도 더 끊겨있었다. 반대로 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>은 success 자체는 조금 낮지만, effort는 줄고 held-out heatmap은 조금 더 고르게 퍼지는 모습이 있었다.</p>

<p>그래서 내 느낌으로는 이번 reward 비교는 이렇게 정리하는 게 맞다.</p>

<ul>
  <li>pure success만 보면 <code class="language-plaintext highlighter-rouge">position only</code>가 가장 강했다</li>
  <li>effort와 분포의 균형까지 보면 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>이 더 절충적인 선택처럼 보였다</li>
  <li><code class="language-plaintext highlighter-rouge">position + velocity</code>만 넣은 버전은 이번 설정에서는 장점이 가장 약했다</li>
</ul>

<h2 id="최종-결론">최종 결론</h2>

<p>이번 확장 포스팅의 목표는 이전 포스팅에서 만들었던 2-link leg RL 문제를 그대로 유지하면서, 실험 구조를 더 엄밀하게 바꿔 보는 것이었다. 처음에 내가 세운 큰 목표는 네 가지였다.</p>

<ol>
  <li>기존 문제를 유지한 채 연구처럼 비교 가능한 구조를 만들기</li>
  <li>action 방식을 비교해 보기</li>
  <li>target 조건을 비교해 보기</li>
  <li>seed를 여러 개 돌리고 결과를 aggregate로 정리해서 한 번 잘 나온 결과와 반복해서 비슷하게 나오는 결과를 구분해 보기</li>
</ol>

<p>지금 돌아보면, 이 목표들은 거의 다 이룬 상태라고 느낀다. 이번 확장 실험에서는 이전 포스팅 내용을 건드리지 않고 별도 패키지로 구조를 다시 잡았고, env / baselines / train / utils / results를 분리했다. 그리고 seed <code class="language-plaintext highlighter-rouge">7</code>, <code class="language-plaintext highlighter-rouge">11</code>, <code class="language-plaintext highlighter-rouge">21</code>을 기준으로 같은 실험을 반복해서 돌리고, run별 결과와 aggregate 결과를 따로 저장하도록 정리했다. 적어도 “한 번 PPO를 돌려봤다” 수준이 아니라, <strong>같은 문제를 다른 조건에서 비교해 볼 수 있는 작은 실험 플랫폼</strong>은 만든 셈이다.</p>

<p>실험 결과를 기준으로 보면 action 비교에서는 목표 각도 변화량 방식이 직접 토크 방식보다 훨씬 안정적으로 학습됐다. 직접 토크 방식은 이번 설정에서 거의 성공을 만들지 못했고, velocity limit으로 자주 끝났다. 그래서 내가 처음 목표 각도 변화량 방식을 더 안전한 선택이라고 생각했던 이유가 결과로도 어느 정도 확인됐다.</p>

<p>target 비교에서는 처음 예상과 조금 다른 결과가 나왔다. 여러 목표를 랜덤하게 보면서 학습한 <code class="language-plaintext highlighter-rouge">randomized_train</code>이 held-out에서도 더 좋을 수도 있다고 생각했는데, 실제로는 <code class="language-plaintext highlighter-rouge">fixed_single</code>이 자기 문제에서는 물론 held-out에서도 더 높은 success rate와 더 낮은 final distance를 보였다. 이 결과를 보고, “randomized target이면 무조건 더 일반화된다”는 기대는 지금 설정에서는 성립하지 않는다는 점을 배웠다. 동시에 <code class="language-plaintext highlighter-rouge">fixed_single</code>의 일반화도 완전히 자유로운 것이 아니라, 학습한 한 점과 위치상 비슷한 held-out 목표들에 더 잘 퍼지는 형태라는 점도 heatmap으로 확인할 수 있었다.</p>

<p>reward 비교에서는 reward를 더 복잡하게 만든다고 무조건 더 좋은 결과가 나오는 것은 아니라는 점이 보였다. 순수 success와 held-out final distance만 보면 <code class="language-plaintext highlighter-rouge">position only</code>가 가장 강했다. 하지만 제어 effort와 heatmap의 모양까지 같이 보면 <code class="language-plaintext highlighter-rouge">position + velocity + action</code>이 더 절충적인 선택처럼 보였다. 즉 이번 실험에서는 “무조건 정답 reward”를 찾았다기보다, reward를 어떻게 설계하느냐에 따라 정책이 어떤 성격을 갖게 되는지가 실제로 달라진다는 점을 확인한 것이 더 중요했다.</p>

<p>그래서 이번 확장 실험을 한 정리하면, <strong>2-link swing-foot reaching 문제에서 PPO 자체가 되는지 확인하는 단계를 넘어서, action / target / reward를 바꾸면 정책의 성능과 성격이 어떻게 달라지는지 비교해 본 과정</strong>이었다고 말할 수 있다. 적어도 지금까지는 계획했던 핵심 비교 축인 action, target, reward 세 가지를 모두 돌려 봤고, seed 반복과 held-out 평가까지 포함해서 결과를 정리했다는 점에서 이번 확장 목표는 대부분 달성했다고 생각한다.</p>

<p>다만 아직 남은 한계도 분명하다. success 정의는 여전히 “5cm 안에 한 번 들어오면 끝”이라서 안정적으로 멈췄는지는 보지 못한다. held-out도 평가용 고정 target 25개를 따로 둔 것은 맞지만, 학습 중 비슷한 위치가 우연히 나올 가능성까지 완전히 제거한 것은 아니다. 또 robustness 실험이나 노이즈, 모델 오차 같은 조건은 아직 보지 않았다. 그래서 이번 결과를 바로 “이 방식이 항상 정답이다”라고 일반화할 수는 없고, <strong>현재 설정 안에서 어떤 선택이 더 잘 맞았는지 확인한 1차 비교 실험</strong>으로 이해하는 것이 맞아 보인다.</p>

<p>그럼에도 불구하고 이번 확장판은 나한테 꽤 의미 있었다. 이전 포스팅이 “강화학습을 한 번 내 문제에 붙여보는 단계”였다면, 이번 확장 포스팅은 “같은 RL 문제를 어떤 기준으로 비교하고 해석해야 하는지 배우는 단계”였다고 느꼈다. 그래서 이번 실험은 단순히 결과 숫자를 더 많이 만든 것이 아니라, <strong>RL 프로젝트를 연구처럼 보이게 만드는 요소가 무엇인지 직접 체험해 본 기록</strong>이라고 정리하고 싶다.</p>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="RL" /><summary type="html"><![CDATA[코드베이스는 여기에 정리해 두었다.]]></summary></entry><entry><title type="html">[기초] 2-Link Leg를 RL 문제로 바꾸기</title><link href="https://jiharangrang.github.io/robotics/2026/04/02/modern-robotics-2link-leg-rl-ppo.html" rel="alternate" type="text/html" title="[기초] 2-Link Leg를 RL 문제로 바꾸기" /><published>2026-04-02T00:00:00+09:00</published><updated>2026-04-02T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/2026/04/02/modern-robotics-2link-leg-rl-ppo</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/2026/04/02/modern-robotics-2link-leg-rl-ppo.html"><![CDATA[<p>코드베이스는 <a href="https://github.com/jiharangrang/Modern_Robotics/tree/main/week6/projects">여기</a>에 정리해 두었다.</p>

<h2 id="목표">목표</h2>

<p>이번 주의 목표는 내가 만든 2-link 다리 동역학 문제를 RL 문제로 바꾸는 것이다.<br />
즉, 기존의 기구학/동역학/PD 제어 코드를 그대로 두고, 그 바깥을 Gymnasium 환경 형태로 감싸서 PPO가 학습할 수 있는 입력과 출력 구조를 만드는 것이 핵심이다.</p>

<h2 id="mdp-정의">MDP 정의</h2>

<p>강화학습을 공부하다 보면 가장 먼저 나오는 말 중 하나가 MDP였다. MDP는 강화학습 문제를 정리하는 가장 기본적인 틀이었다. 에이전트가 현재 상태를 보고 행동을 고르면, 환경이 다음 상태와 보상을 돌려주는 흐름을 MDP라는 형태로 표현하는 것이었다.</p>

<p>내가 이해한 기준으로 보면 MDP는 결국 다섯 가지로 정리된다. 현재 환경이 어떤 상태인지 나타내는 state, 에이전트가 선택하는 action, 그 행동 이후 상태가 어떻게 바뀌는지를 나타내는 transition, 그 행동이 얼마나 좋았는지를 알려주는 reward, 그리고 episode가 언제 끝나는지를 정하는 termination 조건이다. 그래서 이번 주에는 내 2-link 다리 문제를 이 틀에 맞춰 다시 적어보는 것부터 시작했다.</p>

<h3 id="state--observation--action--transition--reward--termination--return">State / Observation / Action / Transition / Reward / Termination / Return</h3>

<ul>
  <li>state: 내부 물리 상태는 <code class="language-plaintext highlighter-rouge">q, qdot</code>와 목표 발 위치 <code class="language-plaintext highlighter-rouge">target_pos</code>로 본다.</li>
  <li>observation: 정책에는 <code class="language-plaintext highlighter-rouge">[q1/pi, q2/pi, qdot1/qdot_max, qdot2/qdot_max, (x_foot-x_target)/reach, (y_foot-y_target)/reach]</code>만 준다.</li>
  <li>action: 정책은 각 관절의 목표 각도 변화량을 <code class="language-plaintext highlighter-rouge">[-1, 1]^2</code> 범위로 출력하고, env 내부에서 <code class="language-plaintext highlighter-rouge">q_des = clip(q + delta_q_max * action, q_limits)</code>로 바꾼다.</li>
  <li>transition: action을 받아 PD + gravity compensation으로 torque를 만들고, Week 4의 <code class="language-plaintext highlighter-rouge">step_dynamics</code>로 다음 <code class="language-plaintext highlighter-rouge">q, qdot</code>를 계산한다.</li>
  <li>reward: <code class="language-plaintext highlighter-rouge">-2.0 * dist^2 - 0.05 * ||qdot||^2 - 0.01 * ||action||^2</code>, 성공 반경 안에 들어오면 <code class="language-plaintext highlighter-rouge">+5.0</code>.</li>
  <li>termination: success, joint limit 위반, NaN, 속도 blow-up이면 episode를 종료하고, 시간 제한은 <code class="language-plaintext highlighter-rouge">truncated</code>로 따로 처리한다.</li>
  <li>return: 현재 시점부터 episode 종료까지의 할인 reward 합이다.</li>
</ul>

<h3 id="왜-state를-q-qdot-target_pos로-잡았나">왜 state를 <code class="language-plaintext highlighter-rouge">q, qdot, target_pos</code>로 잡았나?</h3>

<p>state는 이 환경이 다음 순간으로 넘어가는 데 필요한 최소 핵심 정보를 담고 있어야 한다.<br />
여기서 <code class="language-plaintext highlighter-rouge">q</code>는 현재 다리 자세, <code class="language-plaintext highlighter-rouge">qdot</code>는 현재 운동 상태, <code class="language-plaintext highlighter-rouge">target_pos</code>는 이번 episode에서 발이 가야하는 목표점 좌표이다.</p>

<p>이 문제는 발을 목표점으로 보내는 문제이기 때문에, 목표 위치 정보가 꼭 필요했다.<br />
목표가 없으면 로봇이 어디로 가야 하는지 정할 수 없어서 <code class="language-plaintext highlighter-rouge">q</code>와 <code class="language-plaintext highlighter-rouge">qdot</code>만으로는 이 문제를 설명하기 어려웠다.</p>

<p>결국 이 세 가지가 있어야 다음 상태를 계산할 수 있고, 발끝-목표 오차 기반 reward도 정의할 수 있다.<br />
그래서 이번 환경에서는 <code class="language-plaintext highlighter-rouge">q, qdot, target_pos</code>를 이 문제를 설명하는 최소한의 state로 봤다.</p>

<h3 id="왜-observation은-state를-그대로-쓰지-않았나">왜 observation은 state를 그대로 쓰지 않았나?</h3>

<p>observation은 정책이 실제로 보는 입력이다.<br />
정책이 꼭 state 전체를 원시 형태로 받아야 하는 것은 아니기 때문에, 이번에는 학습하기 좋은 형태로 가공해서 넣었다.</p>

<p>현재 자세와 움직임은 <code class="language-plaintext highlighter-rouge">q</code>, <code class="language-plaintext highlighter-rouge">qdot</code>로 표현하고, 목표 정보는 절대 좌표 대신 발끝과 목표 사이의 상대 오차로 바꿨다.<br />
나는 이 값이 “지금 발이 목표에서 얼마나 떨어져 있는지”를 더 바로 보여준다고 느꼈고, 목표 좌표를 그대로 넣는 것보다 이해하기 쉬웠다.</p>

<p>또 각 항을 기준값으로 나눠 정규화했다.<br />
각도, 속도, 위치 오차는 크기와 단위가 서로 달라서 그대로 넣으면 어떤 값은 너무 크고 어떤 값은 너무 작게 들어간다. 이렇게 되면 신경망이 입력 값들을 균형있게 보기 어렵고 숫자가 큰 입력이 더 강하게 작용하게 되어 학습이 잘 안 될 수 있다. 그래서 각 항을 기준값으로 나눠서 비슷한 크기로 맞췄다.</p>

<p>그래서 observation은 state를 단순 복사한 값이 아니라, <strong>정책이 학습하기 쉽도록 정리한 입력 표현</strong>이라고 할 수 있다.</p>

<h3 id="왜-action을-토크가-아니라-목표-각도-변화량으로-만들었나">왜 action을 토크가 아니라 목표 각도 변화량으로 만들었나?</h3>

<p>이번 action은 정책이 직접 토크를 내는 구조가 아니다.<br />
대신 정책은 <code class="language-plaintext highlighter-rouge">[-1, 1]</code> 범위의 명령을 내고, 그 명령은 “현재 관절 목표를 어느 방향으로 얼마나 움직일지의 정도”를 뜻한다.</p>

<p>먼저 env 안에서</p>

<p><code class="language-plaintext highlighter-rouge">q_des = q + delta_q_max * action</code></p>

<p>으로 목표 관절각을 만든다.<br />
그다음 저수준 PD + gravity compensation 제어기(4주차에 만든)가 실제 토크를 계산한다.</p>

<p>이렇게 한 이유는 직접적인 토크값보다 훨씬 안정적이기 때문이다.<br />
처음부터 정책이 곧바로 토크를 다루게 하면, 물리 모델 문제와 학습 불안정성이 생길 수 있다.</p>

<p>직접 토크가 더 어려운 이유는, policy가 낸 값이 거의 바로 물리계에 들어가기 때문이다.<br />
학습 초반에는 policy가 이상한 값을 자주 내는데, 그 값이 바로 토크가 되면 움직임이 너무 거칠어지거나 속도가 갑자기 커질 수 있다.</p>

<p>반면 지금 방식은 먼저 목표 각도를 만들고, 그다음 PD 제어기가 토크를 계산한다.<br />
중간에 한 번 정리되는 단계가 들어가기 때문에 상대적으로 다루기 쉽다.</p>

<p>이번 구조에서는 정책이 고수준 목표(어디로 얼마나 갈지)만 정하고, 실제 토크 생성은 이미 수학적인 PD 제어기가 맡는다.<br />
즉 이번에는 RL이 방향만 정하고, 실제 토크는 기존 제어기가 만들도록 역할을 나눴다.</p>

<h3 id="-1-1-action이-실제-토크가-되는-과정"><code class="language-plaintext highlighter-rouge">[-1, 1]</code> action이 실제 토크가 되는 과정</h3>

<p>예를 들어 policy가</p>

<p><code class="language-plaintext highlighter-rouge">action = [0.6, -0.4]</code></p>

<p>를 냈다고 하면<br />
이 값은 아직 토크가 아니라, “첫 번째 관절은 조금 더 앞으로, 두 번째 관절은 조금 덜 움직여라” 같은 정규화된 명령이다.</p>

<p>먼저 현재 자세 <code class="language-plaintext highlighter-rouge">q</code>와 <code class="language-plaintext highlighter-rouge">delta_q_max</code>를 이용해 목표 자세를 만든다.</p>

<p><code class="language-plaintext highlighter-rouge">q_des = q + delta_q_max * action</code></p>

<p>예를 들어 현재 <code class="language-plaintext highlighter-rouge">q = [0.20, -0.50]</code>, <code class="language-plaintext highlighter-rouge">delta_q_max = 0.35</code>라면</p>

<p><code class="language-plaintext highlighter-rouge">q_des = [0.41, -0.64]</code></p>

<p>가 된다.</p>

<p>그다음 PD 제어기가</p>

<p><code class="language-plaintext highlighter-rouge">tau = Kp(q_des - q) - Kd qdot</code></p>

<p>형태로 토크를 만든다.<br />
실제 코드에서는 여기에 현재 자세에서 필요한 중력 보상도 더해 주고, 마지막에 너무 큰 값은 제한값 안으로 잘라서 동역학 식에 넣는다.</p>

<p>결국 policy는 직접 토크를 계산하는 것이 아니라, 목표 자세를 조금씩 제안하고 실제 torque 생성은 PD 제어기가 맡는 구조이다.</p>

<h3 id="왜-내-동역학-코드는-rl에서-environment-transition이-되는가">왜 내 동역학 코드는 RL에서 environment transition이 되는가?</h3>

<p>RL에서 environment는 action을 받으면 다음 상태를 만들어야 한다.<br />
이번 문제에서는 그 역할을 하는 것이 바로 Week 4에서 만든 forward dynamics와 적분기다.</p>

<p>현재 <code class="language-plaintext highlighter-rouge">q, qdot</code>와 policy가 낸 action이 있으면, env 내부에서 먼저 torque를 만든다.<br />
그리고 그 torque를 가지고 <code class="language-plaintext highlighter-rouge">step_dynamics</code>를 호출하면 다음 <code class="language-plaintext highlighter-rouge">q, qdot</code>가 나온다.</p>

<p>즉 내가 전에 만들었던 동역학 코드가, RL에서는 다음 상태를 만들어 주는 부분으로 그대로 들어왔다고 볼 수 있다.</p>

<h3 id="왜-reward는-내가-진짜-원하는-것과-정확히-같지-않은가">왜 reward는 “내가 진짜 원하는 것”과 정확히 같지 않은가?</h3>

<p>내가 정말 원하는 것은 “발을 목표점으로 안정적으로 잘 보내는 것”이다.<br />
하지만 이것을 한 줄짜리 수식 하나로 정확히 쓰기는 어렵다.</p>

<p>그래서 reward는 보통 그 목표를 대신 표현하는 몇 가지 측정 가능한 항으로 나눈다.<br />
이번에는</p>

<ul>
  <li>목표와의 거리</li>
  <li>너무 큰 속도</li>
  <li>너무 큰 action</li>
</ul>

<p>세 가지를 사용했다.</p>

<p>그래서 reward는 “내가 진짜 원하는 행동” 그 자체라기보다, 그쪽으로 가도록 도와주는 점수라고 이해했다.</p>

<p>계수들도 어떤 정답이 있어서 정한 값은 아니다.<br />
이번에는 목표에 가까워지는 것을 가장 중요하게 두고 싶어서 거리 항의 비중을 가장 크게 뒀고, 속도와 action 크기는 움직임이 너무 거칠어지지 않게 하는 보조 항이라는 생각으로 더 작게 넣었다.</p>

<h3 id="termination은-왜-따로-정해야-하나">termination은 왜 따로 정해야 하나?</h3>

<p>termination은 episode가 언제 끝나는지 정하는 규칙이다.<br />
이걸 따로 정하지 않으면 agent는 언제 성공한 것인지, 언제 실패한 것인지, 언제 그냥 시간이 끝난 것인지 구분할 수 없다.</p>

<p>이번 환경에서는 목표 반경 안에 들어오면 success로 종료하고, joint limit을 넘거나 NaN이 나오거나 속도가 지나치게 커지면 failure로 종료한다.<br />
반면 step 수가 다 차서 끝나는 경우는 성공이나 실패라기보다 단순한 시간 종료이기 때문에 <code class="language-plaintext highlighter-rouge">truncated</code>로 따로 처리했다.</p>

<h2 id="baseline-계획">Baseline 계획</h2>

<p>PPO를 바로 돌리기 전에 baseline을 먼저 만든 이유는, 학습이 안 될 때 무엇이 문제인지 분리하기 위해서다.<br />
baseline을 먼저 잡는 것이 표준적인 방법인 것 같다.</p>

<ul>
  <li>random policy: action space에서 무작위 샘플</li>
  <li>zero policy: 항상 0 action</li>
  <li>IK policy: Week 3 numerical IK로 <code class="language-plaintext highlighter-rouge">q*</code>를 찾고, <code class="language-plaintext highlighter-rouge">(q* - q) / delta_q_max</code>를 action으로 변환</li>
</ul>

<p>핵심 체크는 <code class="language-plaintext highlighter-rouge">IK &gt; zero/random</code> 순서로 결과나 나오는지다.<br />
이 순서가 안 나오면 PPO를 의심하기 전에 env, reward, termination 등 환경 세팅을 의심해야 한다.</p>

<p>그래서 baseline은 단순 비교용이라기보다, 지금 환경이 제대로 만들어졌는지 확인하는 기준처럼 느껴졌다.</p>

<h2 id="ppoproximal-policy-optimization-계획">PPO(Proximal Policy Optimization) 계획</h2>

<p>이번 주의 첫 알고리즘으로 PPO를 선택했다.<br />
PPO가 연속적인 action 문제에 자주 쓰이고, 처음 실험해 보기에도 비교적 단순한 편이기 때문이다.<br />
policy를 너무 급하게 바꾸지 않고 조금씩만 업데이트해 가면서 reward가 더 좋아지는 방향으로 policy 자체를 직접 학습시키는 알고리즘이라고 이해했다.<br />
observation을 넣으면 action을 내고, 그걸 반복하면서 점점 나아지게 만드는 흐름으로 이해할 수 있어서 첫 알고리즘으로 보기 부담이 덜했다.</p>

<p>이번 실험에서는 두 단계를 나눴다.</p>

<ul>
  <li>built-in sanity check: <code class="language-plaintext highlighter-rouge">Pendulum-v1</code></li>
  <li>custom env 학습: <code class="language-plaintext highlighter-rouge">TwoLinkSwingEnv</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Pendulum-v1</code>를 넣은 이유는 내 커스텀 env 전에 RL 스택 자체가 도는지 확인하기 위해서다.<br />
즉 Pendulum에서는 되는데 내 env만 안 되면, 문제를 PPO가 아니라 내 환경 설계 쪽으로 좁혀 볼 수 있다.</p>

<p>실제 custom env 학습은 <code class="language-plaintext highlighter-rouge">PPO("MlpPolicy", ...)</code>를 사용했고, <code class="language-plaintext highlighter-rouge">EvalCallback</code>으로 best model을 따로 저장했다.<br />
평가는 같은 조건으로 20번 돌려서 평균 reward, success rate, final distance를 비교했다.</p>

<h2 id="실행-결과">실행 결과</h2>

<h3 id="baseline">Baseline</h3>

<p><code class="language-plaintext highlighter-rouge">week6/projects/runs/baselines/baseline_summary.json</code> 기준:</p>

<table>
  <thead>
    <tr>
      <th>Baseline</th>
      <th style="text-align: right">Mean reward</th>
      <th style="text-align: right">Success rate</th>
      <th style="text-align: right">Mean final distance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>random</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-176.79</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">5%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.554</code></td>
    </tr>
    <tr>
      <td>zero</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-160.79</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.659</code></td>
    </tr>
    <tr>
      <td>IK</td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-14.41</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">90%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.106</code></td>
    </tr>
  </tbody>
</table>

<p>이 숫자는 실행할 때마다 조금 달라질 수 있다.<br />
그래도 공통적으로는 IK baseline이 random이나 zero보다 훨씬 좋은 결과를 보였다.</p>

<p>이 결과에서 가장 먼저 눈에 들어온 것은 IK baseline이 확실히 좋았다는 점이다.<br />
적어도 내가 만든 환경에서, 손으로 만든 기준 정책은 목표를 꽤 잘 따라간다는 것은 확인할 수 있었다.</p>

<p align="center">
  <img src="/assets/posts/2026-04-04-modern-robotics-2link-leg-rl-ppo/1_baseline_ik.gif" style="width: 70%; max-width: 520px; height: auto;" />
</p>

<p>IK baseline rollout을 보면, 이 문제에서는 손으로 만든 기준 정책도 꽤 안정적으로 목표 쪽으로 수렴하는 편이라는 것을 직관적으로 볼 수 있었다.</p>

<p>베이스라인은 PPO 결과를 해석할 때 중요하다.<br />
만약 IK도 안 됐다면, PPO가 못 배우는 이유를 policy 탓으로 볼 수 없기 때문이다.</p>

<p>2링크 구조는 수학적으로 깔끔하게 답이 나오는 상황이기 때문에 PPO 보다 IK 베이스라인이 더 좋은 결과를 낼 것으로 예상했다.</p>

<h3 id="ppo-sanity-check">PPO sanity check</h3>

<p><code class="language-plaintext highlighter-rouge">week6/projects/runs/pendulum_ppo/summary.json</code> 기준, 짧은 3,000-step 학습에서:</p>

<ul>
  <li>random mean reward: <code class="language-plaintext highlighter-rouge">-1260.00</code></li>
  <li>trained mean reward: <code class="language-plaintext highlighter-rouge">-1253.60</code></li>
</ul>

<p>큰 차이는 아니지만, PPO 코드가 아예 안 돌아가는 상태는 아니라는 것은 확인할 수 있었다.<br />
즉 설치나 기본 학습 파이프라인 자체가 완전히 깨진 것은 아니었다.</p>

<h3 id="학습-step-수에-따른-경향">학습 step 수에 따른 경향</h3>

<p>이번에는 짧은 12,000-step 결과보다, 실제로 폴더로 남겨 둔 학습 run들을 step 수별로 비교해 보는 쪽이 더 낫다고 느꼈다.<br />
기준은 각 run 폴더 안의 <code class="language-plaintext highlighter-rouge">summary.json</code>이고, 여기서 <code class="language-plaintext highlighter-rouge">trained</code>의 mean reward, success rate, mean final distance를 비교했다.</p>

<p>아래 표의 숫자들은 학습 중에 바로 본 값이 아니라, 따로 만든 평가 환경(eval env)에서 다시 돌려 본 결과다.<br />
한 episode는 최대 150 step이고, 10 episode를 측정했다.<br />
목표 반경 5cm 안에 들어오면 성공으로 처리했다.<br />
mean final distance는 episode가 끝났을 때 발끝이 목표와 얼마나 떨어져 있었는지를 평균낸 값이다.</p>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th style="text-align: right">Trained mean reward</th>
      <th style="text-align: right">Trained success rate</th>
      <th style="text-align: right">Trained mean final distance</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">50000</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-34.91</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">30%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.415</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">300000</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-26.53</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">20%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.267</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">500000</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-25.06</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">20%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.219</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">5000000</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">-17.58</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">50%</code></td>
      <td style="text-align: right"><code class="language-plaintext highlighter-rouge">0.142</code></td>
    </tr>
  </tbody>
</table>

<p>random policy 쪽 숫자는 매번 조금씩 달랐지만, trained policy 쪽은 step 수를 늘릴수록 전반적으로 좋아지는 경향이 보였다.<br />
특히 mean final distance는 <code class="language-plaintext highlighter-rouge">0.415 -&gt; 0.267 -&gt; 0.219 -&gt; 0.142</code>로 계속 줄어들었다.</p>

<p>success rate는 중간에 <code class="language-plaintext highlighter-rouge">30% -&gt; 20% -&gt; 20%</code>처럼 깔끔하게 오르지는 않았다.<br />
그래서 RL에서는 step 수를 늘린다고 항상 모든 지표가 예쁘게 같이 오르는 것은 아니라는 것도 같이 보였다.</p>

<p>그래도 가장 긴 <code class="language-plaintext highlighter-rouge">5000000 step</code>에서는 success rate가 <code class="language-plaintext highlighter-rouge">50%</code>까지 올라갔고, final distance도 가장 작았다.<br />
지금까지 해 본 run들 중에서는 이 결과가 제일 좋았다.</p>

<p>
  <img src="/assets/posts/2026-04-04-modern-robotics-2link-leg-rl-ppo/2_success_curve.png" width="48%" />
  <img src="/assets/posts/2026-04-04-modern-robotics-2link-leg-rl-ppo/3_episode_best.gif" width="48%" />
</p>

<p>왼쪽은 <code class="language-plaintext highlighter-rouge">5000000 step</code> run에서 checkpoint별 success rate가 어떻게 바뀌는지 보여주는 그래프이고, 오른쪽은 같은 run에서 가장 잘 된 rollout 예시다. 숫자로만 볼 때보다, 학습이 진행되면서 실제 움직임도 어느 정도 목표 쪽으로 정리된다는 점을 더 직관적으로 볼 수 있었다.</p>

<h3 id="각-run-안에서의-checkpoint-변화">각 run 안에서의 checkpoint 변화</h3>

<p><code class="language-plaintext highlighter-rouge">evaluation_summary.json</code> 기준으로 보면, 각 run 안에서도 <code class="language-plaintext highlighter-rouge">initial -&gt; mid -&gt; best</code>가 어떻게 바뀌는지 볼 수 있었다.</p>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>Initial reward / success / dist</th>
      <th>Mid reward / success / dist</th>
      <th>Best reward / success / dist</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">50000</code></td>
      <td><code class="language-plaintext highlighter-rouge">-226.46 / 0% / 0.801</code></td>
      <td><code class="language-plaintext highlighter-rouge">-57.25 / 10% / 0.383</code></td>
      <td><code class="language-plaintext highlighter-rouge">-256.70 / 30% / 0.634</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">300000</code></td>
      <td><code class="language-plaintext highlighter-rouge">-226.46 / 0% / 0.801</code></td>
      <td><code class="language-plaintext highlighter-rouge">-51.01 / 25% / 0.464</code></td>
      <td><code class="language-plaintext highlighter-rouge">-77.41 / 15% / 0.527</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">500000</code></td>
      <td><code class="language-plaintext highlighter-rouge">-226.46 / 0% / 0.801</code></td>
      <td><code class="language-plaintext highlighter-rouge">-48.63 / 15% / 0.487</code></td>
      <td><code class="language-plaintext highlighter-rouge">-72.57 / 15% / 0.503</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">5000000</code></td>
      <td><code class="language-plaintext highlighter-rouge">-226.46 / 0% / 0.801</code></td>
      <td><code class="language-plaintext highlighter-rouge">-18.44 / 60% / 0.205</code></td>
      <td><code class="language-plaintext highlighter-rouge">-19.32 / 60% / 0.244</code></td>
    </tr>
  </tbody>
</table>

<p>여기서 느낀 점은 <code class="language-plaintext highlighter-rouge">best</code>라는 이름이 붙어 있어도, 나중에 다시 평가했을 때 항상 가장 좋은 숫자가 나오지는 않는다는 점이었다.<br />
학습 중에는 그 시점 eval 기준으로 제일 좋았던 모델이었지만, 나중에 다시 다른 episode들로 평가하면 <code class="language-plaintext highlighter-rouge">mid</code>가 더 좋아 보이는 경우도 있었다.</p>

<p>그래서 이번에는 “무조건 마지막이 최고”라기보다, <strong>step 수를 늘리면 좋아질 가능성은 있지만 중간 checkpoint도 꼭 같이 봐야 한다</strong>고 느꼈다.</p>

<h2 id="이번에-공부하면서-느낀-점">이번에 공부하면서 느낀 점</h2>

<p>이번 주에는 “PPO를 돌려봤다”보다, <strong>강화학습 실험 결과를 어떻게 읽어야 하는지</strong>를 조금 배운 느낌이 더 컸다.<br />
지금 단계에서 내가 정리한 아쉬운 점과 배운 점은 아래 다섯 가지다.</p>

<ol>
  <li>
    <p><strong>IK baseline은 여전히 강한 기준선이었다.</strong><br />
PPO가 분명히 학습되기는 했지만, 지금 2-link 목표점 문제에서는 내가 이미 알고 있는 IK 방식이 훨씬 안정적이었다. 그래서 RL이 항상 기존 방법보다 바로 좋은 것은 아니라는 점을 알게 됐다.</p>
  </li>
  <li>
    <p><strong>평가에는 생각보다 운 요소가 남아 있었다.</strong><br />
<code class="language-plaintext highlighter-rouge">best</code> 모델이라고 해도 다시 평가했을 때 <code class="language-plaintext highlighter-rouge">mid</code>보다 항상 더 잘 나오지는 않았다. 같은 코드라도 episode 샘플이 달라지면 숫자가 조금 바뀔 수 있어서, 결과를 너무 한 번에 단정하면 안 되겠다고 느꼈다.</p>
  </li>
  <li>
    <p><strong>지금 성공 기준은 꽤 단순하다.</strong><br />
현재는 목표 반경 5cm 안에 한 번 들어오면 성공으로 끝난다. 그래서 목표에 닿은 뒤 안정적으로 멈췄는지, 아니면 지나치며 흔들렸는지는 아직 보지 못하고 있다.</p>
  </li>
  <li>
    <p><strong>step 수를 늘린다고 항상 마지막 모델이 최고는 아니었다.</strong><br />
학습을 오래 돌리면 전반적으로는 좋아졌지만, 가장 좋은 성능은 중간 checkpoint에서 나오는 경우도 있었다. 그래서 마지막 모델만 보는 것보다 <code class="language-plaintext highlighter-rouge">initial</code>, <code class="language-plaintext highlighter-rouge">mid</code>, <code class="language-plaintext highlighter-rouge">best</code>를 같이 보는 게 필요하다는 걸 알게 됐다.</p>
  </li>
  <li>
    <p><strong>아직은 정말 엄밀한 테스트까지 한 것은 아니다.</strong><br />
train env와 eval env는 분리했지만, 목표 좌표는 학습 때와 같은 샘플링 규칙에서 다시 뽑아 쓴 구조였다. 즉 학습 때 일부러 빼 둔 held-out target 집합은 아직 없었고, 완전히 새로운 목표 집합이나 노이즈, 모델 오차 같은 테스트도 하지 않았다. 그래서 지금 결과는 “학습 파이프라인이 돌아가고, 실제로 좋아지기도 한다” 정도까지는 말할 수 있지만, 일반화나 강건성까지 분석하기에는 아직 멀었다고 느꼈다.</p>
  </li>
</ol>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="RL" /><summary type="html"><![CDATA[코드베이스는 여기에 정리해 두었다.]]></summary></entry><entry><title type="html">[기초] 강화학습으로 Cart-pole 제어하기</title><link href="https://jiharangrang.github.io/control/2026/03/26/cartpole_swingup.html" rel="alternate" type="text/html" title="[기초] 강화학습으로 Cart-pole 제어하기" /><published>2026-03-26T00:00:00+09:00</published><updated>2026-03-26T00:00:00+09:00</updated><id>https://jiharangrang.github.io/control/2026/03/26/cartpole_swingup</id><content type="html" xml:base="https://jiharangrang.github.io/control/2026/03/26/cartpole_swingup.html"><![CDATA[<p>코드베이스는 <a href="https://github.com/jiharangrang/dm_cartpole">여기</a>에 정리해 두었다.</p>

<h1 id="강화학습-워크플로우-배우기">강화학습 워크플로우 배우기</h1>

<h3 id="환경이란">환경이란?</h3>

<p>환경은 4개의 질문데 답하는 시스템이다.</p>

<ul>
  <li>지금 상태?</li>
  <li>지금 할 수 있는 일?</li>
  <li>방금 행동은 얼마나 좋았나?</li>
  <li>이번 판은 끝났나?</li>
</ul>

<p>이 프로젝트에서 환경은 <code class="language-plaintext highlighter-rouge">cartpole swingup</code> 이다.</p>

<p>환경 안에는 아래의 요소가 들어있다.</p>

<ul>
  <li>물리 상태
    <ul>
      <li>위치, 속도, 각도, 가속도 등</li>
    </ul>
  </li>
  <li>행동 입력
    <ul>
      <li>에이전트가 카트에 어느 방향으로 얼마나 힘을 줄지</li>
    </ul>
  </li>
  <li>보상 규칙
    <ul>
      <li>지금 행동이 목표에 얼마나 도움이 됐는지 점수로 환산</li>
    </ul>
  </li>
  <li>종료 조건
    <ul>
      <li>한 에피소드를 언제 끝낼지</li>
    </ul>
  </li>
</ul>

<p>한 판(에피소드)은 보통 이렇게 흘러간다.</p>

<ol>
  <li>환경을 초기화 한다.
    <ul>
      <li>어떤 초기 자세로 시작한다.</li>
    </ul>
  </li>
  <li>환경이 현재 상태를 준다.
    <ul>
      <li>이것이 <code class="language-plaintext highlighter-rouge">observation</code> 이다.</li>
      <li>에이전트는 내부 물리 전체를 직접 보는게 아니라, 환경이 건네준 관측값만 본다.</li>
    </ul>
  </li>
  <li>에이전트가 행동을 고른다.
    <ul>
      <li>이것이 <code class="language-plaintext highlighter-rouge">action</code> 이다.</li>
      <li>여기서는 카트를 얼마나 밀 것인가에 해당한다.</li>
    </ul>
  </li>
  <li>환경이 그 행동을 물리에 적용한다.
    <ul>
      <li>카트가 움직이고, 막대가 흔들리며 다음 상태가 만들어진다.</li>
    </ul>
  </li>
  <li>환경이 계산해서 에이전트에게 보상을 준다.
    <ul>
      <li>이것이 <code class="language-plaintext highlighter-rouge">reward</code> 이다.</li>
      <li>목표에 가까워졌으면 큰 점수, 아니면 작은 점수 또는 불리한 점수를 준다.</li>
      <li>에이전트는 결국 어떤 행동 패턴이 장기적으로 reward를 크게 만드는가를 배운다.</li>
    </ul>
  </li>
  <li>환경이 끝났는지 종료를 알려준다.
    <ul>
      <li>아직 진행중이면 2번부터 반복한다.</li>
    </ul>
  </li>
</ol>

<p>위 반복 하나를 하나의 <code class="language-plaintext highlighter-rouge">step</code> 이라고 한다.</p>

<h3 id="라이브러리끼리-어떻게-연결되는가">라이브러리끼리 어떻게 연결되는가?</h3>

<p>이 프로젝트는 3개의 라이브러리가 붙어있다.</p>

<ol>
  <li>dm_control
    <ul>
      <li>물리 엔진과 태스크를 제공한다.</li>
      <li>카트와 막대의 물리 상태를 계산</li>
      <li>행동 넣었을 대 다음 상태 계산</li>
      <li>reward 계산</li>
      <li>epsiode 관련 timestep 반환</li>
    </ul>
  </li>
</ol>

<p>-&gt; 세상이 어떻게 움직이는가를 담당한다.</p>

<p>하지만 자기 방식의 api를 사용하기 때문에 강화학습 라이브러리가 기대하는 Gymnasium 형식과는 다르다.</p>

<ol>
  <li>Gymnasium
    <ul>
      <li>환경 인터페이스의 표준이다.</li>
      <li>강화학습 코드 쪽에서는 보통 환경이 이런 형태여야 한다.
        <ul>
          <li>reset() 가능</li>
          <li>step(action) 가능</li>
          <li>observation_space, action_space 정의되어 있음</li>
        </ul>
      </li>
    </ul>
  </li>
</ol>

<p>-&gt; 환경은 이런 방식으로 말해야 한다는 약속이다.<br />
물리를 계산하는 것이 아니라 표준 규약만 제공한다.</p>

<ol>
  <li>Stable-Baselines3
    <ul>
      <li>학습 알고리즘 구현체이다.</li>
      <li>이 프로젝트에서는 <code class="language-plaintext highlighter-rouge">SAC</code> 를 사용한다.</li>
      <li>observation을 입력으로 받는다.</li>
      <li>action을 선택한다.</li>
      <li>환경과 상호작용하며 데이터를 모은다.</li>
      <li>그 데이터로 정책을 업데이트 한다.</li>
    </ul>
  </li>
</ol>

<p>-&gt; 어떻게 배울 것인가를 담당한다.</p>

<p>dm_control과 SB3 사이의 브리지 역할을 해주기 위해서 <code class="language-plaintext highlighter-rouge">wrapper</code> 라고 하는 어댑터 코드(<a href="../envs/dm_cartpole_swingup_env.py">wrapper</a>)를 작성해야한다.</p>

<p>시뮬레이터 + 표준 인터페이스 + 학습기 구조인 것이다.</p>

<h3 id="환경을-만드는-코드를-한-곳에서-관리하는-이유는">환경을 만드는 코드를 한 곳에서 관리하는 이유는?</h3>

<p>환경을 생성하는 과정을 한 코드에서 관리하는 이유는</p>

<ul>
  <li>학습용 환경</li>
  <li>평가용 환경</li>
  <li>render용 환경</li>
</ul>

<p>이 환경들에 같은 규칙이 적용되어야 하기 때문이다.<br />
나중에 seed나 설정이 미묘하게 달라지는 문제를 막을 수 있다.</p>

<h3 id="observationaction-space가-정의-되어야-하는-이유는">observation/action space가 정의 되어야 하는 이유는?</h3>

<p>SB3가 환경이 어떤 입력과 출력을 갖는지 알아야하기 때문이다.</p>

<p>observation이나 action이 몇 차원인지, 연속값인지 이산값인지, 최소/최대 범위와 같은 정보들이 필요하다.</p>

<h3 id="action을-왜--11-범위로-맞췄나">action을 왜 [-1,1] 범위로 맞췄나?</h3>

<p>이 프로젝트에서는 action이 [-1,1] 범위인데, 환경의 실제 액추에이터 범위와 학습 알고리즘이 다루기 편한 범위가 꼭 같지 않을수도 있다.<br />
그래서 wrapper가 중간에서</p>

<ul>
  <li>에이전트는 [-1,1] 범위의 action만 출력</li>
  <li>wrapper가 그걸 실제 dm_control action 범위로 변환</li>
</ul>

<p>이런 역할들을 한다.</p>

<h3 id="학습-루프가-어떻게-도는가">학습 루프가 어떻게 도는가?</h3>

<p>단순하게</p>

<ol>
  <li>환경 초기화</li>
  <li>현재 상태 받기</li>
  <li>행동 선택</li>
  <li>환경에 행동 적용</li>
  <li>다음 상태와 보상 받기</li>
  <li>그 경험으로 정책 업데이트</li>
  <li>반복</li>
</ol>

<p>실제 프로젝트에서는 실험 관리가 필요하다.</p>

<ul>
  <li>seed 고정</li>
  <li>학습용/평가용 env 분리</li>
  <li>중간 평가</li>
  <li>모델 저장</li>
  <li>로그 기록</li>
</ul>

<p>이 프로젝트의 코드 기준으로 진행 순서를 설명하자면</p>

<ol>
  <li>설정 읽기
    <ul>
      <li>configs/sac_cartpole_swingup.yaml 에서 설정을 읽는다.</li>
      <li>설정에는 이런 값이 있다.
        <ul>
          <li>seed</li>
          <li>총 학습 timestep 수</li>
          <li>learning rate</li>
          <li>batch size</li>
          <li>평가 주기</li>
          <li>저장 주기</li>
        </ul>
      </li>
    </ul>

    <p>이 파일은 한마디로 실험 조건표이다.</p>
  </li>
  <li>seed 고정
    <ul>
      <li>utils/seed.py 로 시드를 고정한다.
        <ul>
          <li>초기 상태가 달라지거나</li>
          <li>action 샘플링이 달라지거나</li>
          <li>학습 결과가 달라지는 것을 막을 수 있다.</li>
        </ul>
      </li>
      <li>seed를 고정하면 완전히 같지는 않아도 비슷한 조건으로 다시 실험 할 수 있다.</li>
    </ul>
  </li>
  <li>환경 만들기
    <ul>
      <li>utils/make_env.py 로 환경을 만든다.
        <ul>
          <li>학습용 환경
            <ul>
              <li>에이전트가 계속 경험을 쌓는 곳</li>
            </ul>
          </li>
          <li>평가용 환경
            <ul>
              <li>지금 실력이 어느정도인지 체크하는 곳</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>환경 점검
    <ul>
      <li>check_env와 random rollout을 한다.
        <ul>
          <li>알고리즘 문제</li>
          <li>환경 버그</li>
          <li>보통 두 가지 원인으로 RL이 실패하기 때문이다.</li>
        </ul>
      </li>
      <li>환경이 Gymnasuim 규약을 지켰는지 확인한다.</li>
    </ul>
  </li>
  <li>학습 루프 시작
    <ul>
      <li>환경이 reset() 되면</li>
      <li>observation 값이 나온다.</li>
    </ul>
  </li>
  <li>에이전트가 action을 고름
    <ul>
      <li>학습 초반에는 행동이 미숙해서 막대를 잘 세우지 못할 것이다.</li>
    </ul>
  </li>
  <li>환경이 그 action을 적용
    <ul>
      <li>action을 받아 실제 물리를 한 step 진행한다.</li>
      <li>결과로
        <ul>
          <li>다음 ovservation</li>
          <li>reward</li>
          <li>terminated</li>
          <li>truncated</li>
          <li>위 항목을 돌려준다.</li>
        </ul>
      </li>
      <li>에이전트 너가 방금 이렇게 행동 했더니 결과가 이렇다 하고 알려주는 단계이다.</li>
    </ul>
  </li>
  <li>경험을 저장
    <ul>
      <li>이 한 번의 환경과 에이전트의 상호작용이 학습 데이터가 된다.</li>
      <li>강화학습에는 보통 이런 형태의 경험이 쌓인다.
        <ul>
          <li>현재 상태</li>
          <li>행동</li>
          <li>보상</li>
          <li>다음 상태</li>
          <li>종료 여부</li>
        </ul>
      </li>
      <li>이걸 transition 이라고 하고 SAC는 이런 transition들을 replay buffer에 쌓아두고 학습에 사용한다.</li>
    </ul>
  </li>
  <li>모델 업데이트
    <ul>
      <li>충분한 데이터가 쌓이면, SAC가 그 데이터를 꺼내서 정책을 업데이트한다.</li>
      <li>쌓인 경험을 사용해 반복적으로 업데이트 한다.</li>
      <li>흐름
        <ul>
          <li>환경에서 경험 수집</li>
          <li>버퍼에 저장</li>
          <li>버퍼에서 샘플 뽑기</li>
          <li>신경망 업데이트</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>에피소드 종료 여부 확인
    <ul>
      <li>terminated나 truncated가 True면 한 판이 끝난다.</li>
      <li>그러면 다시 reset() 해서 새 판을 시작한다.</li>
      <li>반복한다.</li>
    </ul>
  </li>
</ol>

<p>한 스텝 단위 말고 큰 흐름을 보면</p>

<ol>
  <li>환경에서 데이터를 모은다.</li>
  <li>데이터로 정책을 학습한다.</li>
  <li>조금 더 나은 정책으로 다시 데이터를 모은다.</li>
  <li>다시 학습한다.</li>
</ol>

<p>이런 선순환이 계속 일어나는게 강화학습이다.</p>

<h3 id="중간-평가를-왜-하는가">중간 평가를 왜 하는가?</h3>

<ul>
  <li>utils/callbacks.py 에서 학습이 잘 되는지 중간중간 확인을 한다.</li>
  <li>중간 평가를 통해 알 수 있는 건
    <ul>
      <li>reward가 올라가고 있는지</li>
      <li>이번 모델이 이전보다 좋은지</li>
      <li>best model을 따로 저장해야 하는지</li>
    </ul>
  </li>
</ul>

<p>강화학습은 중간에 성능이 흔들릴 수 있어서, 마지막 모델이 항상 최고 모델은 아니다.<br />
따라서 <strong>마지막 모델</strong>과 <strong>최고의 모델</strong>을 둘 다 저장하는게 의미가 있다.</p>

<h3 id="왜-로그를-남기는가">왜 로그를 남기는가?</h3>

<p>로그로는 이런 것들을 확인한다.</p>

<ul>
  <li>평균 episode reward</li>
  <li>평가 reward</li>
  <li>학습 진행도</li>
  <li>TensorBoard 곡선</li>
</ul>

<p>지금 뭐가 일어나고 있는지를 나중에 볼 수 있게 하는 장치다.</p>

<h3 id="학습-결과를-읽는-방법">학습 결과를 읽는 방법</h3>

<p>강화학습은 겉으로는 돌아가도 실제도는 이상하게 배웠을 수 있다.<br />
예를 들어</p>
<ul>
  <li>reward는 조금 오르는데 실제 행동은 불안정함</li>
  <li>몇 번은 잘 되는데 평균적으로는 형편없음</li>
  <li>마지막 모델은 안 좋은데 중간 모델은 좋았음</li>
  <li>숫자는 괜찮은데 실제 목표 행동과 다름</li>
</ul>

<p>따라서 학습 결과는 <strong>숫자와 영상</strong>을 둘 다 봐야한다.</p>

<p>이 프로젝트에서는 evaluate.py와 play.py가 그 역할을 한다.</p>

<h4 id="왜-학습-결과를-따로-평가해야-하나">왜 학습 결과를 따로 평가해야 하나?</h4>

<p>학습중에도 reward 로그가 찍히긴 하지만 학습중 로그는 항상 신뢰할 수 없기 때문이다.</p>

<p>그래서 보통은 학습과 별도로 저장된 모델을 다시 불러와서 지금 이 정책을 시험 삼아 여러 판 돌리면 얼마나 잘하는지 측정한다.</p>

<h4 id="숫자로-볼-때-무엇을-봐야하나">숫자로 볼 때 무엇을 봐야하나?</h4>

<p>여러 episode를 돌린 뒤 통계를 낸다.</p>

<p>주요하게 볼 값은</p>

<ul>
  <li>Mean episode reward
    <ul>
      <li>평균적으로 얼마나 reward를 얻는지</li>
      <li>높아지면 대체로 성능이 좋아졌다고 볼 수 있다.</li>
    </ul>
  </li>
  <li>Std episode reward
    <ul>
      <li>표준편차(성능의 들쭉날쭉함)</li>
      <li>높다는 것은 어떤 판은 잘 하고 어떤 판은 못했다는 의미이다.</li>
    </ul>
  </li>
  <li>Min episode reward
    <ul>
      <li>최악의 에피소드에서 조차 괜찮은지 판단할 때 사용한다.</li>
    </ul>
  </li>
  <li>Max episode reward
    <ul>
      <li>최고점도 중요하지만 최저점도 같이 봐야한다.</li>
    </ul>
  </li>
  <li>Success rate
    <ul>
      <li>성공 기준이 있는 경우 얼마나 성공했는지 평균 reward 보다 직관적으로 알 수 있다.</li>
    </ul>
  </li>
</ul>

<h4 id="영상은-왜-같이-봐야하는가">영상은 왜 같이 봐야하는가?</h4>

<p>숫자는 요약값이라 실제 행동 패턴을 숨길 수 있다.<br />
그래서 영상에서 아래의 요소들을 확인해야한다.</p>

<ul>
  <li>막대를 실제로 위로 올리는가</li>
  <li>올린 뒤 안정적으로 유지하는가</li>
  <li>카트 움직임이 지나치게 거칠지 않은가</li>
  <li>목표 행동인가 꼼수인가</li>
</ul>

<p>강화학습에서는 reward hacking이라는 꼼수가 있을 수 있어서 사람이 보기에는 행동이 이상할 수 있다.</p>

<h4 id="좋은-결과의-기준">좋은 결과의 기준</h4>

<ul>
  <li>평균 reward가 일정 수준 이상</li>
  <li>표준 편차가 낮음</li>
  <li>성공률이 높음</li>
  <li>영상에서도 자연스러움</li>
</ul>

<h4 id="best-model과-latesdt-model을-나눠-저장하는-이유">best model과 latesdt model을 나눠 저장하는 이유?</h4>

<p>학습이 항상 단조롭게 좋아지지 않기 때문이다.<br />
예를 들어, 3k step에서는 잘하다가 5k step에서 오히려 나빠질 수 있다.</p>

<h4 id="결과-체크리스트">결과 체크리스트</h4>

<p>[ ] 평균 성능<br />
[ ] 변동성(표준 편차)<br />
[ ] 성공률<br />
[ ] 실제 영상<br />
[ ] best vs latest 비교</p>

<h3 id="앞으로-어디를-바꿔가며-실험해볼-수-있을까">앞으로 어디를 바꿔가며 실험해볼 수 있을까?</h3>

<p>이제 뭐를 바꿔보면 학습 결과가 달라지는지 생각해볼 단계이다.<br />
강화학습은 한 번 돌리고 끝나는 게 아니라, 문제 정의와 학습 조건을 조금씩 바꾸면서 관찰하는 과정이다.</p>

<p>이 프로젝트 기준으로는 5가지를 바꿔볼 수 있다.</p>

<ul>
  <li>observation
    <ul>
      <li>모든 관측이 필요할까?</li>
      <li>어떤 정보가 빠지면 학습이 어려워질까?</li>
      <li>어떤 정보를 주면 더 쉽게 배울까?</li>
      <li>이를 통해 에이전트가 문제를 풀기위해 어떤 정보가 필요한지를 배울 수 있다.</li>
      <li>현실 문제에서는 센서가 불완전한 경우가 많기 때문에 정보의 선정이 중요하다.</li>
    </ul>
  </li>
  <li>action
    <ul>
      <li>action의 범위를 바꾸면?</li>
      <li>연속 action과 이산 action을 바꾸면?</li>
      <li>행동 공간이 학습 난이도에 얼마나 영향을 주는가를 알 수 있다.</li>
    </ul>
  </li>
  <li>reward
    <ul>
      <li>막대가 위에 가까울수록 보상을 더 키우면?</li>
      <li>중심 근처에 카트가 있을수록 추가 보상</li>
      <li>action이 너무 크면 패널티</li>
      <li>성공 후 유지 시간에 보상</li>
      <li>reward를 어떻게 주느냐에 따라 에이전트가 완전히 다른 전략을 배울 수 있다.</li>
      <li>점수만 잘 따는 이상한 행동을 배울 수 있으니 주의하자.</li>
    </ul>
  </li>
  <li>episode/환경 조건
    <ul>
      <li>episode가 너무 짧으면 학습 전에 끝나지는 않을까?</li>
      <li>너무 길면 비효율적이지 않을까?</li>
      <li>노이즈 추가</li>
      <li>초기 상태 랜덤화</li>
      <li>관측 지연 추가</li>
      <li>Robust 제어 분야에서 중요해진다.</li>
    </ul>
  </li>
  <li>알고리즘 및 학습 파라미터
    <ul>
      <li>지금은 SAC를 사용하지만 PPO를 사용하면?</li>
      <li>config에서
        <ul>
          <li>learning_rate</li>
          <li>batch_size</li>
          <li>gamma</li>
          <li>tau</li>
          <li>total_timesteps</li>
          <li>이 값들을 바꾸면 학습 안정성이나 속도가 달라질 수 있다.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h4 id="실험을-하는-과정">실험을 하는 과정</h4>

<p>많이 바꾸는 것보다는 무엇을 왜 바꿨는지 기록하면서 비교하는 것이 중요하다.</p>

<ol>
  <li>
    <p>기준 실험 하나를 만든다.</p>
  </li>
  <li>
    <p>변수 하나만 바꾼다.</p>
  </li>
  <li>
    <p>다시 학습 시킨다.</p>
  </li>
  <li>
    <p>평가와 gif를 비교한다.</p>
  </li>
  <li>
    <p>어떤 변화가 있었는지 메모한다.</p>
  </li>
</ol>

<p>기준 : 현재 config</p>

<p>실험1 : max_episode_steps만 증가</p>

<p>실험2 : total_timesteps만 증가</p>

<p>이런 식으로 설계해야 원인이 무엇인지 보이게 된다.<br />
여러개를 한 번에 바꾸면 결과가 달라져도 이유를 알 수 없다.</p>

<h4 id="중요한-rl-실험-본질">중요한 RL 실험 본질</h4>

<ul>
  <li>어떤 정보를 보여줄지 정하기</li>
  <li>어떤 행동을 허용할지 정하기</li>
  <li>무엇을 보상할지 정하기</li>
  <li>얼마나 오래 학습시킬지 정하기</li>
  <li>결과를 다시 보고 수정 반복하기</li>
</ul>]]></content><author><name>Harang Ji</name></author><category term="Control" /><category term="RL" /><summary type="html"><![CDATA[코드베이스는 여기에 정리해 두었다.]]></summary></entry><entry><title type="html">LQR vs MPC : 제어 실패의 이유 차이가 뭘까</title><link href="https://jiharangrang.github.io/control/2026/02/19/lqr_mpc_rl-2.html" rel="alternate" type="text/html" title="LQR vs MPC : 제어 실패의 이유 차이가 뭘까" /><published>2026-02-19T00:00:00+09:00</published><updated>2026-02-19T00:00:00+09:00</updated><id>https://jiharangrang.github.io/control/2026/02/19/lqr_mpc_rl-2</id><content type="html" xml:base="https://jiharangrang.github.io/control/2026/02/19/lqr_mpc_rl-2.html"><![CDATA[<h2 id="동기">동기</h2>

<p>로봇 제어를 공부하면서 LQR-MPC 제어를 사용할 때, 막상 “각 제어 방식이 어느 지점에서 차이를 보이는지”를 내 손으로 검증해본 적은 없었다.</p>

<p>이번 프로젝트의 질문은 단순하다.</p>

<ol>
  <li>제약이 없을 때</li>
  <li>여러 제약으로 인한 포화가 추가되었을 때</li>
  <li>지연/노이즈 오차가 추가되었을 때</li>
</ol>

<p>각각에서 LQR, MPC는 어떤 실패모드로 무너지고, 무엇이 강건성을 만들어 내는가?</p>

<p>이를 위해 Mujoco의 표준 도립진자 환경에서 동일 조건으로 구현/비교하고, 결과를 재현 가능한 코드와 로그로 남긴다.</p>

<h2 id="실험-세팅">실험 세팅</h2>

<p>아래 카트-폴 모델을 사용하여 운동 방정식을 통해 수식을 작성하였다.</p>

<p><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/image.png" alt="image.png" /></p>

<p>$x : \text{카트 위치},\ \theta : \text{막대 각도(직립이 } \theta=0\text{)},\ u : \text{환경 action}$</p>

<p>$M : \text{카트 질량}$<br />
$m : \text{막대 질량}$<br />
$l : \text{힌지}\rightarrow\text{막대 COM 거리}$<br />
$I : \text{막대 COM 기준 관성(힌지 축 기준)}$<br />
$g : \text{중력가속도} $<br />
$F : \text{카트에 작용하는 수평 힘(외력)} $<br />
$F’ = \mathrm{gear}\,u \ \text{(제어 힘)}$</p>

<p>$\theta$ 가 가장 위험하니 $Q$ 에서 $\theta$ 에 가장 큰 가중치 80을 두었다.</p>

<p>$R$은 입력 제한이 있으니 과도한 입력을 줄이기 위해 0.1로 두었다.</p>

<p>$ Q=\begin{bmatrix}
  1.0 &amp; 0 &amp; 0 &amp; 0<br />
  0 &amp; 80.0 &amp; 0 &amp; 0<br />
  0 &amp; 0 &amp; 1.0 &amp; 0<br />
  0 &amp; 0 &amp; 0 &amp; 10.0
  \end{bmatrix}$</p>

<p>$R=\begin{bmatrix}
  0.1
  \end{bmatrix}$</p>

<p>모델은 이전 프로젝트에서 분석한 시뮬레이션 이산화 모델을 그대로 사용하였다.</p>

<hr />

<p><strong>아래 내용은 이전 프로젝트에서 발췌한 부분</strong></p>

<p>Mujoco에 들어가 있는 모델을 사용하여, 시뮬레이션 환경에 최적화 + 선형화된 $A_d^{fd}, B_d^{fd}$ 를 구했다.</p>

<p>관측 상태는 동일하게 $\mathbf{x}=[x,\theta,\dot{x},\dot{\theta}]$를 사용하며, 아래는 터미널에서 확인한 값이다.</p>

<ul>
  <li>이론 모델 (theory)
    <ul>
      <li>$A_d^{th}\approx$
\(\begin{bmatrix} 1.0000 &amp; -0.0023 &amp; 0.0400 &amp; -0.0000 \\ 0.0000 &amp; 1.0240 &amp; 0.0000 &amp; 0.0403 \\ 0.0000 &amp; -0.1171 &amp; 1.0000 &amp; -0.0023 \\ 0.0000 &amp; 1.2053 &amp; 0.0000 &amp; 1.0240 \end{bmatrix}\)</li>
      <li>$B_d^{th}\approx [\,0.006700\ -0.015800\ 0.335309\ -0.793131\,]^\top$</li>
      <li>$K^{th}\approx [\,-0.3554\ -7.1830\ -0.7326\ -1.6947\,]$</li>
    </ul>
  </li>
  <li>시뮬 수치 선형화 (FD)
    <ul>
      <li>$A_d^{fd}\approx$
\(\begin{bmatrix} 1.0000 &amp; -0.0023 &amp; 0.0399 &amp; 0.0001 \\ 0.0000 &amp; 1.0234 &amp; 0.0002 &amp; 0.0387 \\ 0.0000 &amp; -0.1123 &amp; 0.9967 &amp; 0.0053 \\ 0.0000 &amp; 1.1573 &amp; 0.0076 &amp; 0.9450 \end{bmatrix}\)</li>
      <li>$B_d^{fd}\approx [\,0.006652\ -0.015364\ 0.331717\ -0.760593\,]^\top$</li>
      <li>$K^{fd}\approx [\,-0.3706\ -7.9560\ -0.7996\ -1.6728\,]$</li>
    </ul>
  </li>
</ul>

<p><strong>발췌 부분 끝</strong></p>

<hr />

<h2 id="실험-결과">실험 결과</h2>

<p>본격적으로 LQR과 MPC를 직접 분석해보려고 한다.</p>

<h3 id="제약이-없는-경우">제약이 없는 경우</h3>

<p>선형 이산 시스템과 2차 비용에서, 제약이 없고 terminal cost가 DARE의 P인 경우,<br />
유한지평 MPC의 첫 입력은 무한지평 LQR의 입력과 일치해야한다.<br />
지금 실험은 제약/노이즈/지연 실험에서 나타나는 차이를 원인별로 분리하기 위해, 시뮬레이션 구현이 이 성질을 재현하는지 검증한다.</p>

<p><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_diff_10s.png" alt="이상적인 파이썬 계산 결과" /><br />
이상적인(무마찰, 무댐핑) 파이썬(sanity 체크용)에서 계산 결과</p>

<p><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_diff_amp280_10s_d5_applied.png" alt="실제 시뮬레이션 계산 결과" /><br />
실제 시뮬레이션(Mujoco)에서 계산 결과</p>

<p>동일 모델에 파이썬과 실제 Mujoco 시뮬레이션에 무제약을 적용했다.<br />
두 환경에서 모두 MPC에 입력 제약을 충분히 크게 설정하여 제약이 활성화되지 않도록 했을 때, MPC와 LQR의 제어 입력 차이(u_mpc - u_lqr)가 $10^{-8}$ 수준으로 수치오차 범위 안에서 일치했다.<br />
무제약 구간에서 MPC가 LQR과 동일한 입력을 내는지 확인해서, 앞으로의 실험에서 차이가 발생하면 그것이 제약이나 노이즈 때문임을 분리해서 해석할 수 있는 기준을 확립하였다.<br />
이제 MPC의 실질적인 이점은 제약이 있는 상황에서 나타남을 확인해 볼 차례이다.</p>

<h3 id="제약이-있는-경우">제약이 있는 경우</h3>

<h4 id="제약">제약</h4>

<p>제약을 크게 3가지로 두었다.</p>

<p>$x$ : 레일의 길이 제약<br />
$u$ : 입력의 제약<br />
$\Delta u$ : 입력 변화량의 제약</p>

<p>$u$ 와 $\Delta u$ 는 시뮬레이션 상황이지만, 모터가 낼 수 있는 힘 범위와 순간 힘이 변할 수 있는 양에 한계가 정해져 있다는 것을 반영하였다.</p>

<ol>
  <li>$u_{\min} \leq u \leq u_{\max}$,   where   $u_{\min} = -3$,   $u_{\max} = 3$</li>
  <li>$x_{\min} \leq x \leq x_{\max}$,   where   $x_{\min} = -1$,   $x_{\max} = 1$</li>
  <li>$\Delta u = 2.6$</li>
</ol>

<p><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_gap_vs_du.png" alt="success_gap" />
<img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/per_amp_success_vs_du.png" alt="Per_amp" /></p>

<p>$\Delta u$ 가 너무 작거나 너무 크면 이 amp 구간에서 LQR이랑 MPC가 둘 다 실패하거나(둘 다 0%), 둘 다 성공해서(둘 다 100%) 차이를 보여주기 어려웠다.<br />
그래서 먼저 amp 250~290처럼 결과가 갈리기 시작하는 전이 구간을 잡고, 그 안에서 $\Delta u$ 를 0.8~3.0으로 실험했다.<br />
위 두 그래프는 amp별, $\Delta u$별 성공률 전체 실험 결과를 보여준다.<br />
그 중에서 두 제어기의 성공률 차이가 가장 잘 드러나는 값 $\Delta u = 2.6$ 을 대표값으로 선택하였다.</p>

<h4 id="성공-판정-조건">성공 판정 조건</h4>

<p>외란이 가해진 이후</p>

<ol>
  <li>막대 각도가 $90^\circ$ 이내 유지</li>
  <li>카트가 레일 양 끝에 닿지 않음</li>
</ol>

<h3 id="결과">결과</h3>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_rate.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/theta_max_post.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/recovery_time.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_energy.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_timeseries_all_amps_seed0.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/mpc250.gif" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/mpc290.gif" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
</table>

<p>(1 - amp 250~290 성공률)<br />
(2 - amp 250~290 최대 각도)<br />
(3 - amp 250~290 회복 시간)<br />
(4 - amp 250~290 외란 이후 20 step동안 제어 입력 u)<br />
(5 - amp 270, 290 상태 시계열)<br />
(MPC amp 250 제어 성공)<br />
(MPC amp 290 제어 실패)</p>

<p>Outcome Rate 그래프(1)에서 amp 250~290 구간에서 LQR 제어는 amp 270까지 직립 유지에 성공하고 MPC 제어는 amp 290까지도 성공하는 것을 볼 수 있다.<br />
amp 275부터 LQR의 실패 원인은 $x$ fail이다.<br />
LQR은 제어에 현재 상태만 반영할 뿐, $x$ 나 $u$ 같은 제약을 사전에 고려하지 않는다.<br />
따라서 $x$ 위치 고려 없이 카트가 움직이다가 레일 끝에 부딪혀 제어를 성공적으로 할 수 없었다.<br />
반면에 MPC 제어는 $x$ 범위를 고려해서 예측 지평 내에서 $x$ 한계를 회피하도록 입력 $u$ 를 재분배했기 때문에 충돌하지 않았다.</p>

<p>Theta max post 그래프(2)는 전체 시뮬레이션 시간동안의 최대 값을 나타내며 LQR은 amp 275부터 실패하므로 값이 비어 보인다.<br />
시계열 데이터를 확인한 결과 외력이 들어오는 step 동안에 관성에 의해 순간 기울어진 각도가 막대의 최대 각도였다.<br />
내가 확인하고 싶은 것은 외란이 들어온 순간을 제외한 각도의 오버슈트였다. 따라서 아래 Theta_max_after_zero 그래프를 추가하였다.<br />
아래 그래프는 외란 이후 막대가 카트 제어에 의해 0도를 한 번 지나친 시점 이후의 최대각이다.<br />
외란의 강도가 강해질수록 각도가 증가했지만, 제어의 영향으로 완벽한 선형으로 증가하지는 않았다.</p>

<p>Recovery Time 그래프(3)에서는 MPC 제어일때, amp 275에서 가장 느린 회복 시간을 보여주는 것을 볼 수 있다.<br />
이는 외력의 힘이 더 약한 경우와 강한 외력에 맞서 강한 제어가 들어올 경우에, 회복 시간이 더 짧아진다는 것을 보여준다.<br />
회복 시간의 정의는 다음과 같다.</p>

<p>$\Delta t$ : 샘플링 시간(시뮬레이션 1 step 시간)<br />
$k_{0}$ : 외란 시작 스텝<br />
$D$ : 외란 길이 (step)<br />
$k_{s} = k_{0} + D$ : 외란 종료 직후부터 평가 시작하는 기준 스텝</p>

<p>각 스텝에서 리커버리 밴드 진입 여부를</p>

\[b_k = \mathbf{1} \left( \vert\theta_k\vert \le \varepsilon_{\theta} \land \vert\dot{\theta}_k\vert \le \varepsilon_{\omega} \right)\]

<p>위와 같이 정의해서 각도와 각속도가 모두 기준치 이하일 때만 1을 출력하게 하고</p>

<p>hold 시간 $T_{h}$ 를 시뮬레이션 step 시간인 $\Delta t$ 로 나눠서, 시간을 안정적으로 머물러야하는 step으로 바꾸면</p>

<p>$N_h = \max \left( 1, \mathrm{round} \left( \frac{T_h}{\Delta t} \right) \right)$</p>

<p>리커버리 완료 스텝은</p>

\[k_{\mathrm{rec}} = \min \left\{ k \ge k_s \mid \prod_{j=0}^{N_h-1} b_{k+j} = 1 \right\}\]

<p>곱연산을 통해서 모든 스텝이 1일때만 전체가 1이 되는 특성을 사용했다.</p>

<p>$\prod_{j=0}^{N_h-1} b_{k+j} = 1$ 을 처음 만족하는 $k$를 회복 완료 스텝으로 판정하였다.</p>

<p>$\varepsilon_{\theta}$ : $3^\circ$<br />
$\varepsilon_{\omega}$ : $0.1 \ rad/s$<br />
$T_{h}$ : $0.5 \ s$</p>

<p>기준치는 위와 같이 설정했다.<br />
요약하자면, (3도 이내 &amp; 0.1의 각속도 이하)를 0.5초 유지하면 회복한 것으로 판정하였다.</p>

<p>Theta 그래프와 Recovery 그래프를 보면 두 제어기 모두 성공하는 구간 amp 250~270에서 제약이 활성화 되는 빈도가 낮아, 값이 거의 동일한 것을 확인할 수 있다.<br />
제약이 거의 활성화 되지 않는 구간에서는 LQR의 해가 사실상 최적해이기때문에 MPC 역시 LQR에 근접한다.</p>

<p>하지만 외란 이후부터 LQR 실패 시점 까지 분리된 제어 입력 에너지 그래프(4)를 보면, amp 270 까지는 LQR과 MPC가 거의 비슷하지만 LQR이 실패하는 275부터 MPC 에너지가 상대적으로 급상승 하는 것 처럼 보인다.<br />
이 현상은 LQR이 실패 이후 step 구간에서는 제어가 멈추고, 실패 직전 step까지 MPC보다 약한 제어 입력을 내보내기 때문이다.<br />
각 시점별로 확인하기 위해 두 번째 그래프에서 외란이 끝나는 시점인 105번째 step부터 LQR 실패 시점인 128 step 시점까지만 제어 입력을 분리해보았다.<br />
음(-) 부호의 제어 입력은 카트를 왼쪽 벽으로 향하게 하는 힘이 된다.<br />
MPC는 109 step 부근에 강하게 가속해서 막대의 균형을 먼저 잡고, 115 step 부근에서 양(+) 부호 제어 입력을 강하게 넣어 충돌 방지를 위한 감속을 시작한다.<br />
하지만 LQR은 109 step 부근에서 균형을 약하게 잡고 양(+) 부호로 감속하는 제어 입력 역시 약하다.<br />
MPC는 $x$ 위치에 대한 제약을 고려하기 때문에 균형을 빠르게 잡고 남은 시간을 레일을 벗어나지 않기 위해 감속하는 것에 사용하지만, LQR은 레일 제약을 알지 못하기 때문에 막대 상태에 따른 최적의 제어만 하다가 결국 레일 끝에 부딪혀 막대 균형 잡기를 실패하게 된다.</p>

<p>전체 시계열 그래프(5)를 보면 MPC는 각속도를 강한 제어로 양수로 바꾼 후 균형을 유지하지만, LQR은 상대적으로 약한 제어로 균형을 잡으려다가 시간이 부족하여 레일 끝에 도달하는 것으로 나타난다.</p>

<h3 id="노이즈지연-모델">노이즈/지연 모델</h3>

<p>이제 앞선 실험과 동일한 환경에서 노이즈와 지연만 추가해서 응답이 어떻게 바뀌는지 확인해볼 차례이다.</p>

<h4 id="노이즈가-추가된-경우">노이즈가 추가된 경우</h4>

<p>센서 노이즈는 $\theta_{\mathrm{meas}} = \theta + \mathcal{N}(0,\sigma_\theta^2)$ 로 적용하였다.</p>

<p>$\sigma_\theta = {0.005, 0.01}$ 을 적용하여 2가지 경우에 대해 실험하였다.</p>

<p>노이즈는 제어의 전체적인 경향은 유지한채, 각 제어 입력에 $\pm$ 범위를 주게되어 전체적인 응답 위에 작은 진동이 추가될 것으로 예상했다.</p>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_rate005.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_timeseries_all_amps_seed0005.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/sat_rate005.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"></td>
  </tr>
</table>

<p>(6 - amp 250~290 성공률)<br />
(7 - amp 270, 290 상태 시계열)<br />
(8 - amp 250~290 포화 활성 비율 u, du)</p>

<p>노이즈가 생긴 상황에서는 노이즈가 없는 상황과 비교했을 때, LQR 제어기의 성공률이 amp 270에서 감소하고 275에서는 증가하였다.<br />
한 두 step 차이로 $x$ fail로 실패하던 amp 275에서, 노이즈의 영향으로 제어 값이 범위를 갖게 되어 드물게 성공하는 경우가 생겼다는 것을 알 수 있다.<br />
반대로 노이즈로인해 $x$ 가 감소한 경우에는 성공하던 amp 270에서도 드물게 실패하는 경우가 생겼다.</p>

<p>각속도와 제어 입력 그래프에서 특히 잔진동이 증가함을 확인했고, 이로인해 리커버리 타임이 증가한 것을 알 수 있다.</p>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_rate001.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_timeseries_all_amps_seed0001.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/sat_rate001.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"></td>
  </tr>
</table>

<p>(9 - amp 250~290 성공률)<br />
(10 - amp 270, 290 상태 시계열)<br />
(11 - amp 250~290 포화 활성 비율 u, du)</p>

<p>노이즈가 0.01로 증가하면서 0.005에서 보인 경향성이 더 짙어졌다.<br />
90% 이상 성공률을 가지는 amp를 의미하는 $A_{90}$ 값은 LQR의 경우 0.005 노이즈에서 $A_{90} = 265$ 에서 0.01 노이즈일때 $A_{90} = 260$ 으로 감소했고, $A_{10}$ 의 경우 노이즈가 증가함에 따라 $A_{10} = 275$ 에서 $A_{10} = 280$ 으로 증가하였다.<br />
노이즈로 인한 제어 무작위성으로 인해 각 amp에서 더 견디거나 덜 견딜수 있게 되어 초기 조건에 따라 성공률이 낮아지거나 높아지는 것으로 확인된다.<br />
Constraint Activation 그래프를 보면 외력이 강해서 포화가 길었던 amp에서는 노이즈가 포화를 줄이는 방향으로 작용하고, 외력이 약해서 포화가 짧았던 amp에서는 노이즈가 포화를 증가시키는 방향으로 작용했다.<br />
이는 포화선 부근 전후에 위치하는 제어 입력들이 노이즈에 의해 포화선 밖으로 나가고 들어오는 비율의 차이 때문에 발생한다.</p>

<p>각속도와 제어 입력은 일관된 경향으로 더 불안정해졌고, 리커버리 타임은 더 증가하였다.</p>

<h4 id="지연이-추가된-경우">지연이 추가된 경우</h4>

<p>지연은 $u_{\mathrm{applied}}(k) = u_{\mathrm{cmd}}(k-d)$, $d = {1, 2}$ step으로 2가지 지연 상황을 실험하였다.</p>

<p>지연은 노이즈와 달리 작은 진동이 추가되는 것이 아니라 전체적인 응답 자체의 불안정성을 높여서 수렴을 어렵게 하고, 그 결과 응답 형태가 진동/발산하는 형태일 것으로 예상했다.</p>

<p>기존 amp 250~290 범위 실험에서는 지연이 1 step일때, MPC가 제약을 만족하면서 제어 입력 u를 찾을 수 없었다(solver_fail/infeasible).<br />
따라서 지연에 의해 제어 가능한 외력 amp 구간이 얼마나 감소하는지 확인하기 위해 amp 50부터 275까지 25 단위로 성공률을 확인했다.</p>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_rated1.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_timeseries_all_amps_seed2d1.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
</table>

<p>(12 - amp 50~275 성공률)<br />
(13 - amp 250, 275 상태 시계열)</p>

<p>amp 50부터 100 구간에서는 LQR과 MPC 모두 제어에는 성공했지만 이후 구간에서 두 모델 모두 꾸준히 성공률이 감소했다.<br />
LQR은 $x$ fail, MPC는 $\theta$ fail, solver fail이 각각 발생했다.<br />
LQR은 앞선 경우(노이즈/포화)와 마찬가지로 $x$ 제약을 반영하지 못해 제어에 실패했고, MPC는 제어 입력 지연에 hard한 제약까지 맞물려 입력 해를 찾지 못하거나(solver fail) 제어가 불안정해져 각도 제어에 실패(theta fail)하였다.</p>

<p>노이즈/지연이 없을 때 $A_{90} = 265$ 값을 가지던 LQR이 지연이 추가되자 amp 100에서부터 성공률이 감소하기 시작해서 $A_{90} = 125$ 를 가지게 되었다.<br />
지연에 의해 버틸 수 있는 외력이 일찍 감소하기 시작하였고, 최대 외력 한도 $A_{max}$ 이 $A_{max} = 270$ 에서 $A_{max} = 250$ 으로 7.4% 감소하였다.<br />
MPC도 마찬가지로 $A_{max} = 275$ 까지 감소하였다.</p>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/success_rated2.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-2/u_timeseries_all_amps_seed2d2.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
</table>

<p>(14 - amp 50~275 성공률)<br />
(15 - amp 250, 275 상태 시계열)</p>

<p>지연이 2 step이 된 경우, LQR과 MPC 모두 최대 외력 한도는 다른 amp 구간에 비해 크게 감소하지 않은 $A_{max} = 250$ 값을 가졌지만 낮은 amp 범위에서 전체적인 성공률이 급격하게 감소했다.<br />
또한 제어 진동이 계속 유지되어 리커버리 타임을 구할 수 없었다.</p>

<p>LQR은 각도 제어 실패(theta fail)와 $x$ 제약 실패(x fail)를 모두 가지고 있었지만, MPC는 각도 제어에 실패(theta fail)하거나 $x$ 제약을 지키기위한 제어 해를 찾는 것에 실패(solver fail)한다는 차이가 있었다.</p>

<p>지연 실험으로부터 입력이 늦어짐에따라 위상 여유가 감소함에따라 진동/발산 방향으로 제어가 변하면서 불안정해진다는 것을 확인할 수 있다.<br />
지연은 성공률 분산을 키우는 노이즈와 달리 임계 외력 구간을 일관되게 왼쪽으로 이동시킨다는 것을 확인하였다.</p>

<h2 id="결론-및-토의">결론 및 토의</h2>

<p>앞선 결과를 조건별로 종합하면, 포화/노이즈/지연은 성능을 떨어뜨리는 방식이 서로 다르게 나타났다. 포화는 입력과 상태의 허용 범위를 직접 제한하여, 제어기가 사용할 수 있는 여유를 물리적으로 깎아먹는다. 특히 $x$ 레일 제약이 있는 환경에서는, 제약을 사전에 고려하지 않는 LQR이 $x$ fail로 먼저 무너지는 반면, MPC는 예측 지평 내에서 $x$ 한계를 회피하도록 입력 $u$를 재분배하여 같은 외란에서도 성공 구간을 확장했다.</p>

<p>노이즈는 지연과 달리 임계 구간 자체를 일관되게 이동시키기보다는, 성공률의 분산을 키우는 형태로 나타났다. $\theta$ 측정 노이즈가 증가하면 특정 amp에서 “한 두 step 차이로” 성공/실패가 갈리며, $A_{90}$은 감소(안정적으로 버틸 수 있는 구간 축소)하고 $A_{10}$은 증가(경계 amp에서 운 좋게/나쁘게 버티는 경우 증가)하는 경향이 확인되었다. 즉 노이즈는 평균 성능을 낮추는 효과도 있지만, 더 직접적으로는 변동성을 키워 성공률 곡선의 경계를 모호하게 만든다.</p>

<p>반면 지연은 성공률 분산을 키우기보다는 임계 구간 자체를 왼쪽으로 이동시키는 효과가 지배적이었다. 지연이 추가되자 LQR은 $A_{90}$이 $265 \rightarrow 125$로 크게 감소했고, $A_{max}$ 역시 $270 \rightarrow 250$으로 감소했다. 이는 입력이 늦어지면서 위상 여유가 줄어들고, 동일 외란에서도 제어가 진동/발산 방향으로 변해 안정성 여유가 줄어들기 때문이다. 이때 실패 모드 역시 LQR과 MPC에서 갈린다. LQR은 주로 $x$ fail이 중심이었고, MPC는 1. 지연 + hard 제약으로 인해 feasibility가 먼저 붕괴하는 solver_fail/infeasible, 2. feasibility가 유지되더라도 지연으로 인해 $\theta$ fail로 이어지는 두 경향이 나타났다.</p>

<p>이 차이는 로봇 제어에서 현실에 적용할 때 중요하다고 생각한다. 현실 시스템은 센서 노이즈와 입력 지연이 항상 존재하므로, MPC를 “hard 제약 + fallback 없음”으로 구성하면 성능 열화 이전에 최적화가 먼저 죽는 문제가 생길 수 있다. 따라서 실제 적용에서는 soft constraint, solver 실패 시 대체 제어기, 지연을 미리 고려한 모델링, 그리고 노이즈를 완화하는 상태 추정기가 함께 필요할 것 같다.</p>

<p>한계로는 노이즈를 $\theta$에만, 지연을 입력에만 적용했으며, fallback이 없는 MPC 설정이라 feasibility 문제가 과장될 수 있다. 다음 단계로 soft constraint 및 지연을 고려한 모델 MPC로 설정을 확장하고, 동일 프로토콜에서 임계 amp($A_{90}, A_{10}, A_{max}$)와 실패 모드 분포가 어떻게 바뀌는지 개선 효과를 검증하면 좋을 것 같다.</p>

<h2 id="부록실험-정의">부록/실험 정의</h2>

<p>사용한 환경/모델/비용함수/제약/외란/평가/프로토콜을 한 곳에 고정해 재현 가능하도록 정리했다.</p>

<h3 id="m0-표기단위">M0. 표기/단위</h3>

<ul>
  <li>상태: $\mathbf{x}_k = [x,\theta,\dot{x},\dot{\theta}]^\top$   (단위: $x\,[m]$, $\theta\,[rad]$, $\dot{x}\,[m/s]$, $\dot{\theta}\,[rad/s]$)</li>
  <li>입력: $u_k$ (환경 action), $F_k = \mathrm{gear}\,u_k$   (단위: $u\,[\mathrm{unitless}]$, $F\,[N]$)</li>
  <li>샘플링: $\Delta t =$ <code class="language-plaintext highlighter-rouge">0.04</code> s, $k$는 이산 시간 인덱스</li>
</ul>

<h3 id="m1-시뮬레이션환경">M1. 시뮬레이션/환경</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시뮬레이터</td>
      <td>MuJoCo <code class="language-plaintext highlighter-rouge">3.4.0</code></td>
    </tr>
    <tr>
      <td>Env</td>
      <td><code class="language-plaintext highlighter-rouge">Gymnasium InvertedPendulum-v5</code> (<code class="language-plaintext highlighter-rouge">xml_file=envs/assets/inverted_pendulum_unlimited_hinge.xml</code>)</td>
    </tr>
    <tr>
      <td>integrator / substeps</td>
      <td><code class="language-plaintext highlighter-rouge">RK4 / frame_skip=2</code> (model timestep <code class="language-plaintext highlighter-rouge">0.02 s</code>)</td>
    </tr>
    <tr>
      <td>timestep</td>
      <td>$\Delta t =$ <code class="language-plaintext highlighter-rouge">0.04</code> s</td>
    </tr>
    <tr>
      <td>마찰/댐핑</td>
      <td><code class="language-plaintext highlighter-rouge">cart geom friction=(1.0, 0.1, 0.1)</code>, <code class="language-plaintext highlighter-rouge">hinge damping=1.0</code></td>
    </tr>
    <tr>
      <td>gear</td>
      <td>$\mathrm{gear} =$ <code class="language-plaintext highlighter-rouge">100</code></td>
    </tr>
    <tr>
      <td>레일/조인트 리밋</td>
      <td>$x \in [x_{\min}, x_{\max}]$,   $x_{\min}=$ <code class="language-plaintext highlighter-rouge">-1(기본)</code> , $x_{\max}=$ <code class="language-plaintext highlighter-rouge">1(기본)</code></td>
    </tr>
  </tbody>
</table>

<h3 id="m2-모델-및-선형화이산화">M2. 모델 및 선형화/이산화</h3>

<ul>
  <li>선형화 기준점: 직립 근처 $\theta^* = 0$, $\dot{x}^* = 0$, $\dot{\theta}^* = 0$, $x^* =$ <code class="language-plaintext highlighter-rouge">0</code>, $u^* =$ <code class="language-plaintext highlighter-rouge">0</code></li>
  <li>이산 선형 모델: $\mathbf{x}_{k+1} = A_d \mathbf{x}_k + B_d u_k$</li>
  <li>$A_d, B_d$ 수치는 위 발췌 내용($A_d^{th}/A_d^{fd}$, $B_d^{th}/B_d^{fd}$) 중 아래에서 선택한 것을 사용한다.</li>
</ul>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>사용 모델</td>
      <td><code class="language-plaintext highlighter-rouge">fd</code></td>
    </tr>
    <tr>
      <td>$A_d$</td>
      <td><code class="language-plaintext highlighter-rouge">A_d^{fd}</code></td>
    </tr>
    <tr>
      <td>$B_d$</td>
      <td><code class="language-plaintext highlighter-rouge">B_d^{fd}</code></td>
    </tr>
  </tbody>
</table>

<h3 id="m3-외란-주입">M3. 외란 주입</h3>

<ul>
  <li>외란 시작/길이: $k_0 =$ <code class="language-plaintext highlighter-rouge">100(기본)</code>, $D =$ <code class="language-plaintext highlighter-rouge">5(실험값)</code> step,   $k_s = k_0 + D$</li>
  <li>amp 정의:</li>
</ul>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>적용 물리량</td>
      <td><code class="language-plaintext highlighter-rouge">카트 수평 외력 F_ext (action bias 아님)</code></td>
    </tr>
    <tr>
      <td>적용 위치</td>
      <td><code class="language-plaintext highlighter-rouge">slider joint DOF (qfrc_applied)</code></td>
    </tr>
    <tr>
      <td>파형</td>
      <td><code class="language-plaintext highlighter-rouge">window</code> ($k_0 \le k &lt; k_0 + D$)</td>
    </tr>
    <tr>
      <td>크기</td>
      <td><code class="language-plaintext highlighter-rouge">amp [N]</code>, $F_{\mathrm{ext}}(k)=s\cdot\mathrm{amp}$ (seed별 $s\in{-1,+1}$, 기본 balanced)</td>
    </tr>
  </tbody>
</table>

<h3 id="m4-제어기-설계">M4. 제어기 설계</h3>

<h4 id="m41-lqr">M4.1 LQR</h4>

<ul>
  <li>시스템: $\mathbf{x}_{k+1} = A_d \mathbf{x}_k + B_d u_k$</li>
  <li>비용함수(무한지평 LQR):</li>
</ul>

\[J = \sum_{k=0}^{\infty} \left(\mathbf{x}_k^\top Q\,\mathbf{x}_k + u_k^\top R\,u_k\right)\]

<ul>
  <li>$Q, R$</li>
</ul>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>$Q$</td>
      <td>\(\mathrm{diag}([\,1 , 80 , 1 , 10\,])\)</td>
    </tr>
    <tr>
      <td>$R$</td>
      <td><code class="language-plaintext highlighter-rouge">0.1</code></td>
    </tr>
    <tr>
      <td>선정 근거</td>
      <td><code class="language-plaintext highlighter-rouge">각도에 중점을 둔 큰 가중치와 과도한 입력 방지를 위한 작은 입력 가중치</code></td>
    </tr>
    <tr>
      <td>사용 게인</td>
      <td><code class="language-plaintext highlighter-rouge">K^{fd}</code></td>
    </tr>
  </tbody>
</table>

<ul>
  <li>구현상의 제약 처리</li>
</ul>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>raw 입력</td>
      <td>$u_{\mathrm{raw}} = -K\mathbf{x}_k$</td>
    </tr>
    <tr>
      <td>입력 포화</td>
      <td>$u = \mathrm{clip}(u_{\mathrm{raw}}, u_{\min}, u_{\max})$</td>
    </tr>
    <tr>
      <td>레이트 제한</td>
      <td>$\vert u_k-u_{k-1}\vert \le \Delta u_{\max}$</td>
    </tr>
    <tr>
      <td>적용 순서</td>
      <td><code class="language-plaintext highlighter-rouge">clip/rate 제약 교집합으로 동시 투영</code></td>
    </tr>
  </tbody>
</table>

<h4 id="m42-mpc">M4.2 MPC</h4>

<ul>
  <li>예측모델: $\mathbf{x}_{i+1} = A_d \mathbf{x}_i + B_d u_i$</li>
  <li>예측 지평: $N =$ <code class="language-plaintext highlighter-rouge">20</code> (외란이 5 step이므로 20 step은 외란 이후 회복 구간 포함)</li>
  <li>비용(예시: terminal 포함):</li>
</ul>

\[\min_{\{u_i\}_{i=0}^{N-1}} \sum_{i=0}^{N-1}\left(\mathbf{x}_i^\top Q\,\mathbf{x}_i + u_i^\top R\,u_i\right) + \mathbf{x}_N^\top P\,\mathbf{x}_N\]

<ul>
  <li>제약: M5의 제약을 예측 상태와 입력에 적용한다. (현재 상태가 아닌 미래 구간)
\(\mathbf{x}_1,\ldots,\mathbf{x}_N
\quad(\text{i.e., }\mathbf{x}_{k+1},\ldots,\mathbf{x}_{k+N}),
\qquad
u_0,\ldots,u_{N-1}\)</li>
</ul>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>terminal cost $P$</td>
      <td><code class="language-plaintext highlighter-rouge">DARE의 P (solve_discrete_are)</code></td>
    </tr>
    <tr>
      <td>terminal constraint</td>
      <td><code class="language-plaintext highlighter-rouge">미사용</code></td>
    </tr>
    <tr>
      <td>QP solver</td>
      <td><code class="language-plaintext highlighter-rouge">auto (osqp 우선, 없으면 scipy)</code></td>
    </tr>
    <tr>
      <td>tol / max_iter</td>
      <td><code class="language-plaintext highlighter-rouge">eps_abs=1e-5, eps_rel=1e-5, max_iter=4000</code></td>
    </tr>
    <tr>
      <td>warm start</td>
      <td><code class="language-plaintext highlighter-rouge">on</code></td>
    </tr>
    <tr>
      <td>infeasible 처리</td>
      <td><code class="language-plaintext highlighter-rouge">별도 slack/fallback 없음 (해 실패 시 RuntimeError)</code></td>
    </tr>
  </tbody>
</table>

<h3 id="m5-제약">M5. 제약</h3>

<ul>
  <li>입력 제약: $u_{\min} \le u_k \le u_{\max}$,   $u_{\min} = -3$, $u_{\max} = 3$</li>
  <li>상태 제약(레일): $x_{\min} \le x_k \le x_{\max}$,   $x_{\min} = -1$, $x_{\max} = 1$</li>
  <li>입력 변화량: $\vert u_k-u_{k-1}\vert \le \Delta u_{\max}$,   $\Delta u_{\max} = 2.6$</li>
</ul>

<blockquote>
  <ul>
    <li>센서 노이즈: $\theta_{\mathrm{meas}} = \theta + \mathcal{N}(0,\sigma_\theta^2)$, $\sigma_\theta =$ <code class="language-plaintext highlighter-rouge">0.005, 0.01</code></li>
    <li>입력 지연: $u_{\mathrm{applied}}(k) = u_{\mathrm{cmd}}(k-d)$, $d =$ <code class="language-plaintext highlighter-rouge">1, 2</code> step<br />
(제어기는 지연을 고려하지 않고 $u_{\mathrm{cmd}}(k)$를 계산하며, 실제 적용은 $d$ step 지연된 값이 사용됨)</li>
  </ul>
</blockquote>

<h3 id="m6-평가-지표">M6. 평가 지표</h3>

<ul>
  <li>
    <p>성공/실패: <code class="language-plaintext highlighter-rouge">전 시간 구간 기준. terminated_any=0이면 성공, |θ|&gt;1.5708 또는 |x|≥1.0 또는 NaN이면 실패</code></p>
  </li>
  <li>$\theta$ 오버슈트:
    <ul>
      <li>$\theta_{\max,\mathrm{all}} = \max_k \vert\theta_k\vert$</li>
      <li>$\theta_{\max,\mathrm{post}} = k_s$ 이후 첫 0-crossing 뒤 구간에서 $\max \vert\theta_k\vert$ (success only)</li>
    </ul>
  </li>
  <li>리커버리 시간:<br />
 \(b_k = \mathbf{1} \left( \vert\theta_k\vert \le \varepsilon_{\theta} \land \vert\dot{\theta}_k\vert \le \varepsilon_{\omega} \right)\)
    <ul>
      <li>$\varepsilon_{\theta} = 3^\circ$, $\varepsilon_{\omega} = 0.1\ \mathrm{rad/s}$, $T_h = 0.5\ \mathrm{s}$</li>
      <li>$N_h = \max \left( 1, \mathrm{round} \left( \frac{T_h}{\Delta t} \right) \right)$</li>
      <li>$k_{\mathrm{rec}} = \min{\,k \ge k_s \mid \prod_{j=0}^{N_h-1} b_{k+j} = 1\,}$</li>
      <li>리커버리 시간: $t_{\mathrm{rec}} = (k_{\mathrm{rec}}-k_s)\Delta t$</li>
    </ul>
  </li>
  <li>입력 사용량
    <ul>
      <li>$E_u = \sum_{k=0}^{T-1} u_{\text{applied},k}^2$</li>
      <li>summary CSV의 <code class="language-plaintext highlighter-rouge">u_energy</code>는 에피소드 전체 구간의 applied input 제곱합</li>
      <li><code class="language-plaintext highlighter-rouge">u_energy.png</code>의 1행은 별도 보조지표로, 외란 이후 20 step 구간($k_{\text{start}}\ldots k_{\text{start}}+k_{\text{len}}-1$)에서의 $\sum u_k^2$를 사용</li>
    </ul>
  </li>
</ul>

<h4 id="m61-그래프-산출-규칙">M6.1 그래프 산출 규칙</h4>

<ul>
  <li>성공률 그래프(<code class="language-plaintext highlighter-rouge">success_rate*.png</code>)
    <ul>
      <li>집계 단위: <code class="language-plaintext highlighter-rouge">(controller, amp)</code>별 seed 평균</li>
      <li>계산식: $\mathrm{SuccessRate}(c,a)=\frac{1}{N_{\text{seed}}}\sum_{s=1}^{N_{\text{seed}}}\mathbf{1}[\mathrm{success}_{c,a,s}=1]$</li>
      <li><code class="language-plaintext highlighter-rouge">success=1</code> 조건: summary CSV의 <code class="language-plaintext highlighter-rouge">terminated_any=0</code> (실패 조건은 M6의 성공/실패 정의와 동일)</li>
    </ul>
  </li>
  <li>시계열 그래프(<code class="language-plaintext highlighter-rouge">u_timeseries*.png</code>)
    <ul>
      <li>데이터 소스: step CSV</li>
      <li>채널: $\theta$, $\dot{\theta}$, $x$, $u_{\mathrm{applied}}$ (없으면 $u_{\mathrm{raw}}$ 대체)</li>
      <li>선택 규칙: 지정한 <code class="language-plaintext highlighter-rouge">seed</code>와 amp(단일 amp 또는 amp index 목록)만 시각화</li>
      <li>보조 표기: 외란 구간 $[t_0,\ t_0+\mathrm{duration})$ 음영, 리커버리 마커(설정된 $\varepsilon_\theta,\varepsilon_\omega,T_h$ 기준)</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">u</code>/<code class="language-plaintext highlighter-rouge">du</code> 포화(활성) 비율 그래프(<code class="language-plaintext highlighter-rouge">sat_rate*.png</code>)
    <ul>
      <li>계산 구간: 에피소드 유효 구간 $k=0\ldots T-1$ (실패 시 실패 시점까지)</li>
      <li><code class="language-plaintext highlighter-rouge">u</code> 활성 마스크:
\(\mathbf{1}\!\left[|u_k|\ge u_{\max}-s_u\right], \quad s_u\equiv \texttt{sat_tol}\)</li>
      <li><code class="language-plaintext highlighter-rouge">du</code> 활성 마스크:
\(\mathbf{1}\!\left[|\Delta u_k|\ge \Delta u_{\max}-s_{\Delta u}\right],\quad
\Delta u_k=u_k-u_{k-1},\quad
s_{\Delta u}\equiv \texttt{du_tol}\)</li>
      <li>비율:
\(r_u=\frac{1}{T}\sum_{k=0}^{T-1}\mathbf{1}\!\left[|u_k|\ge u_{\max}-s_u\right]\)
\(r_{\Delta u}=\frac{1}{T-1}\sum_{k=1}^{T-1}\mathbf{1}\!\left[|\Delta u_k|\ge \Delta u_{\max}-s_{\Delta u}\right]\)</li>
    </ul>
  </li>
  <li>$A_{90}$ 정의:
    <ul>
      <li>$A_{90} = \max{\text{amp} \mid \text{success_rate}(\text{amp}) \ge 0.9}$</li>
    </ul>
  </li>
  <li>$A_{10}$ 정의:
    <ul>
      <li>$A_{10} = \max{\text{amp} \mid \text{success_rate}(\text{amp}) \ge 0.1}$</li>
    </ul>
  </li>
  <li>$A_{\max}$ 정의:
    <ul>
      <li>$A_{\max} = \max{\text{amp} \mid \text{success_rate}(\text{amp}) \ge 0}$</li>
    </ul>
  </li>
</ul>

<h3 id="m7-실험-프로토콜">M7. 실험 프로토콜</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>초기조건 분포</td>
      <td><code class="language-plaintext highlighter-rouge">x0, xdot0, thetadot0 ~ Uniform[-0.01, 0.01], theta0=0 (override)</code></td>
    </tr>
    <tr>
      <td>에피소드 길이</td>
      <td><code class="language-plaintext highlighter-rouge">250</code> step</td>
    </tr>
    <tr>
      <td>seed 수</td>
      <td><code class="language-plaintext highlighter-rouge">50</code> (0~49)</td>
    </tr>
    <tr>
      <td>seed 특성</td>
      <td>LQR/MPC 비교는 동일 seed에서 동일 노이즈/외란 시퀀스를 사용</td>
    </tr>
    <tr>
      <td>amp sweep (Level1/Noise)</td>
      <td><code class="language-plaintext highlighter-rouge">250,255,260,265,270,275,280,285,290</code> (간격 <code class="language-plaintext highlighter-rouge">5</code>)</td>
    </tr>
    <tr>
      <td>amp sweep (Delay)</td>
      <td><code class="language-plaintext highlighter-rouge">50,75,100,125,150,175,200,225,250,255,260,265,270,275,280,285,290</code> (간격 <code class="language-plaintext highlighter-rouge">25</code>)</td>
    </tr>
    <tr>
      <td>조기 종료</td>
      <td>종료 조건: $\vert\theta\vert &gt; 1.5708$ 또는 $\vert x\vert \geq 1.0$ 또는 NaN</td>
    </tr>
    <tr>
      <td>로그</td>
      <td><code class="language-plaintext highlighter-rouge">step CSV(k,t_sec,x,theta,xdot,thetadot,u_raw,u_applied,du_applied,disturb_force,reward,terminated,truncated) + summary CSV(성공/실패, term_reason, sat/du 활성화, recovery, u_energy 등)</code></td>
    </tr>
  </tbody>
</table>

<h3 id="m8-터미널-실행-예시">M8. 터미널 실행 예시</h3>

<p>인자들에 대한 자세한 설명은 GitHub EXPERIMENT_COMMANDS.md 파일 참조</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> experiments.fd_compare.eval_sweep_fd_compare <span class="se">\</span>
    <span class="nt">--mode</span> metrics <span class="se">\</span>
    <span class="nt">--controllers</span> lqr_fd,mpc_fd <span class="se">\</span>
    <span class="nt">--amps</span> 50,75,100,125,150,175,200,225,250,275 <span class="se">\</span>
    <span class="nt">--seeds</span> 50 <span class="se">\</span>
    <span class="nt">--disturbance-kind</span> window <span class="se">\</span>
    <span class="nt">--t0</span> 100 <span class="se">\</span>
    <span class="nt">--duration</span> 5 <span class="se">\</span>
    <span class="nt">--theta0</span> 0 <span class="se">\</span>
    <span class="nt">--termination-theta</span> 1.5708 <span class="se">\</span>
    <span class="nt">--termination-x-limit</span> 1.0 <span class="se">\</span>
    <span class="nt">--x-fail-limit</span> 1.0 <span class="se">\</span>
    <span class="nt">--x-fail-eps</span> 0.0 <span class="se">\</span>
    <span class="nt">--x-fail-hold</span> 1 <span class="se">\</span>
    <span class="nt">--actuator-u-max</span> 3.0 <span class="se">\</span>
    <span class="nt">--actuator-u-min</span> <span class="nt">-3</span>.0 <span class="se">\</span>
    <span class="nt">--actuator-du-max</span> 2.6 <span class="se">\</span>
    <span class="nt">--du-tol</span> 0.01 <span class="se">\</span>
    <span class="nt">--mpc-state-constraint</span> x <span class="se">\</span>
    <span class="nt">--mpc-x-margin</span> 0.02 <span class="se">\</span>
    <span class="nt">--metric-du-threshold</span> 2.6 <span class="se">\</span>
    <span class="nt">--sat-tol</span> 0.02 <span class="se">\</span>
    <span class="nt">--steps</span> 250 <span class="se">\</span>
    <span class="nt">--actuation-delay-steps</span> 1 <span class="se">\</span>
    <span class="nt">--step-log-dir</span> logs/fd_compare/steps_metrics <span class="se">\</span>
    <span class="nt">--out</span> logs/fd_compare/summary_force.csv
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> experiments.fd_compare.plot_fd_compare <span class="se">\</span>
    <span class="nt">--csv</span> logs/fd_compare/summary_force.csv <span class="se">\</span>
    <span class="nt">--outdir</span> plots/fd_compare <span class="se">\</span>
    <span class="nt">--step-log-dir</span> logs/fd_compare/steps_metrics <span class="se">\</span>
    <span class="nt">--u-seed</span> 2 <span class="se">\</span>
    <span class="nt">--u-amp-idx</span> 5,10 <span class="se">\</span>
    <span class="nt">--u-energy-amp</span> 250 <span class="se">\</span>
    <span class="nt">--u-energy-seed</span> 2
</code></pre></div></div>]]></content><author><name>Harang Ji</name></author><category term="Control" /><category term="LQR" /><summary type="html"><![CDATA[동기]]></summary></entry><entry><title type="html">이론 모델 LQR vs 시뮬레이션 모델 LQR</title><link href="https://jiharangrang.github.io/control/2026/02/02/lqr_mpc_RL-1.html" rel="alternate" type="text/html" title="이론 모델 LQR vs 시뮬레이션 모델 LQR" /><published>2026-02-02T00:00:00+09:00</published><updated>2026-02-02T00:00:00+09:00</updated><id>https://jiharangrang.github.io/control/2026/02/02/lqr_mpc_RL-1</id><content type="html" xml:base="https://jiharangrang.github.io/control/2026/02/02/lqr_mpc_RL-1.html"><![CDATA[<h2 id="동기">동기</h2>

<p>제어 이론을 배우며 궁금했던 점은 ‘이론으로 푼 수식이 실제 로봇도 잘 제어할 수 있을까?’ 였다.
보통 예제에서는 마찰을 무시하거나 이상적인 조건을 가정하지만, 실제 로봇이나 시뮬레이터는 물리적 제약이 있다.</p>

<p>LQR을 시뮬레이션에 적용할 기회가 생긴 김에, 단순히 제어가 잘 된다에서 멈추지 않고 교과서적인 이론 모델로 설계한 제어기와 시뮬레이션 환경을 역으로 분석해서 만든 데이터 기반 모델을 붙여 보았을 때 이론은 시뮬레이션의 복잡함을 어디까지 버텨낼 수 있을지 알아보고자 했다.</p>

<p>이를 위해 Mujoco의 표준 도립진자 환경에서 동일 조건으로 구현/비교하고, 결과를 재현 가능한 코드와 로그로 남긴다.</p>

<h2 id="실험-세팅">실험 세팅</h2>

<p>아래 카트-폴 모델을 사용하여 운동 방정식을 통해 수식을 작성하였다.</p>

<p><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/image.png" alt="image.png" /></p>

<p>$x : \text{카트 위치},\ \theta : \text{막대 각도(직립이 } \theta=0\text{)},\ u : \text{환경 action}$</p>

<p>$M : \text{카트 질량}$<br />
$m : \text{막대 질량}$<br />
$l : \text{힌지}\rightarrow\text{막대 COM 거리}$<br />
$I : \text{막대 COM 기준 관성(힌지 축 기준)}$<br />
$g : \text{중력가속도} $<br />
$F : \text{카트에 작용하는 수평 힘(외력)} $<br />
$F’ = \mathrm{gear}\,u \ \text{(제어 힘)}$</p>

<h3 id="관성-포함-비선형-운동방정식">관성 포함 비선형 운동방정식</h3>

<p>$(M+m)\ddot{x} + m l\left(\ddot{\theta}\cos\theta - \dot{\theta}^{2}\sin\theta\right) = F$</p>

<p>$(I+m l^{2})\ddot{\theta} + m l\ddot{x}\cos\theta - m g l\sin\theta = 0$</p>

<p>$A = M+m$</p>

<p>$B = m l\cos\theta$</p>

<p>$C = I+m l^{2}$</p>

<p>라고 하면,</p>

<p>$\Delta(\theta) = AC - B^{2} = (M+m)(I+m l^{2}) - (m l\cos\theta)^{2}$</p>

<p>$\ddot{x} = \dfrac{C\left(F + m l\dot{\theta}^{2}\sin\theta\right) - B\left(m g l\sin\theta\right)}{\Delta(\theta)}$</p>

<p>$\ddot{\theta} = \dfrac{-B\left(F + m l\dot{\theta}^{2}\sin\theta\right) + A\left(m g l\sin\theta\right)}{\Delta(\theta)}$</p>

<h3 id="직립-근방에서-선형화">직립 근방에서 선형화</h3>

<p>$\sin\theta \approx \theta,\quad \cos\theta \approx 1,\quad \dot{\theta}^{2}\sin\theta \approx 0$</p>

<p>$D = \Delta(0) = (M+m)(I+m l^{2}) - (m l)^{2} = (M+m)I + M m l^{2}$</p>

<p>$\ddot{x} = -\dfrac{m^{2} g l^{2}}{D}\theta + \dfrac{I+m l^{2}}{D}F$</p>

<p>$\ddot{\theta} = \dfrac{(M+m)m g l}{D}\theta - \dfrac{m l}{D}F$</p>

<h3 id="상태-정의-및-선형-상태공간-a-b">상태 정의 및 선형 상태공간 (A, B)</h3>

<p>MuJoCo InvertedPendulum-v5의 관측 순서가</p>

\[\mathrm{obs} = [\,x,\ \theta,\ \dot{x},\ \dot{\theta}\,]\]

<p>이므로, 상태를 아래처럼 정의한다.</p>

\[\mathbf{x} = \begin{bmatrix} x \\ \theta \\ \dot{x} \\ \dot{\theta} \end{bmatrix}\]

<p>입력은 먼저 카트에 작용하는 수평 힘 $F$로 두면,</p>

\[\dot{\mathbf{x}} = A_c\,\mathbf{x} + B_c\,F\]

<p>이고, 위에서 얻은 선형화 식</p>

\[D = (M+m)I + M m l^{2}\]

\[\ddot{x} = -\dfrac{m^{2} g l^{2}}{D}\,\theta + \dfrac{I+m l^{2}}{D}\,F\]

\[\ddot{\theta} = \dfrac{(M+m)m g l}{D}\,\theta - \dfrac{m l}{D}\,F\]

<p>을 이용해 분모를 다음처럼 두고 정리한다.</p>

\[A_c =
\begin{bmatrix}
0 &amp; 0 &amp; 1 &amp; 0 \\
0 &amp; 0 &amp; 0 &amp; 1 \\
0 &amp; -\dfrac{m^{2} g l^{2}}{D} &amp; 0 &amp; 0 \\
0 &amp; \dfrac{(M+m)m g l}{D} &amp; 0 &amp; 0
\end{bmatrix},\qquad
B_c =
\begin{bmatrix}
0 \\
0 \\
\dfrac{I+m l^{2}}{D} \\
-\dfrac{m l}{D}
\end{bmatrix}\]

<p>이다.</p>

<p>마지막으로 MuJoCo 환경에서의 action $u$를 직접 입력으로 쓰고 싶다면,</p>

\[F = \mathrm{gear}\,u\]

<p>이므로</p>

\[\dot{\mathbf{x}} = A_c\,\mathbf{x} + B_u\,u,\quad B_u = B_c\,\mathrm{gear}\]

<p>로 바꿔 쓸 수 있다.</p>

<p>지금까지 구한 $A_c, B_c$는 연속 시간을 가정하고 구한 이론 행렬이다.
하지만 시뮬레이션에서는 이산 시간으로 step마다 물리 엔진 계산이 돌아가므로, 추가로 이산화하는 과정이 필요하다.</p>

<p>시뮬레이션 환경을 확인해본 결과, 샘플링 시간이 0.04초인 것을 확인하였다.</p>

<p>$dt=0.04$에 맞게 이산화하면, (입력은 각 step 동안 일정하다고 가정하는 ZOH: Zero-Order Hold)</p>

\[\mathbf{x}_{k+1} = A_d\,\mathbf{x}_k + B_d\,u_k\]

<p>형태의 이산시간 선형 모델을 얻는다.</p>

<h3 id="zoh-이산화">ZOH 이산화</h3>

<p>연속시간 모델이</p>

\[\dot{\mathbf{x}} = A_c\,\mathbf{x} + B_u\,u\qquad (B_u = B_c\,\mathrm{gear})\]

<p>일 때, ZOH 이산화는 아래의 블록 행렬 지수로 정확하게 계산할 수 있다.</p>

\[\exp\!\left(
\begin{bmatrix}
A_c &amp; B_u \\
0 &amp; 0
\end{bmatrix}
 dt\right)
=
\begin{bmatrix}
A_d &amp; B_d \\
0 &amp; 1
\end{bmatrix}\]

<p>즉,</p>

\[A_d = \left[\exp\!\left(
\begin{bmatrix}
A_c &amp; B_u \\
0 &amp; 0
\end{bmatrix}
 dt\right)\right]_{1:4,\,1:4},\qquad
B_d = \left[\exp\!\left(
\begin{bmatrix}
A_c &amp; B_u \\
0 &amp; 0
\end{bmatrix}
 dt\right)\right]_{1:4,\,5}\]

<p>로 얻어진다.</p>

<p>입력을 힘 $F$로 두고 싶다면 $\dot{\mathbf{x}} = A_c\mathbf{x} + B_c F$ 에 대해 같은 방식으로</p>

\[\exp\!\left(
\begin{bmatrix}
A_c &amp; B_c \\
0 &amp; 0
\end{bmatrix}
 dt\right)
=
\begin{bmatrix}
A_d &amp; B_d^{(F)} \\
0 &amp; 1
\end{bmatrix}\]

<p>를 계산해 $B_d^{(F)}$를 얻고, $F=\mathrm{gear}\,u$ 관계로 $B_d = B_d^{(F)}\,\mathrm{gear}$로 변환해도 동일하다.</p>

<p>여기까지가 연속 모델 → 이론(이산) 모델을 만드는 과정이다. 이제 다음 단계에서는 (1) 이산화된 이론 행렬 $(A_d^{th}, B_d^{th})$과 (2) 시뮬레이터에서 수치 선형화로 얻은 $(A_d^{fd}, B_d^{fd})$를 각각 사용해 이산 LQR을 설계하고 성능/실패를 비교한다.</p>

<h3 id="시뮬레이션-수치-선형화">시뮬레이션 수치 선형화</h3>

<p>Mujoco에 들어가 있는 모델을 그대로 사용하여, 시뮬레이션 환경에 최적화 + 선형화된 $A_d^{fd}, B_d^{fd}$ 를 구했다.</p>

<p>시뮬레이션은 이산화 되어있기 때문에 추가로 이산화를 해줄 필요가 없다.</p>

<p>이산시간 시스템을</p>

\[\mathbf{x}_{k+1} = f(\mathbf{x}_k, u_k)\]

<p>라고 보고, 직립 평형점 $(\mathbf{x}^*, u^*)$ 주변에서 유한차분(Finite Difference)으로 자코비안을 근사한다.</p>

\[\begin{aligned}
A_d^{fd} &amp;\approx \left.\frac{\partial f}{\partial \mathbf{x}}\right|_{(\mathbf{x}^*,u^*)} \\
B_d^{fd} &amp;\approx \left.\frac{\partial f}{\partial u}\right|_{(\mathbf{x}^*,u^*)}
\end{aligned}\]

<p>즉, 상태의 각 성분을 아주 조금 $\varepsilon$ 만큼 흔들어 본 뒤, 그때의 다음 상태 변화량으로 기울기를 계산한다.</p>

\[A_d^{fd}[:,i] \approx \frac{f(\mathbf{x}^* + \varepsilon \mathbf{e}_i, u^*) - f(\mathbf{x}^* - \varepsilon \mathbf{e}_i, u^*)}{2\varepsilon},\qquad
B_d^{fd} \approx \frac{f(\mathbf{x}^*, u^*+\varepsilon) - f(\mathbf{x}^*, u^* - \varepsilon)}{2\varepsilon}\]

<p>아래는 실제 구현에서 핵심만 남긴 스니펫이다(관측 $\mathbf{x}=[x,\theta,\dot x,\dot\theta]$를 그대로 사용했다).</p>

<p>이렇게 얻은 $A_d^{fd}, B_d^{fd}$는 MuJoCo가 가진 실제 요소(마찰, 관성, 수치적분 오차 등)가 반영된 시뮬레이터 기준 선형 모델이다. 따라서 $A_d^{th},B_d^{th}$로 설계한 LQR과 $A_d^{fd},B_d^{fd}$로 설계한 LQR을 같은 조건에서 비교하면,</p>
<ul>
  <li>이론 모델 미스매치가 성능에 주는 영향</li>
  <li>포화/외란 조건에서의 실패 모드 차이</li>
</ul>

<p>를 정량적으로 분석해 볼 수 있다.</p>

<h3 id="결과---이론-이산모델-vs-시뮬-수치-선형화-모델의-a_db_dk-차이">결과 - 이론 이산모델 vs 시뮬 수치 선형화 모델의 $A_d,B_d,K$ 차이</h3>

<p>이론 모델 $A_d^{th}, B_d^{th}$과 시뮬레이터 수치 선형화 모델 $A_d^{fd}, B_d^{fd}$을 각각 만들고, 동일한 $Q,R$로 이산 LQR을 설계해 비교했다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">python -m experiments.run</code>을 두 모드(theory / fd)로 각각 실행했고, 각 실행에서 영상 1개 + 로그 1개(json)가 저장되었다.</li>
  <li>동시에 터미널에 $A_d, B_d$와 그로부터 계산된 LQR 이득 $K$를 출력하도록 하여, 두 모델의 차이가 설계 결과에 얼마나 반영되는지 확인했다.</li>
</ul>

<p>관측 상태는 동일하게 $\mathbf{x}=[x,\theta,\dot{x},\dot{\theta}]$를 사용하며, 아래는 터미널에서 확인한 값이다.</p>

<ul>
  <li>이론 모델 (theory)
    <ul>
      <li>$A_d^{th}\approx$
\(\begin{bmatrix} 1.0000 &amp; -0.0023 &amp; 0.0400 &amp; -0.0000 \\ 0.0000 &amp; 1.0240 &amp; 0.0000 &amp; 0.0403 \\ 0.0000 &amp; -0.1171 &amp; 1.0000 &amp; -0.0023 \\ 0.0000 &amp; 1.2053 &amp; 0.0000 &amp; 1.0240 \end{bmatrix}\)</li>
      <li>$B_d^{th}\approx [\,0.006700\ -0.015800\ 0.335309\ -0.793131\,]^\top$</li>
      <li>$K^{th}\approx [\,-0.3554\ -7.1830\ -0.7326\ -1.6947\,]$</li>
    </ul>
  </li>
  <li>시뮬 수치 선형화 (FD)
    <ul>
      <li>$A_d^{fd}\approx$
\(\begin{bmatrix} 1.0000 &amp; -0.0023 &amp; 0.0399 &amp; 0.0001 \\ 0.0000 &amp; 1.0234 &amp; 0.0002 &amp; 0.0387 \\ 0.0000 &amp; -0.1123 &amp; 0.9967 &amp; 0.0053 \\ 0.0000 &amp; 1.1573 &amp; 0.0076 &amp; 0.9450 \end{bmatrix}\)</li>
      <li>$B_d^{fd}\approx [\,0.006652\ -0.015364\ 0.331717\ -0.760593\,]^\top$</li>
      <li>$K^{fd}\approx [\,-0.3706\ -7.9560\ -0.7996\ -1.6728\,]$</li>
    </ul>
  </li>
</ul>

<p>정리하면, $A_d, B_d, K$ 의 상대오차가 순서대로 $3.95\%, 3.8\%, 10.46\%$ 있었고 $A_d,B_d$가 완전히 동일하지 않기 때문에(시뮬 내부의 마찰/관성/수치적분 때문에), 같은 $Q,R$을 쓰더라도 최종 LQR 이득 $K$가 달라진다. 이 차이가 이후의 외란/포화 조건에서 성능 차이(회복시간, 각도 RMS, 포화 빈도)로 이어지는지 실험으로 확인해보려 한다.</p>

<h3 id="이론-이산화-vs-시뮬-이산화">이론 이산화 vs 시뮬 이산화</h3>

<p><code class="language-plaintext highlighter-rouge">eval_sweep.py</code>와 <code class="language-plaintext highlighter-rouge">plot_results.py</code>라는 스크립트를 2개 만들어서 두 데이터를 추출/비교했다.</p>

<p><code class="language-plaintext highlighter-rouge">eval_sweep</code> 파일로 amp, seed, success, terminated_any, T, u_energy, sat_rate, sat_steps, sat_any, theta_max_post, theta_rms_post, recovery_time, steps, dt, u_max, t0, duration, kind, theta0 등을 계산하여 csv에 저장하였다.</p>

<p><code class="language-plaintext highlighter-rouge">plot_result</code> 파일로 위 데이터의 그래프를 그렸다.</p>

<p>다음과 같은 조건으로 데이터를 계산했다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> experiments.eval_sweep <span class="se">\</span>
  <span class="nt">--amps</span> 0,0.5,1,2,3,4,5 <span class="se">\</span>
  <span class="nt">--seeds</span> 20 <span class="se">\</span>
  <span class="nt">--disturbance-kind</span> impulse <span class="nt">--t0</span> 200 <span class="se">\</span>
  <span class="nt">--theta0</span> 0.2 <span class="se">\</span>
  <span class="nt">--out</span> logs/summary.csv
</code></pre></div></div>

<p>이후 plot을 통해 각 amp별 10번씩 시뮬레이션을 수행하여 결과를 시각화 해보았다.</p>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/recovery_time.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/sat_any_rate.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/success_rate.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/theta_max_post.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/theta_rms_post.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/u_energy.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
</table>

<h4 id="그래프-간단-설명">그래프 간단 설명</h4>

<ul>
  <li>
    <p><strong>recovery_time</strong>: 외란이 끝난 시점 이후로, $\vert \theta \vert$가 기준 임계값 $\theta_{\text{tol}}$ 아래로 들어가서 연속 $N$ 스텝 동안 유지될 때까지 걸린 시간 [s]. (성공 에피소드만 집계)</p>

    <ul>
      <li>amp=3 기준에서 LQR_fd : 0.96s, LQR_theory : 1.2s로 시뮬 이산화 조건에서 0.32s(약 25%) 빨랐다.</li>
    </ul>
  </li>
  <li>
    <p><strong>sat_any_rate</strong>: 에피소드 동안 한 번이라도 입력이 포화($\vert u \vert \ge u_{\max}$)에 걸린 경우의 비율</p>

    <ul>
      <li>amp=3 부터 입력이 포화된 경우가 생기기 시작했다.</li>
    </ul>
  </li>
  <li>
    <p><strong>success_rate</strong>: 종료 조건을 만족하지 않고(각도/레일 범위 이탈 등) 설정한 최대 스텝까지 안정적으로 유지한 에피소드 비율.</p>

    <ul>
      <li>특정 amp에서 포화되기는 했지만, 아직까지는 모두 제어에 성공했다.</li>
    </ul>
  </li>
  <li>
    <p><strong>theta_max_post</strong>: 외란 이후 구간에서의 $\vert \theta \vert$ 최대값의 평균 [rad]. (성공 에피소드만 집계)</p>
  </li>
  <li>
    <p><strong>theta_rms_post</strong>: 외란 이후 구간에서의 $\theta$ RMS(제곱평균제곱근) 값의 평균 [rad]. (성공 에피소드만 집계)</p>

    <ul>
      <li>LQR_fd : 0.006723 rad, LQR_theory : 0.007318 rad 으로 fd가 약 8.1% 더 작았다.</li>
    </ul>
  </li>
  <li>
    <p><strong>u_energy</strong>: 전체 구간에서 제어 입력 에너지로 $\sum_k u_k^2$ 를 사용한 값의 평균.</p>

    <ul>
      <li>LQR_fd : 12.217 rad, LQR_theory : 11.791 rad 으로 fd가 약 3.6% 더 컸다.</li>
      <li>fd가 더 공격적으로 에너지를 사용하고 더 빨리 안정권으로 복귀한다.</li>
    </ul>
  </li>
</ul>

<h4 id="중간-결론">중간 결론</h4>

<ul>
  <li>sat_any 그래프는 한 번이라도 포화면 1을 찍기 때문에 amp=3 이상에서 의미가 없어졌다. 대신 sat_steps와 sat_rate를 추가로 추출해서 같은 포화일 때, 얼마나 더 또는 덜 포화에 걸리는지 확인이 필요하다.</li>
  <li>amp를 0부터 5까지 주었지만 amp=3 이상부터 결과가 같게 나온것을 보니 시뮬레이션 내부적으로 제어 입력 클램핑이 amp=3 까지로 제한되어 있음을 예상할 수 있다.</li>
  <li>
    <p>지금 말할 수 있는 것</p>

    <ul>
      <li>동일한 성공률 100% 조건에서, 포화가 발생하는 amp&gt;3 구간에서, FD 기반의 LQR이 더 빠르게 회복하고, 각도 RMS가 더 작지만, 에너지를 더 쓴다.</li>
    </ul>
  </li>
  <li>
    <p>아직 낼 수 없는 결론</p>

    <ul>
      <li>amp가 커져도 계속 잘 버틴다.</li>
    </ul>
  </li>
</ul>

<h3 id="개선">개선</h3>

<ul>
  <li>
    <p>amp=3에서 시뮬레이션 내부적으로 제한이 걸리기 때문에, 내가 직접 외력을 정의해서 카트에 가해주는 방식으로 외란을 변경했다.</p>
  </li>
  <li>
    <p>한 번이라도 포화되었는지를 보는 sat_any를 구체화해서 포화된 비율(rate)과 연속적으로 포화된 스텝 수를 나타내는 그래프를 그렸다.</p>
  </li>
  <li>
    <p>총 비용 J를 계산해서 amp별로 그래프에 나타내었다.</p>
  </li>
  <li>
    <p>외력을 impulse 형태가 아닌 wind의 형태로 5 step 동안 지속적으로 작용하도록 하였다.</p>
  </li>
  <li>
    <p>포화가 일어나는 지점 근방의 amp로 세밀하게 나누어 그래프를 그렸다.</p>
  </li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python <span class="nt">-m</span> experiments.eval_sweep <span class="se">\</span>
  <span class="nt">--amps</span> 250,260,270,280,290 <span class="se">\</span>
  <span class="nt">--seeds</span> 10 <span class="se">\</span>
  <span class="nt">--disturbance-kind</span> window <span class="nt">--duration</span> 5 <span class="se">\</span>
  <span class="nt">--t0</span> 100 <span class="se">\</span>
  <span class="nt">--theta0</span> 0.0 <span class="se">\</span>
  <span class="nt">--out</span> logs/summary.csv
</code></pre></div></div>

<h2 id="결과">결과</h2>

<table>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/J_empF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/recovery_timeF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/sat_max_runF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/sat_rateF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/success_rateF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/theta_max_postF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
  <tr>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/theta_rms_postF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
    <td align="center"><img src="/assets/posts/2026-02-02-lqr_mpc_RL-1/u_energyF.png" style="width: 100%; max-width: 320px; height: auto;" /></td>
  </tr>
</table>

<ul>
  <li>
    <p>이론적 모델은 한계 상황에서 무너졌으나, 데이터 기반 모델은 살아남았다.</p>

    <ul>
      <li>외란 강도 amp=280에서 이론 LQR은 약 30%의 실패율을 보였으나, 시뮬 LQR은 100% 생존하며 더 높은 강건성을 보였다.</li>
    </ul>
  </li>
  <li>
    <p>살아남았더라도 제어의 질은 달랐다.</p>

    <ul>
      <li>
        <p>시뮬 LQR은 외란 강도가 증가해도 2초 내외로 회복했으나, 이론 LQR은 amp=270 이상에서 회복 시간이 급격히 증가했다.</p>
      </li>
      <li>
        <p>두 제어기의 최대 이탈 각도는 유사했음에도 불구하고, RMS 값은 차이가 컸다. 이론 LQR이 감쇠가 부족하여 도립으로 복귀하는 과정에서 불필요한 진동이 있었다는 것을 알 수 있다. 이 진동이 길게 지속되어 RMS에 값이 누적되었다.</p>
      </li>
    </ul>
  </li>
  <li>
    <p>amp=280에서 두 제어기의 최대 각도가 거의 동일하게 나타난 것은 성공한 경우만의 평균이기 때문이다.</p>

    <ul>
      <li>raw data 분석 결과, 이론 LQR은 실패한 경우에서 각도가 0.48rad 이상 올라갔으나, 성공 케이스는 시뮬 LQR과 비슷한 0.37rad 수준에서 멈췄다.</li>
    </ul>
  </li>
  <li>
    <p>왜 LQR만으로는 부족할까? (MPC를 사용하는 이유)</p>

    <ul>
      <li>
        <p>시뮬 LQR은 이론 LQR 대비 약 2~5% 더 많은 에너지를 소비했다. 그 결과 적극적인 제어 입력을 가해 과도 응답 구간을 단축시켰다.</p>
      </li>
      <li>
        <p>두 제어기의 총 비용 $J$는 비슷했지만, 미세한 잔류 진동이 $J$ 값에 크게 반영되지 않았기 때문이다. 실질적인 제어 안정감은 $J$ 값의 차이보다 컸다.</p>
      </li>
      <li>
        <p>amp=270 이상에서 두 제어기 모두 외란이 들어오는 5 step 동안 입력 포화 상태였다. LQR은 이런 입력 제한을 고려하지 못하므로, amp=290 이상의 외란에서는 물리적 한계로 인해 제어 불능 상태가 되었다.</p>
      </li>
    </ul>
  </li>
</ul>]]></content><author><name>Harang Ji</name></author><category term="Control" /><category term="LQR" /><summary type="html"><![CDATA[동기]]></summary></entry><entry><title type="html">[Pegasus] [완] 전압 클램핑 및 전류 제한 반영을 통한 이론-시뮬레이션 응답 일치화</title><link href="https://jiharangrang.github.io/robotics/control/2026/01/10/pegasus-5.html" rel="alternate" type="text/html" title="[Pegasus] [완] 전압 클램핑 및 전류 제한 반영을 통한 이론-시뮬레이션 응답 일치화" /><published>2026-01-10T00:00:00+09:00</published><updated>2026-01-10T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/control/2026/01/10/pegasus-5</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/control/2026/01/10/pegasus-5.html"><![CDATA[<h1 id="플랜트의-차이-발견">플랜트의 차이 발견</h1>

<p>지금 나의 시스템은 아래와 같이 플랜트가 제어량 $\delta$ 를 입력 받아서 전달함수를 거쳐 속도와 각속도를 출력하게 되어있다.</p>

<p>플랜트를 매트랩 제어기 이후의 Ros2, Gazebo 전 과정이라고 보고 피드백 구조를 만들었다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/1.png" alt="image.png" /></p>

<p>모터 플러그인을 사용했다보니 어쩔 수 없이 ros2와 gazebo에 들어가는 입력이 토픽 인터페이스에 맞춰서 속도, 각속도 또는 바퀴의 각속도의 형태였다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>이론 플랜트</th>
      <th>gazebo 플랜트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>입력</td>
      <td>$\delta$</td>
      <td>$v, w$</td>
    </tr>
    <tr>
      <td>출력</td>
      <td>$v, w$</td>
      <td>$v, w$</td>
    </tr>
  </tbody>
</table>

<p>플랜트의 입력이 내 이론 시스템과 달랐고 추가적으로 <strong>내부 모터 플러그인이 한 번 더 PI 제어</strong>를 자체적으로 수행하고 있었기 때문에 반응성이 이론 예상 값과 더 다르다는 생각이 들었다. 그래서 모터 제어 플러그인 내부와 gazebo와 matlab의 연결고리 역할을 해주는 ros2의 코드를 수정해서 실제 플랜트와 최대한 유사하게 바꿔보았다.</p>

<h2 id="ros2--모터-플러그인-코드-수정">Ros2 / 모터 플러그인 코드 수정</h2>

<p>우선 매트랩에서 PID를 거쳐 나온 $\delta$ 값을 ros2에서 디커플링후 배터리전압 $V_{in}$ 을 곱해서 모터 플러그인으로 넘겨주는 코드로 수정했다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/2.png" alt="image.png" /></p>

<ul>
  <li>Ros2에서 udp 입력을 $\delta_t, \delta_r$ 로 해석해서 $\delta_l = clamp(\delta_t-\delta_r),  \ \delta_r = clamp(\delta_t+\delta_r)$ 로 만든뒤 <code class="language-plaintext highlighter-rouge">/left_monotor/command/voltage, /right_motor/command/voltage</code> 로 그대로 publish 했다.</li>
  <li>
    <p>모터 플러그인 내부에 <code class="language-plaintext highlighter-rouge">command_mode = voltage</code>를 추가해서 PI 제어를 우회하고, 실제 마찰/점성/관성 계산만 처리되게 했다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-5/3.png" alt="image.png" /></p>
  </li>
  <li>
    <p>최종 시스템의 흐름은 아래와 같다.</p>
  </li>
  <li>MATLAB/Simulink
    <ul>
      <li>v_ref, w_ref → 오차 계산 → PID → 조작량 $\delta_t, \delta_r$  생성</li>
      <li>$\delta_t, \delta_r$ 를 UDP(3001)로 double 2개 전송</li>
    </ul>
  </li>
  <li>ROS2 브릿지
    <ul>
      <li>10ms마다 UDP 수신: ($\delta_t, \delta_r$)</li>
      <li>디커플링/믹싱: $\delta_L$ = clamp($\delta_t-\delta_r$), $\delta_R$ = clamp($\delta_t+\delta_r$)</li>
      <li>publish:
        <ul>
          <li><code class="language-plaintext highlighter-rouge">/left_motor/command/voltage</code> ← $\delta_L$</li>
          <li><code class="language-plaintext highlighter-rouge">/right_motor/command/voltage</code> ← $\delta_R$</li>
        </ul>
      </li>
      <li>동시에 /robot/odom를 구독해서 (pos, vel, pqr, euler, body_vel) 총 14개 double을 UDP(3002)로 MATLAB에 송신</li>
    </ul>
  </li>
  <li>Gazebo 모터 플러그인
    <ul>
      <li><code class="language-plaintext highlighter-rouge">/left_motor/command/voltage, /right_motor/command/voltage</code> 토픽에서 $\delta$ 수신(정규화)</li>
      <li>$\delta$ 를 [-1,1]로 clamp 후 $V = \delta * V_{in}$ 으로 실제 전압 생성</li>
      <li>내부 PI는 사용하지 않고, 전기/기계 모델로 전류/각속도 계산 → 토크를 링크에 적용</li>
    </ul>
  </li>
  <li>Gazebo 상태 → MATLAB
    <ul>
      <li>Gazebo p3d/odom → ROS2 브릿지 → UDP(3002) 14개 값 전송 → MATLAB이 받아 v_meas, w_meas 등으로 다음 스텝 제어에
  사용</li>
    </ul>
  </li>
</ul>

<h3 id="코드-수정-후-결과">코드 수정 후 결과</h3>

<p>이론과 같은 $K_p = 6, \ K_i = 15$ 을 적용시켜서 Gazebo 시뮬레이션을 돌려봤다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/4.png" alt="image.png" /></p>

<ul>
  <li>이론과 시뮬레이션이 비슷한 응답을 보이는 것을 확인할 수 있었다.</li>
  <li>오히려 시뮬레이션 환경에서 더 높은 오버슈트와 더 빠른 응답을 확인할 수 있었는데</li>
  <li>그 이유를 생각해봤다.
    <ul>
      <li>시뮬레이션에는 $\delta$ 를 -1에서 1까지 정규화 해두었기 때문에 최대 값이 1이라 모터 전압에 한계가 있지만,</li>
      <li>이론 응답에서는 모터가 무한대의 전압을 제공한다고 가정한 상황이기 때문에 일치하지 않았다.</li>
      <li>이론 시스템에도 -1부터 1까지 클램프를 줘서 응답이 변하게 수정해보았다.</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-5/5.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/6.png" alt="image.png" /></p>

<ul>
  <li>이론 시스템에도 클램프를 건 이후 이론 시스템의 오버슈트가 상승했고, 라이징 타임이 길어졌다.</li>
  <li>하지만 여전히 시뮬레이션 응답이 더 빠르게 나타났다.</li>
  <li>시뮬레이터 설정 확인 중에 최대 전류 제한이 없다는 것을 발견하였다.
    <ul>
      <li>$\delta = 1$ 일때, 48/4.18 = 11.5A 까지 전류가 올라가서 초기 토크가 약  $9.2 N \cdot m$로 약 1.44배 더 높게 나올 수 있어 risetime이 빨라질 수 있겠다는 생각이 들었다.</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-5/7.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/8.png" alt="image.png" /></p>

<ul>
  <li>성공했다!</li>
  <li>0.5 m/s 속도를 주었을 때, 시뮬레이션과 이론의 응답을 동기화 시켰다.</li>
  <li>이전에 초기 시뮬레이션에서 응답이 더 빨랐던 것은 전류가 실제 스펙보다 더 높게 흘렀기 때문이었다.</li>
  <li>우연일 수 있기 때문에 두세 번 비교 해보았다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-5/9.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-5/10.png" alt="image.png" /></p>

<ul>
  <li>임의로 0.7의 입력을 주었을 때도 동일하게 응답하는 것을 확인했다.</li>
  <li>이제 목표하는 시스템 요구 사항을 만족하기 위해서 오버슈트를 10% 이하로, $T_s = 1s$  이하로 튜닝하는 작업이 필요하다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-5/11.png" alt="image.png" /></p>

<ul>
  <li>0.7의 stepinput</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-5/12.png" alt="image.png" /></p>

<ul>
  <li>
    <p>0.4의 stepinput</p>
  </li>
  <li>앞서 했던 튜닝 방법을 사용하여 $K_p = 3, \ K_i = 1$ 로 선정하였다.</li>
  <li>
    <p>오버슈트가 사라지고 1차 시스템과 비슷한 응답이 나올 수 있었다.</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;0.7 stepinput data&gt;
  <span class="o">===</span> Gazebo Velocity Step Response <span class="o">===</span>
  RiseTime: 0.4592 s
  SettlingTime: 4.9527 s
  SettlingMin: 0.6341
  SettlingMax: 0.7263
  Overshoot: 4.7906%
  Undershoot: 4.4353%
  Peak: 0.7263
  PeakTime: 2.9900 s
    
  <span class="o">===</span> Ideal Velocity Step Response <span class="o">===</span>
  RiseTime: 0.4030 s
  SettlingTime: 0.7038 s
  SettlingMin: 0.6297
  SettlingMax: 0.6997
  Overshoot: 0.0005%
  Undershoot: <span class="nt">-0</span>.0000%
  Peak: 0.6997
  PeakTime: 4.9765 s
</code></pre></div>    </div>
  </li>
</ul>

<h1 id="결론">결론</h1>

<h2 id="프로젝트-성과">프로젝트 성과</h2>

<p>이 프로젝트에서는 아마존 페가수스 모바일 로봇을 대상으로 수학적 모델링 → PID 제어 설계 → Gazebo 시뮬레이션 검증까지 완전한 제어 시스템 개발 사이클을 경험할 수 있었다.</p>

<h3 id="주요성과">주요성과</h3>

<ul>
  <li>물리 기반 모델링 :
    <ul>
      <li>차체/바퀴의 병진/회전 운동방정식과 DC 모터 전달함수를 유도하고, 시스템 파라미터가 응답에 영향을 미치는 영향을 정량적으로 분석했다.</li>
    </ul>
  </li>
  <li>제어기 설계 및 검증 :
    <ul>
      <li>Matlab Root locus 방법으로 PI 제어기 파라미터 설계</li>
      <li>Simulink 시뮬레이션 결과와 Gazebo 물리 엔진 결과를 정량적으로 일치 (오차 5% 이내%)</li>
    </ul>
  </li>
  <li>시뮬레이션 환경 구축 :
    <ul>
      <li>SDF 포맷으로 로봇 모델 제작</li>
      <li>Ros2 - Gazebo - Matlab UDP 통신 파이프라인 구축</li>
      <li>실시간 제어 루프 구현 (100hz)</li>
    </ul>
  </li>
</ul>

<h3 id="주요-공학적-배움-돌아보기">주요 공학적 배움 돌아보기</h3>

<ul>
  <li><strong>문제 : 처음 설계한 PID 게인 $K_p=6,\ K_i=15$ 를 Gazebo에 적용했을 때 예상보다 응답이 느렸다.</strong></li>
  <li>원인 분석 :
    <ul>
      <li>모터 플러그인 내부에 자체적인 PI 제어기가 포함되어 있어서 2중 제어가 적용되고 있었다.</li>
      <li>플랜트에 들어가는 입력이 속도로 변환된 입력값이었다.</li>
    </ul>
  </li>
  <li>해결 :
    <ul>
      <li><code class="language-plaintext highlighter-rouge">command_mode = voltage</code>를 추가해서 커스텀 모드에서는 PI 제어를 우회</li>
      <li>이론 플랜트와 Gazebo 플랜트의 입력을 맞추기 위해 PID 출력 값인 $\delta_t, \delta_r$ 을 Gazebo에 그대로 Publish</li>
    </ul>
  </li>
  <li>배운점 :
    <ul>
      <li>마찰이나 다른 설정값이 문제라서 응답이 느린 줄 알고 가장 많은 시간을 낭비했다.</li>
      <li>제어를 용이하게 해주는 플러그인이지만, 사용할 때 내부적으로 구동 되는 원리를 먼저 파악하고 사용해야함을 느꼈다.</li>
    </ul>
  </li>
  <li><strong>병진 속도 응답에 회전 입력이 전혀 영향을 주지 않았다.</strong></li>
  <li>원인 분석 :
    <ul>
      <li>$V_R + V_L = V_{in}(\delta_t + \delta_r) + V_{in}(\delta_t - \delta_r) = 2V_{in}\delta_t$</li>
      <li>회전 성분이 합에서 자동으로 소거</li>
    </ul>
  </li>
  <li>물리적 의미 :
    <ul>
      <li>양쪽을 다르게 밀더라도, 로봇은 돌지만 전진력의 총합은 변하지 않는다.</li>
      <li>이 직교성 덕분에 병진과 회전의 독립적인 PID 제어가 가능했다.</li>
    </ul>
  </li>
  <li><strong>P3D 플러그인에 가우시안 노이즈를 줬더니 정지상태인데도 속도가 튀었다.</strong></li>
  <li>원인 분석 :
    <ul>
      <li>$v = \frac{x_k - x_{k-1}}{\Delta t} = \frac{(x + n_k) - (x + n_{k-1})}{\Delta t} = \frac{n_k - n_{k-1}}{\Delta t}$</li>
      <li>위치 노이즈 n이 작아도, 분모 $\Delta t$ 가 작은 값이면 노이즈가 증폭 된다.</li>
    </ul>
  </li>
  <li>해결 :
    <ul>
      <li>위치 미분 대신 휠 오도메트리로 속도를 직접 측정</li>
      <li>칼만 필터 적용</li>
    </ul>
  </li>
  <li>배운점 :
    <ul>
      <li>센서 융합의 중요성</li>
      <li>GPS 같은 위치 센서는 속도 추정에 부적합하고, IMU나 엔코더로 직접 측정 센서를 써야한다는 것을 배웠다.</li>
    </ul>
  </li>
  <li><strong>이론을 통해 PID 설계한 게인이 시뮬레이션에는 정확하게 맞지 않는다.</strong></li>
  <li>원인 분석 :
    <ul>
      <li>이론에는 마찰이나 점성이 반영되지 않았기 때문에 응답이 달랐다.</li>
      <li>통신 지연은 확인하지 못했지만 가능성이 있을 것 같다.</li>
    </ul>
  </li>
  <li>배운점 :
    <ul>
      <li>Trial &amp; Error 방법이 가장 쉽고 정확한 길이라는 걸 느꼈다. 실제 응답을 관찰하면서 미세 조정하는 감각이 조금 생긴 것 같다.</li>
    </ul>
  </li>
  <li>문제 : 이론 응답이 시뮬레이션 응답에 비해 반응성이 빨랐다.</li>
  <li>원인 분석 :
    <ul>
      <li>이론 시스템에는 무한대의 전압이 가능하게 설정되어 있었다.</li>
    </ul>
  </li>
  <li>해결 :
    <ul>
      <li>이론 시스템에도 오차를 클램핑 해서 실제 로봇 환경과 동일하게 맞춰주었다.</li>
    </ul>
  </li>
  <li>배운점 :
    <ul>
      <li>이론 시스템을 너무 이상적으로 만들어두면 지금 이론 시스템이 맞게 설계 되어있는지, 시뮬레이션 환경이 제대로 구성되어 있는 것인지 비교 할 수 없다는 것을 깨달았다.</li>
    </ul>
  </li>
</ul>

<h3 id="아쉬운-점">아쉬운 점</h3>

<ul>
  <li>한 가지 아쉬운 점이라면, 센서의 노이즈 설정을 빼고도 Gazebo에서 넘어오는 정보가 자글자글하게 튀는 형태를 가지고 있었는데 원인을 찾을 수 없었다.</li>
  <li>마찰 때문인지, 아니면 물리 엔진 자체의 수치해석상의 한계인지 알 수 없었다.</li>
  <li>어쩌면 센서 데이터는 항상 저런식으로 자글자글하게 들어오는 것인데 내가 경험이 부족해서 당연하게 못 느끼는 걸 수도 있겠다는 생각을 했다.</li>
</ul>

<h3 id="향후-발전-방향">향후 발전 방향</h3>

<ul>
  <li>센서 융합 : 현재는 물리 엔진이 제공하는 속도 정보만 피드백에 사용했지만, IMU 센서와 휠오도메트리를 추가하여 칼만 필터를 적용해 위치 추정의 정확도를 높여보고 싶다.</li>
  <li>실제 구현 : 시뮬레이션에서 검증된 알고리즘을 임베디드 보드에 탑재해서 실 주행에서는 또 어떤 결과가 나오는지 테스트 하고 싶다.</li>
</ul>

<h3 id="프로젝트를-마치며">프로젝트를 마치며</h3>

<p>“이론은 맞는데 왜 안 되지? → 아 시뮬레이터에 숨은 로직이 있었구나” 처럼 이론과 실제 적용 사이의 간과할 수 있는 내용들을 찾아가면서 배우는 것이 수업에서 A+ 를 받는 것보다 값지다는 것을 배웠다.</p>

<p>이런 깨달음의 순간들이 모여서 엔지니어의 직관이 된다고 생각했다.</p>

<p>이 기본 경험을 바탕으로 앞으로 더 복잡하고 정교한 로봇 시스템을 제어하는 엔지니어가 될 수 있었으면 좋겠다.</p>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="Control" /><category term="Control" /><summary type="html"><![CDATA[플랜트의 차이 발견]]></summary></entry><entry><title type="html">[Pegasus] 이론적 전달함수 모델링의 한계</title><link href="https://jiharangrang.github.io/robotics/control/2026/01/06/pegasus-4.html" rel="alternate" type="text/html" title="[Pegasus] 이론적 전달함수 모델링의 한계" /><published>2026-01-06T00:00:00+09:00</published><updated>2026-01-06T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/control/2026/01/06/pegasus-4</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/control/2026/01/06/pegasus-4.html"><![CDATA[<h2 id="ros2---matlab-통신-인터페이스">Ros2 - Matlab 통신 인터페이스</h2>

<p>각 환경에서 서버와 클라이언트가 잘 작동하는 것을 확인 했으니, 두 환경 사이의 통신을 확인할 차례이다.</p>

<p>먼저 Matlab에서 수신할 14개의 데이터를 각각 데이터 그룹별로 분리하는 다이어그램을 만들었다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-4/1.png" alt="image.png" /></p>

<ul>
  <li>14개의 데이터는
    <ul>
      <li>pos (x, y, z) : 지도 기준 현재 위치</li>
      <li>vel (vx, vy, vz) : 지도 기준 이동 속도</li>
      <li>pqr (wx, wy, wz) : 롤/피치/요 각도의 초당 각속도.
        <ul>
          <li>우리는 지상 로봇이기 때문에 wz 값을 주요하게 본다.</li>
        </ul>
      </li>
      <li>euler (roll, pitch, yaw) : 롤/피치/요 각도
        <ul>
          <li>ros2는 자세를 쿼터니언으로 4개의 숫자로 주는데, 직관적으로 롤/피치/요가 좋기 때문에 변환해서 사용한다.</li>
        </ul>
      </li>
      <li>body_vel : 전진 속도, 좌우 속도를 로봇 기준으로 나타낸 속도
        <ul>
          <li>지상 좌표계 기준이 아닌, 로봇이 자기가 보기에 앞으로 가면 전진, 뒤로 가면 후진이다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Ros2 파이썬에서 리틀에디안으로 값을 보내고 받고 있기 때문에 리틀 에디안을 사용한다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/2.gif" alt="image.gif" /></p>

<ul>
  <li>Matlab에서 송신한 속도 1, 각속도 3 데이터가 Ros2 터미널에 수신되었고</li>
  <li>
    <p>Ros2에서 송신한 값들도 Matlab에서 수신되는 것을 볼 수 있다.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>          <span class="bp">self</span><span class="p">.</span><span class="n">robot_pos</span> <span class="o">=</span> <span class="p">[</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">2.0</span><span class="p">,</span> <span class="mf">3.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">robot_vel</span> <span class="o">=</span> <span class="p">[</span><span class="mf">4.0</span><span class="p">,</span> <span class="mf">5.0</span><span class="p">,</span> <span class="mf">6.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">robot_body_vel</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">robot_pqr</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">euler</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">robot_cmd</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.0</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">]</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">linear_cmd</span> <span class="o">=</span> <span class="mf">0.0</span>
          <span class="bp">self</span><span class="p">.</span><span class="n">angular_cmd</span> <span class="o">=</span> <span class="mf">0.0</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="통신-테스트---문제-발생">통신 테스트 - 문제 발생</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-4/3.gif" alt="image.gif" /></p>

<ul>
  <li>제어를 적용하지 않고 단순히 음의 피드백만 적용된 시스템이다.</li>
  <li>step input을 0.5로 주고 실행했지만 로봇이 전진이 아닌 후진을 하기 시작했다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/4.png" alt="image.png" /></p>

<ul>
  <li>응답 그래프도 음수로 뒤집혀 있었다.</li>
</ul>

<h3 id="통신-테스트---문제-해결">통신 테스트 - 문제 해결</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-4/5.png" alt="image.png" /></p>

<ul>
  <li>문제는 모델링 좌표계였다. 바퀴의 회전 방향은 양의 z 축을 기준으로 한 오른손 법칙의 회전 방향이였다.</li>
  <li>
    <p>하지만 바퀴는 z축이 몸체 링크의 z축과 같은 초기 상태였고, x축을 기준으로 양의 roll 회전을 하고나니 전진 명령을 걸면 -x 축으로 회전하는 것이였다.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">&lt;</span><span class="n">link</span> <span class="n">name</span><span class="o">=</span><span class="s">"front_left_wheel"</span><span class="o">&gt;</span>
  		  <span class="o">&lt;</span><span class="n">pose</span><span class="o">&gt;</span><span class="mf">0.0</span> <span class="mf">0.25</span> <span class="mf">0.03</span> <span class="o">-</span><span class="mf">1.57079</span> <span class="mi">0</span> <span class="mi">0</span><span class="o">&lt;/</span><span class="n">pose</span><span class="o">&gt;</span>
</code></pre></div>    </div>
  </li>
  <li>롤 회전 방향을 음수로 바꿔주니 정상적으로 전진할 수 있게 되었다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/6.gif" alt="image.gif" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-4/7.png" alt="image.png" /></p>

<h3 id="pid-제어-적용">PID 제어 적용</h3>

<p>이제 플랜트 앞에 PID 제어기를 추가해서 응답이 어떻게 바뀌는 지 확인할 차례이다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-4/8.png" alt="image.png" /></p>

<ul>
  <li>블록 다이어그램은 위와 같은 피드백 구조로 구성했다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/9.png" alt="image.png" /></p>

<ul>
  <li>앞서 설계한 병진 운동의 PID 제어 파라미터를 적용했다.</li>
  <li>시스템에 이산적인 데이터가 들어오기 때문에 일반적인 미분/적분 블록을 사용하면 값이 정확하지 않다.</li>
  <li>
    <p>따라서 이산 미분/적분 블록을 사용하여 인풋 데이터에 맞게 PID 계산을 해주었다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-4/10.gif" alt="image.gif" /></p>
  </li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/11.png" alt="image.png" /></p>

<ul>
  <li>0.5의 스텝 인풋에 의해 1차 시스템과 유사하게 오버슈트 없이 0.5 주변으로 수렴하는 결과를 얻을 수 있다.</li>
  <li>전체적인 경향성은 예상대로 나왔지만, 자글 자글한 이유가 노이즈인지 시스템 다이어그램 설계 문제인지 궁금했다.</li>
  <li>
    <p>따라서 가우시안 노이즈 값을 0으로 설정해봤다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-4/12.png" alt="image.png" /></p>
  </li>
  <li>큰 차이가 없었다.</li>
  <li>가우시안 노이즈도 값이 작은 값인 0.01 표준 편차로 지정되어 있어서 크게 영향이 없었던 것이다.</li>
  <li>
    <p>다음으로 I 제어기 파라미터를 15에서 5로 줄여봤다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-4/13.png" alt="image.png" /></p>
  </li>
  <li>라이징 타임만 늘어났지 미세한 진동은 잡히지 않았다.</li>
  <li>진동의 원인을 찾아보는 도중 2가지 가능성을 알아냈다.
    <ol>
      <li>물리 엔진의 접촉/마찰 때문에 실제 속도가 미세하게 출렁이는 상황.
        <ol>
          <li>바퀴와 지면 접촉은 수치해석이라 상수 마찰계수로 고정되지 않는 경우 발생</li>
        </ol>
      </li>
      <li>샘플링 주기와 matlab/ros2의 타이머 주기가 맞지 않아서 잔떨림 발생</li>
    </ol>
  </li>
</ul>

<h3 id="값이-사라지는-현상">값이 사라지는 현상</h3>

<p>응답 출력 도중 그래프가 끊기면서 로그의 값들도 nan으로 바뀌며 로봇이 원점으로 순간이동 하는 현상이 있다.</p>

<p>어느 순간 cur 값이 매우 크게 커지면서 아래 로그를 내보낸다.</p>

<p>[ERROR]: Error receiving UDP data: The ‘data’ field must be a float in [-3.402823466e+38, 3.402823466e+38]</p>

<p>값이 지정 범위보다 커져서 생긴 문제이며 gazebo의 물리값이 수치적으로 불안정하다는 것을 의미한다.</p>

<p>$K_p$ 값이 큰 경우 오차를 더 크게 반영하므로 더 쉽게 불안정해지는 경향이 있었다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">INFO</span><span class="p">]:</span> <span class="n">Received</span> <span class="k">from</span> <span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">41356</span><span class="p">):</span> <span class="sa">b</span><span class="s">'</span><span class="se">\xfc\xa9</span><span class="s">`</span><span class="se">\xbf</span><span class="s">5</span><span class="se">\xdb\xfc</span><span class="s">?~d</span><span class="se">\xed</span><span class="s">$</span><span class="se">\x9d\xf0\xaf</span><span class="s">?'</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="mf">1.8119537873993377</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="mf">0.06435719878402678</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="mf">1.0991584502632518e+50</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="o">-</span><span class="mf">5.976224117146322e+51</span>
<span class="p">[</span><span class="n">INFO</span><span class="p">]:</span> <span class="n">Received</span> <span class="k">from</span> <span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">41356</span><span class="p">):</span> <span class="sa">b</span><span class="s">'X</span><span class="se">\xe5</span><span class="s">+A</span><span class="se">\xc3\xfd\xfc</span><span class="s">?</span><span class="se">\xff</span><span class="s"> </span><span class="se">\n\xa0\xb6</span><span class="s">y</span><span class="se">\xb0</span><span class="s">?'</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="o">-</span><span class="mf">1.0991584502632518e+51</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="mf">1.1952448234292644e+52</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">ERROR</span><span class="p">]:</span> <span class="n">Error</span> <span class="n">receiving</span> <span class="n">UDP</span> <span class="n">data</span><span class="p">:</span> <span class="n">The</span> <span class="s">'data'</span> <span class="n">field</span> <span class="n">must</span> <span class="n">be</span> <span class="n">a</span> <span class="nb">float</span> <span class="ow">in</span> <span class="p">[</span><span class="o">-</span><span class="mf">3.402823466e+38</span><span class="p">,</span> <span class="mf">3.402823466e+38</span><span class="p">]</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="o">-</span><span class="mf">1.1156458270172006e+51</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="mf">1.2311021681321425e+52</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">ERROR</span><span class="p">]:</span> <span class="n">Error</span> <span class="n">receiving</span> <span class="n">UDP</span> <span class="n">data</span><span class="p">:</span> <span class="n">The</span> <span class="s">'data'</span> <span class="n">field</span> <span class="n">must</span> <span class="n">be</span> <span class="n">a</span> <span class="nb">float</span> <span class="ow">in</span> <span class="p">[</span><span class="o">-</span><span class="mf">3.402823466e+38</span><span class="p">,</span> <span class="mf">3.402823466e+38</span><span class="p">]</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="o">-</span><span class="mf">1.1321332037711494e+51</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="mf">1.2669595128350202e+52</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">ERROR</span><span class="p">]:</span> <span class="n">Error</span> <span class="n">receiving</span> <span class="n">UDP</span> <span class="n">data</span><span class="p">:</span> <span class="n">The</span> <span class="s">'data'</span> <span class="n">field</span> <span class="n">must</span> <span class="n">be</span> <span class="n">a</span> <span class="nb">float</span> <span class="ow">in</span> <span class="p">[</span><span class="o">-</span><span class="mf">3.402823466e+38</span><span class="p">,</span> <span class="mf">3.402823466e+38</span><span class="p">]</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">INFO</span><span class="p">]:</span> <span class="n">Received</span> <span class="k">from</span> <span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">41356</span><span class="p">):</span> <span class="sa">b</span><span class="s">'</span><span class="se">\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff</span><span class="s">'</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">INFO</span><span class="p">]:</span> <span class="n">Received</span> <span class="k">from</span> <span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">41356</span><span class="p">):</span> <span class="sa">b</span><span class="s">'</span><span class="se">\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff</span><span class="s">'</span>
<span class="p">[</span><span class="n">cmd</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">cur</span><span class="p">]</span> <span class="n">linear</span> <span class="p">:</span>  <span class="n">nan</span>  <span class="n">angular</span> <span class="p">:</span>  <span class="n">nan</span>
<span class="p">[</span><span class="n">INFO</span><span class="p">]:</span> <span class="n">Received</span> <span class="k">from</span> <span class="p">(</span><span class="s">'127.0.0.1'</span><span class="p">,</span> <span class="mi">41356</span><span class="p">):</span> <span class="sa">b</span><span class="s">'</span><span class="se">\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff</span><span class="s">'</span>
</code></pre></div></div>

<p>검색해보니 물리 엔진이 발산함에 따라 발생하는 흔한 현상이라는 정보를 얻을 수 있었다.</p>

<h3 id="이상적인-제어와-시뮬레이션-환경-제어-비교">이상적인 제어와 시뮬레이션 환경 제어 비교</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-4/14.png" alt="image.png" /></p>

<ul>
  <li>이상적인 환경에서 설계한 전달함수로 pid 제어하는 다이어그램이다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/15.png" alt="image.png" /></p>

<ul>
  <li>시뮬레이션 환경에서 받아오는 값으로 pid 제어하는 다이어그램이다.</li>
</ul>

<p>이 두 값을 모두 Matlab 워크스페이스에 내보내서 그래프를 그려서 비교해보려고 한다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-4/16.png" alt="image.png" /></p>

<ul>
  <li>$K_p = 6, K_i = 15$ 인 경우 동일한 0.5의 step input을 넣었을 때 속도 응답이다.</li>
  <li>I 제어기의 영향으로 0.5로 $e_{ss}$ = 0 으로 수렴하는 것을 볼 수 있다.</li>
  <li>P 제어기의 $K_p$ 값이 너무 커서 그런지 공진처럼 미세 노이즈가 남아 있는 것을 볼 수 있다.</li>
  <li>왜 이런 차이가 발생할까?
    <ul>
      <li>모터 플러그인에 이미 제어기가 있어서 오차를 반영하여 전압을 만들고 [-1,1]로 클램프 한다.</li>
      <li>모터 플러그인의 모델링은 내가 시뮬링크로 모델링한 전달함수와 다르다.</li>
      <li>저항/인덕턴스/감쇠/관성에 의해 전류에서 토크로 즉시 바뀌지 못하고 에너지가 소모된다.</li>
      <li>바퀴와 지면 사이의 마찰이 있기 때문에 에너지가 소모되어 초기에 오버슈트가 잘 나지 않는다.</li>
    </ul>
  </li>
</ul>

<p>Gazebo 환경에서의 응답을 기준으로 노이즈를 줄이고 수렴속도를 높이기 위해 $K_p$ 와 $K_i$ 값을 수정해보았다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-4/17.png" alt="image.png" /></p>

<ul>
  <li>병진 속도는 $K_p = 1, K_i = 35$ 일때 오버슈트 없이 가장 안정적인 응답이 나왔다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-4/18.png" alt="image.png" /></p>

<ul>
  <li>회전 속도는 $K_p = 0.6, K_i = 20$ 일때 가장 안정적인 응답이 나왔다.</li>
  <li>수학적인 계산으로 1차 파라미터를 찾고 그 파라미터를 기준으로 실제 환경이나 시뮬레이션에 적용했을때, 이론적으로 찾은 파라미터가 최적이 아니였다.</li>
  <li>지글러-니콜스 방법 처럼 경험적으로 파라미터를 쉽게 찾는 등의 방법론이 있지만 결국 pid 제어에서는</li>
  <li>trial-error 방식으로 더 적합한 파라미터를 찾아 반복하는 과정이 제어기 설계의 핵심이라는 것을 배울 수 있었다.</li>
</ul>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="Control" /><category term="Control" /><summary type="html"><![CDATA[Ros2 - Matlab 통신 인터페이스 [ERROR]: Error receiving UDP data: The ‘data’ field must be a float in [-3.402823466e+38, 3.402823466e+38] [INFO]: Received from (‘127.0.0.1’, 41356): b’\xfc\xa9`\xbf5\xdb\xfc?~d\xed$\x9d\xf0\xaf?’ [INFO]: Received from (‘127.0.0.1’, 41356): b’X\xe5+A\xc3\xfd\xfc?\xff \n\xa0\xb6y\xb0?’ [ERROR]: Error receiving UDP data: The ‘data’ field must be a float in [-3.402823466e+38, 3.402823466e+38] [ERROR]: Error receiving UDP data: The ‘data’ field must be a float in [-3.402823466e+38, 3.402823466e+38] [ERROR]: Error receiving UDP data: The ‘data’ field must be a float in [-3.402823466e+38, 3.402823466e+38] [INFO]: Received from (‘127.0.0.1’, 41356): b’\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff’ [INFO]: Received from (‘127.0.0.1’, 41356): b’\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff’ [INFO]: Received from (‘127.0.0.1’, 41356): b’\x00\x00\x00\x00\x00\x00\xf8\xff\x00\x00\x00\x00\x00\x00\xf8\xff’]]></summary></entry><entry><title type="html">[Pegasus] Matlab-Ros2-Gazebo 통합 아키텍처</title><link href="https://jiharangrang.github.io/robotics/control/2026/01/03/pegasus-3.html" rel="alternate" type="text/html" title="[Pegasus] Matlab-Ros2-Gazebo 통합 아키텍처" /><published>2026-01-03T00:00:00+09:00</published><updated>2026-01-03T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/control/2026/01/03/pegasus-3</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/control/2026/01/03/pegasus-3.html"><![CDATA[<h1 id="gazebo--ros2-시뮬레이터">Gazebo / Ros2 시뮬레이터</h1>

<p>이제 명령을 내리기위한 컨트롤 용도로 Ros2를 사용, Gazebo 시뮬레이터를 사용해 시뮬레이션 환경에서 제어를 적용해 볼 것이다.</p>

<p>Ros2는 사용해본 경험이 있지만, Gazebo는 경험이 없어서 설치부터 환경세팅까지 정리 한 후 로봇을 모델링했다.</p>

<p>로봇 모델링은 아래와 같은 모델 코드로 구현했고, 외장 디자인과 바퀴등은 인터넷 자료를 활용했다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/1.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/2.png" alt="활용한 인터넷 자료 디자인" /></p>

<p>활용한 인터넷 자료 디자인</p>

<h2 id="모델-제작-과정">모델 제작 과정</h2>

<details>
  <summary>모델 코드 model.sdf</summary>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;?xml version='1.0'?&gt;
    &lt;sdf version='1.7'&gt;
      &lt;model name='mobilebot'&gt;
        &lt;link name="base_link"&gt;
          &lt;inertial&gt;
            &lt;mass&gt;102&lt;/mass&gt;
            &lt;inertia&gt;
                &lt;ixx&gt;3.3668&lt;/ixx&gt;
                &lt;ixy&gt;0&lt;/ixy&gt;
                &lt;ixz&gt;0&lt;/ixz&gt;
                &lt;iyy&gt;5.0881&lt;/iyy&gt;
                &lt;iyz&gt;0&lt;/iyz&gt;
                &lt;izz&gt;7.8412&lt;/izz&gt;
            &lt;/inertia&gt;
          &lt;/inertial&gt;
          &lt;collision name="collision_base"&gt;
            &lt;geometry&gt;
              &lt;box&gt;
    			&lt;size&gt;0.6 0.4 0.05&lt;/size&gt;
              &lt;/box&gt;
            &lt;/geometry&gt;
          &lt;/collision&gt;
    	    &lt;visual name="visual_base"&gt;
            &lt;geometry&gt;
              &lt;box&gt;
    			&lt;size&gt;0.6 0.4 0.05&lt;/size&gt;
              &lt;/box&gt;
            &lt;/geometry&gt;
            &lt;material&gt;
              &lt;ambient&gt;0.8 0.8 0.8 1&lt;/ambient&gt;
              &lt;diffuse&gt;0.3 0.3 0.3 1&lt;/diffuse&gt;
              &lt;specular&gt;0.1 0.1 0.1 1&lt;/specular&gt;
              &lt;emissive&gt;0.0 0.0 0.0 1&lt;/emissive&gt;
            &lt;/material&gt;
          &lt;/visual&gt;
    	  &lt;/link&gt;
    
        &lt;link name="front_right_hinge"&gt;
    			&lt;pose&gt;0.0 -0.215 0.025 0 0 0&lt;/pose&gt;
    		  &lt;inertial&gt;
    				&lt;mass&gt;0.1&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;0.01&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;0.01&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;0.01&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    			&lt;visual name="visual_front_right_hinge"&gt;
    				&lt;geometry&gt;
    					&lt;box&gt;
    						&lt;size&gt;0.03 0.03 0.1&lt;/size&gt;
    					&lt;/box&gt;
    				&lt;/geometry&gt;
    				&lt;material&gt;
    					&lt;ambient&gt;0.8 0.8 0.8 1&lt;/ambient&gt;
    					&lt;diffuse&gt;0.6 0.6 0.6 1&lt;/diffuse&gt;
    					&lt;specular&gt;0.1 0.1 0.1 1&lt;/specular&gt;
    					&lt;emissive&gt;0.0 0.0 0.0 1&lt;/emissive&gt;
    				&lt;/material&gt;
    			&lt;/visual&gt;
    	  &lt;/link&gt;
    
        &lt;joint name="front_right_axis_joint" type="fixed"&gt;
    			&lt;child&gt;front_right_hinge&lt;/child&gt;
    			&lt;parent&gt;base_link&lt;/parent&gt;
    		&lt;/joint&gt; 
    
        &lt;link name="front_left_hinge"&gt;
    			&lt;pose&gt;0.0 0.215 0.025 0 0 0&lt;/pose&gt;
    		  &lt;inertial&gt;
    				&lt;mass&gt;0.1&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;0.01&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;0.01&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;0.01&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    			&lt;visual name="visual_front_left_hinge"&gt;
    				&lt;geometry&gt;
    					&lt;box&gt;
    						&lt;size&gt;0.03 0.03 0.1&lt;/size&gt;
    					&lt;/box&gt;
    				&lt;/geometry&gt;
    				&lt;material&gt;
    					&lt;ambient&gt;0.8 0.8 0.8 1&lt;/ambient&gt;
    					&lt;diffuse&gt;0.6 0.6 0.6 1&lt;/diffuse&gt;
    					&lt;specular&gt;0.1 0.1 0.1 1&lt;/specular&gt;
    					&lt;emissive&gt;0.0 0.0 0.0 1&lt;/emissive&gt;
    				&lt;/material&gt;
    			&lt;/visual&gt;
    		&lt;/link&gt;
    
            &lt;joint name="front_left_axis_joint" type="fixed"&gt;
    			&lt;child&gt;front_left_hinge&lt;/child&gt;
    			&lt;parent&gt;base_link&lt;/parent&gt;
    		&lt;/joint&gt; 
    
    &lt;link name="front_left_wheel"&gt;
    		  &lt;pose&gt;0.0 0.25 0.03 1.57079 0 0&lt;/pose&gt;
    		  &lt;inertial&gt;
    				&lt;mass&gt;4.0&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;0.0064&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;0.0064&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;0.0128&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		  &lt;collision name="collision_front_left_wheel"&gt;
    		  	&lt;pose&gt;0 0 0 0 0 0&lt;/pose&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.05&lt;/length&gt;
    						&lt;radius&gt;0.08&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			  &lt;!--max_contacts&gt;1&lt;/max_contacts--&gt;
    				&lt;surface&gt;
    		      &lt;friction&gt;
    		        &lt;ode&gt;
    		          &lt;mu&gt;20.0&lt;/mu&gt;
    		          &lt;mu2&gt;20.0&lt;/mu2&gt;
    		          &lt;fdir1&gt;1 0 0&lt;/fdir1&gt;
    		          &lt;slip1&gt;0&lt;/slip1&gt;
    		          &lt;slip2&gt;0.001&lt;/slip2&gt;		          
    		        &lt;/ode&gt;
    		      &lt;/friction&gt;
    		      &lt;contact&gt;
                &lt;ode&gt;
                  &lt;kp&gt;1000000000000000000000000000.0&lt;/kp&gt;
                  &lt;kd&gt;1000000000000000000000000000.0&lt;/kd&gt;
                &lt;/ode&gt;
              &lt;/contact&gt;
    			  &lt;/surface&gt;
    		  &lt;/collision&gt;
    			&lt;visual name="visual_front_left_wheel"&gt;
       			&lt;pose&gt;0.0 0.0 0.0 1.57079 0 0&lt;/pose&gt;
    		    &lt;geometry&gt;
    		      &lt;mesh&gt;
    		        &lt;uri&gt;model://mobilebot/meshes/wheel.dae&lt;/uri&gt;
    		        &lt;scale&gt;0.45 0.45 0.45&lt;/scale&gt;
    		      &lt;/mesh&gt;
    		    &lt;/geometry&gt;
    		  &lt;/visual&gt;
    		&lt;/link&gt;
    
        &lt;joint name="front_left_wheel_joint" type="revolute"&gt;
    			&lt;child&gt;front_left_wheel&lt;/child&gt;
    			&lt;parent&gt;front_left_hinge&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 0 1&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;2.0&lt;/effort&gt;
    				&lt;velocity&gt;3.0&lt;/velocity&gt;			
    			&lt;/limit&gt;
    			&lt;joint_properties&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/joint_properties&gt;			
    		&lt;/joint&gt;
    
        &lt;link name="front_right_wheel"&gt;
    			&lt;pose&gt;0.0 -0.25 0.03 1.57079 0 0&lt;/pose&gt;
    		  &lt;inertial&gt;
    				&lt;mass&gt;4.0&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;0.0064&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;0.0064&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;0.0128&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    			&lt;collision name="collision_front_right_wheel"&gt;
    			  &lt;pose&gt;0 0 0 0 0 0&lt;/pose&gt;
    			  &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.05&lt;/length&gt;
    						&lt;radius&gt;0.08&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    		    &lt;!--max_contacts&gt;1&lt;/max_contacts--&gt;
    				&lt;surface&gt;
    		      &lt;friction&gt;
    		        &lt;ode&gt;
    		          &lt;mu&gt;20.0&lt;/mu&gt;
    		          &lt;mu2&gt;20.0&lt;/mu2&gt;
    		          &lt;fdir1&gt;1 0 0&lt;/fdir1&gt;
    		          &lt;slip1&gt;0&lt;/slip1&gt;
    		          &lt;slip2&gt;0.001&lt;/slip2&gt;		          
    		        &lt;/ode&gt;
    		      &lt;/friction&gt;
    		      &lt;contact&gt;
                &lt;ode&gt;
                  &lt;!--min_depth&gt;0.001&lt;/min_depth--&gt;
                  &lt;kp&gt;1000000000000000000000000000.0&lt;/kp&gt;
                  &lt;kd&gt;1000000000000000000000000000.0&lt;/kd&gt;
                &lt;/ode&gt;
              &lt;/contact&gt;
    			  &lt;/surface&gt;
    			&lt;/collision&gt;
    			&lt;visual name="visual_front_right_wheel"&gt;
       			&lt;pose&gt;0.0 0.0 0.0 1.57079 0 0&lt;/pose&gt;			
    		    &lt;geometry&gt;
    		      &lt;mesh&gt;
    		        &lt;uri&gt;model://mobilebot/meshes/wheel.dae&lt;/uri&gt;
    		        &lt;scale&gt;0.45 0.45 0.45&lt;/scale&gt;
    		      &lt;/mesh&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    		&lt;/link&gt;
    
        &lt;joint name="front_right_wheel_joint" type="revolute"&gt;
    			&lt;child&gt;front_right_wheel&lt;/child&gt;
    			&lt;parent&gt;front_right_hinge&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 0 1&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;2.0&lt;/effort&gt;
    				&lt;velocity&gt;3.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;joint_properties&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/joint_properties&gt;			
    		&lt;/joint&gt;
    
        &lt;link name="front_yaw_link"&gt;
    			&lt;pose&gt;0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_front_yaw_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_front_yaw_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="front_yaw_joint" type="revolute"&gt;
          &lt;child&gt;front_yaw_link&lt;/child&gt;
    			&lt;parent&gt;base_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 0 1&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
    
    &lt;link name="front_roll_link"&gt;
    			&lt;pose&gt;0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_front_roll_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_front_roll_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="front_roll_joint" type="revolute"&gt;
          &lt;child&gt;front_roll_link&lt;/child&gt;
    			&lt;parent&gt;front_yaw_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;1 0 0&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
    		
    		&lt;link name="front_pitch_link"&gt;
    			&lt;pose&gt;0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_front_pitch_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;sphere&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/sphere&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_front_pitch_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;sphere&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/sphere&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="front_pitch_joint" type="revolute"&gt;
          &lt;child&gt;front_pitch_link&lt;/child&gt;
    			&lt;parent&gt;front_roll_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 1 0&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
    
    &lt;link name="back_yaw_link"&gt;
    			&lt;pose&gt;-0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_back_yaw_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_back_yaw_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="back_yaw_joint" type="revolute"&gt;
          &lt;child&gt;back_yaw_link&lt;/child&gt;
    			&lt;parent&gt;base_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 0 1&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
    		
    		&lt;link name="back_roll_link"&gt;
    			&lt;pose&gt;-0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_back_roll_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_back_roll_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;cylinder&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/cylinder&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="back_roll_joint" type="revolute"&gt;
          &lt;child&gt;back_roll_link&lt;/child&gt;
    			&lt;parent&gt;back_yaw_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;1 0 0&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
    		
    		&lt;link name="back_pitch_link"&gt;
    			&lt;pose&gt;-0.25 0 -0.04 0 1.5707 1.5707&lt;/pose&gt;			
    			&lt;visual name="visual_back_pitch_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;sphere&gt;
    		        &lt;length&gt;0.005&lt;/length&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/sphere&gt;
    		    &lt;/geometry&gt;
    			&lt;/visual&gt;
    			&lt;collision name="collision_back_pitch_link"&gt;
    		    &lt;geometry&gt;
    		      &lt;sphere&gt;
    		        &lt;radius&gt;0.01&lt;/radius&gt;
    		      &lt;/sphere&gt;
    		    &lt;/geometry&gt;
    			&lt;/collision&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.01&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;5.145833333333334e-06&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;5.145833333333334e-06&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;1.0125000000000003e-04&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		&lt;/link&gt;
    		
        &lt;joint name="back_pitch_joint" type="revolute"&gt;
          &lt;child&gt;back_pitch_link&lt;/child&gt;
    			&lt;parent&gt;back_roll_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 1 0&lt;/xyz&gt;
    			&lt;/axis&gt;
    			&lt;limit&gt;
    				&lt;effort&gt;1000.0&lt;/effort&gt;
    				&lt;velocity&gt;100.0&lt;/velocity&gt;				
    			&lt;/limit&gt;
    			&lt;dynamics&gt;
    				&lt;damping&gt;0.0&lt;/damping&gt;
    				&lt;friction&gt;0.001&lt;/friction&gt;				
    			&lt;/dynamics&gt;
        &lt;/joint&gt;
        
        &lt;link name="cover"&gt;
    			&lt;pose&gt;0.0 -0.0 -0.02 0 0 1.57079&lt;/pose&gt;
    			&lt;inertial&gt;
    				&lt;mass&gt;0.0001&lt;/mass&gt;
    				&lt;inertia&gt;
    					&lt;ixx&gt;0.00001&lt;/ixx&gt;
    					&lt;ixy&gt;0&lt;/ixy&gt;
    					&lt;ixz&gt;0&lt;/ixz&gt;
    					&lt;iyy&gt;0.00001&lt;/iyy&gt;
    					&lt;iyz&gt;0&lt;/iyz&gt;
    					&lt;izz&gt;0.00001&lt;/izz&gt;
    				&lt;/inertia&gt;
    			&lt;/inertial&gt;
    		  &lt;visual name="visual_cover"&gt;
    		    &lt;geometry&gt;
    		      &lt;mesh&gt;
    		        &lt;uri&gt;model://mobilebot/meshes/robot.dae&lt;/uri&gt;
    		        &lt;scale&gt;0.26 0.25 0.32&lt;/scale&gt;
    		      &lt;/mesh&gt;
    		    &lt;/geometry&gt;
    		  &lt;/visual&gt;
    		&lt;/link&gt;
    		&lt;joint name="cover_joint" type="fixed"&gt;
    			&lt;child&gt;cover&lt;/child&gt;
    			&lt;parent&gt;base_link&lt;/parent&gt;
    			&lt;axis&gt;
    				&lt;xyz&gt;0 0 1&lt;/xyz&gt;
    			&lt;/axis&gt;
    		&lt;/joint&gt;
    
        &lt;plugin name="libgazebo_ros_p3d" filename="libgazebo_ros_p3d.so"&gt;
          &lt;ros&gt;
            &lt;namespace&gt;robot&lt;/namespace&gt;
            &lt;remapping&gt;odom:=odom&lt;/remapping&gt;
          &lt;/ros&gt;
          &lt;frame_name&gt;map&lt;/frame_name&gt;
          &lt;body_name&gt;base_link&lt;/body_name&gt;
          &lt;update_rate&gt;30.0&lt;/update_rate&gt;
          &lt;gaussian_noise&gt;10&lt;/gaussian_noise&gt;
          &lt;xyzOffsets&gt;0 0 0&lt;/xyzOffsets&gt;
    		  &lt;rpyOffsets&gt;0 0 0&lt;/rpyOffsets&gt;
        &lt;/plugin&gt;
    
        &lt;plugin name="left_motor" filename="libgazebo_ros_dc_motor.so"&gt;
          &lt;motor_shaft_joint&gt;front_left_wheel_joint&lt;/motor_shaft_joint&gt;
          &lt;motor_wrench_frame&gt;front_left_wheel&lt;/motor_wrench_frame&gt;
          
          &lt;parameter type="bool" name="publish_encoder"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_velocity"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_current"&gt;false&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_motor_joint_state"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_load"&gt;false&lt;/parameter&gt;
    
          &lt;parameter type="double" name="update_rate"&gt;1000&lt;/parameter&gt;
          &lt;parameter type="double" name="motor_nominal_voltage"&gt;48&lt;/parameter&gt;
          &lt;parameter type="double" name="moment_of_inertia"&gt;0.00001&lt;/parameter&gt;
          &lt;parameter type="double" name="armature_damping_ratio"&gt;0.0001&lt;/parameter&gt;
          &lt;parameter type="double" name="electromotive_force_constant"&gt;0.0322&lt;/parameter&gt;
          &lt;parameter type="double" name="electric_resistance"&gt;4.18&lt;/parameter&gt;
          &lt;parameter type="double" name="electric_inductance"&gt;0.012&lt;/parameter&gt;
          &lt;parameter type="double" name="gear_ratio"&gt;25.0&lt;/parameter&gt;
          &lt;parameter type="double" name="encoder_ppr"&gt;100&lt;/parameter&gt;
          &lt;parameter type="double" name="velocity_noise"&gt;0.0&lt;/parameter&gt;
          &lt;parameter type="double" name="kp_v"&gt;2.0&lt;/parameter&gt;
          &lt;parameter type="double" name="ki_v"&gt;0.0&lt;/parameter&gt;
        &lt;/plugin&gt;
        
        &lt;plugin name="right_motor" filename="libgazebo_ros_dc_motor.so"&gt;    
          &lt;motor_shaft_joint&gt;front_right_wheel_joint&lt;/motor_shaft_joint&gt;
          &lt;motor_wrench_frame&gt;front_right_wheel&lt;/motor_wrench_frame&gt;
    
          &lt;parameter type="bool" name="publish_encoder"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_velocity"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_current"&gt;false&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_motor_joint_state"&gt;true&lt;/parameter&gt;
          &lt;parameter type="bool" name="publish_load"&gt;false&lt;/parameter&gt;
    
          &lt;parameter type="double" name="update_rate"&gt;1000&lt;/parameter&gt;
          &lt;parameter type="double" name="motor_nominal_voltage"&gt;48&lt;/parameter&gt;
          &lt;parameter type="double" name="moment_of_inertia"&gt;0.00001&lt;/parameter&gt;
          &lt;parameter type="double" name="armature_damping_ratio"&gt;0.0001&lt;/parameter&gt;
          &lt;parameter type="double" name="electromotive_force_constant"&gt;0.0322&lt;/parameter&gt;
          &lt;parameter type="double" name="electric_resistance"&gt;4.18&lt;/parameter&gt;
          &lt;parameter type="double" name="electric_inductance"&gt;0.012&lt;/parameter&gt;
          &lt;parameter type="double" name="gear_ratio"&gt;25.0&lt;/parameter&gt;
          &lt;parameter type="double" name="encoder_ppr"&gt;100&lt;/parameter&gt;
          &lt;parameter type="double" name="velocity_noise"&gt;0.0&lt;/parameter&gt;
          &lt;parameter type="double" name="kp_v"&gt;2.0&lt;/parameter&gt;
          &lt;parameter type="double" name="ki_v"&gt;0.0&lt;/parameter&gt;
        &lt;/plugin&gt;
        
        &lt;static&gt;0&lt;/static&gt;
        &lt;allow_auto_disable&gt;1&lt;/allow_auto_disable&gt;
      &lt;/model&gt;
    &lt;/sdf&gt;
    
</code></pre></div>  </div>
</details>

<ul>
  <li>위 모델 코드를 사용해서 각 link별로 크기와 위치를 선정</li>
  <li>각 link와 몸체 base_link를 joint를 사용해서 연결</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-3/3.jpg" alt="image.jpg" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/4.jpg" alt="image.jpg" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/5.jpg" alt="image.jpg" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/6.jpg" alt="image.jpg" /></p>

<h2 id="ros2-통신-과정">Ros2 통신 과정</h2>

<ul>
  <li>플러그인은 Gazebo 물리 엔진과 ROS2 노드를 이어주는 역할을 한다.</li>
  <li>하드웨어 대신 Gazebo에서 동일한 인터페이스를 위해 Gazebo 플러그인을 사용했다.
    <ul>
      <li>구체적으로, 이동 로봇의 구동부는 플러그인이 <code class="language-plaintext highlighter-rouge">/cmd_vel</code> 토픽으로 들어오는 <code class="language-plaintext highlighter-rouge">geometry_msgs/Twist</code> 메시지를 속도로 변환해주고, 센서 플러그인은 Gazeb0에서 계산된 상태를 ROS2 토픽으로 퍼블리시한다.</li>
    </ul>
  </li>
  <li>
    <p>로봇의 현재 상태 정보를 얻기 위한 P3D 플러그인을 적용</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;plugin name="libgazebo_ros_p3d" filename="libgazebo_ros_p3d.so"&gt;
        &lt;ros&gt;
          &lt;namespace&gt;robot&lt;/namespace&gt;
          &lt;remapping&gt;odom:=odom&lt;/remapping&gt;
        &lt;/ros&gt;
        &lt;frame_name&gt;map&lt;/frame_name&gt;
        &lt;body_name&gt;base_link&lt;/body_name&gt;
        &lt;update_rate&gt;30.0&lt;/update_rate&gt;
        &lt;gaussian_noise&gt;0.01&lt;/gaussian_noise&gt;
        &lt;xyzOffsets&gt;0 0 0&lt;/xyzOffsets&gt;
  		  &lt;rpyOffsets&gt;0 0 0&lt;/rpyOffsets&gt;
      &lt;/plugin&gt;
</code></pre></div>    </div>

    <ul>
      <li>몸체 link의 위치와 자세를 읽어서 ROS2로 Pose/Odometry를 퍼블리시 해주는 플러그인이다.</li>
      <li><code class="language-plaintext highlighter-rouge">&lt;frame_name&gt;map&lt;/frame_name&gt;</code> 를 통해 map 기준으로 몸체 link의 위치를 퍼블리시 했다.
        <ul>
          <li>Gazebo는 기본적으로 world 프레임이 있는데, 그걸 ROS2 기준의 프레임으로 대응시켰다.</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">&lt;update_rate&gt;30.0&lt;/update_rate&gt;</code> 현실적인 센서 업데이트 주기 중 일반적인 수준으로 맞췄다.</li>
      <li><code class="language-plaintext highlighter-rouge">&lt;gaussian_noise&gt;0.01&lt;/gaussian_noise&gt;</code> 퍼블리시 되는 위치나 자세 값에 랜덤오차를 0.01 정도 표준편차를 주었다.
        <ul>
          <li>0은 너무 이상적이라고 생각했다. 실세 노이즈를 어느 정도 반영한 상태에서 제어기를 설계하고 싶었다.</li>
          <li>정지 상태에서는 실제 위치 x는 같다. 측정된 위치는 실제 위치에 임의로 노이즈를 더하기 때문에 값이 위아래로 튄다.</li>
          <li>$v = \frac{x_k - x_{k-1}}{\Delta t}$ 에서 $x_k = x + n_k$ 이 k 시점의 실제 위치에 노이즈를 더한 것이고, $v = \frac{n_k - n_{k-1}}{\Delta t}$ 가 되기 때문에 속도는 노이즈의 변화율이 된다.</li>
          <li>분모의 $\Delta t$ 가 매우 작기 때문에 노이즈의 변화량이 증폭되어 정지상태여도 빠른 주행중인 것처럼 보인다.</li>
          <li>따라서 <strong>위치 기반 속도 추정은 노이즈에 매우 민감</strong>하고 우리는 휠 오도메트리를 사용해서 속도를 직접 측정하면 이 증폭을 피할 수 있다. 속도를 직접 측정하니까 미분 과정이 없어서 노이즈가 순수 노이즈 크기만큼만 반영이 된다.</li>
          <li>소프트웨어적으로는 위치 센서를 위치 미분해서 얻지 않고 엔코터나 IMU 쪽에서만 얻게하거나, 필터링이나 스무딩을 한다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>
    <p>DC 모터에 파라미터를 할당하고 제어를 위한 오픈소스 플러그인을 적용</p>

    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  &lt;plugin name="left_motor" filename="libgazebo_ros_dc_motor.so"&gt;
        &lt;motor_shaft_joint&gt;front_left_wheel_joint&lt;/motor_shaft_joint&gt;
        &lt;motor_wrench_frame&gt;front_left_wheel&lt;/motor_wrench_frame&gt;
          
        &lt;parameter type="bool" name="publish_encoder"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_velocity"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_current"&gt;false&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_motor_joint_state"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_load"&gt;false&lt;/parameter&gt;
    
        &lt;parameter type="double" name="update_rate"&gt;1000&lt;/parameter&gt;
        &lt;parameter type="double" name="motor_nominal_voltage"&gt;48&lt;/parameter&gt;
        &lt;parameter type="double" name="moment_of_inertia"&gt;0.00001&lt;/parameter&gt;
        &lt;parameter type="double" name="armature_damping_ratio"&gt;0.0001&lt;/parameter&gt;
        &lt;parameter type="double" name="electromotive_force_constant"&gt;0.0322&lt;/parameter&gt;
        &lt;parameter type="double" name="electric_resistance"&gt;4.18&lt;/parameter&gt;
        &lt;parameter type="double" name="electric_inductance"&gt;0.012&lt;/parameter&gt;
        &lt;parameter type="double" name="gear_ratio"&gt;25.0&lt;/parameter&gt;
        &lt;parameter type="double" name="encoder_ppr"&gt;100&lt;/parameter&gt;
        &lt;parameter type="double" name="velocity_noise"&gt;0.0&lt;/parameter&gt;
        &lt;parameter type="double" name="kp_v"&gt;2.0&lt;/parameter&gt;
        &lt;parameter type="double" name="ki_v"&gt;0.0&lt;/parameter&gt;
      &lt;/plugin&gt;
        
      &lt;plugin name="right_motor" filename="libgazebo_ros_dc_motor.so"&gt;    
        &lt;motor_shaft_joint&gt;front_right_wheel_joint&lt;/motor_shaft_joint&gt;
        &lt;motor_wrench_frame&gt;front_right_wheel&lt;/motor_wrench_frame&gt;
    
        &lt;parameter type="bool" name="publish_encoder"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_velocity"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_current"&gt;false&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_motor_joint_state"&gt;true&lt;/parameter&gt;
        &lt;parameter type="bool" name="publish_load"&gt;false&lt;/parameter&gt;
    
        &lt;parameter type="double" name="update_rate"&gt;1000&lt;/parameter&gt;
        &lt;parameter type="double" name="motor_nominal_voltage"&gt;48&lt;/parameter&gt;
        &lt;parameter type="double" name="moment_of_inertia"&gt;0.00001&lt;/parameter&gt;
        &lt;parameter type="double" name="armature_damping_ratio"&gt;0.0001&lt;/parameter&gt;
        &lt;parameter type="double" name="electromotive_force_constant"&gt;0.0322&lt;/parameter&gt;
        &lt;parameter type="double" name="electric_resistance"&gt;4.18&lt;/parameter&gt;
        &lt;parameter type="double" name="electric_inductance"&gt;0.012&lt;/parameter&gt;
        &lt;parameter type="double" name="gear_ratio"&gt;25.0&lt;/parameter&gt;
        &lt;parameter type="double" name="encoder_ppr"&gt;100&lt;/parameter&gt;
        &lt;parameter type="double" name="velocity_noise"&gt;0.0&lt;/parameter&gt;
        &lt;parameter type="double" name="kp_v"&gt;2.0&lt;/parameter&gt;
        &lt;parameter type="double" name="ki_v"&gt;0.0&lt;/parameter&gt;
      &lt;/plugin&gt;
</code></pre></div>    </div>

    <ul>
      <li>모터 스펙은 앞서 정의한 값을 넣어줬다.</li>
      <li><code class="language-plaintext highlighter-rouge">update_rate</code> 는 1000HZ로 설정했다.
        <ul>
          <li>DC 모터는 동특성이 로봇 움직임보다 훨씬 빠르게 변하는 시스템이므로 외부 제어 주기보다 훨씬 높은 주파수로 작동하도록 했다.</li>
          <li>미세하게 힘을 조절해서 정밀한 제어가 가능한 장점도 있다.</li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">moment_of_inertia = 0.00001, armature_damping_ratio = 0.0001</code> 모터가 관성 모멘트가 없어서 무한대 가속도가 나오는 것을 방지했다.
        <ul>
          <li>입력과 동시에 바로 최고 속도가 나오지 않아야 실제 응답과 비슷하다고 생각했다.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="플러그인-적용-후">플러그인 적용 후</h3>

<p>실제 로봇이 없어도 Gazebo 환경에서 로봇이 구동되며 실시간으로 값을 받아볼 수 있게 되었다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/7.png" alt="image.png" /></p>

<ul>
  <li>모터에 대한 정보도 좌/우 따로 토픽으로 받을 수 있게 되었다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-3/8.gif" alt="image.gif" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/9.gif" alt="image.gif" /></p>

<ul>
  <li>한 쪽 바퀴에만 $1rad/s$ 에 해당하는 각속도를 넣어서 테스트 했다.</li>
</ul>

<h3 id="turtlebot3-패키지-연동">Turtlebot3 패키지 연동</h3>

<p>터틀봇3 패키지 내에 들어있는 teleop node를 활용해서 키보드로 로봇을 제어해봤다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/10.png" alt="image.png" /></p>

<ul>
  <li>키보드 값을 입력 받아서 topic으로 <code class="language-plaintext highlighter-rouge">/cmd_vel</code> 로 쏘면, Control node가 구독해서 모바일봇으로 다시 퍼블리시 해주는 구조이다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-3/11.gif" alt="image.gif" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[cmd] linear :  3.0  angular :  0.0
[cur] linear :  6.957660564507382  angular :  -4.336003365866704
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -11.48909999033403  angular :  -2.4905442053146474
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  15.354914321095018  angular :  3.460247668695481
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -2.017019213756047  angular :  4.682560264096943
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  17.38343493116724  angular :  3.7183501743370213
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -1.796576020986112  angular :  9.611881336718003
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -3.5824107926800606  angular :  -13.074454957071762
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -13.872253149371469  angular :  -0.642756694489865
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  2.166972600360915  angular :  7.851183712573045
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  -19.907223824591874  angular :  20.055677520612377
[cmd] linear :  3.0  angular :  0.0
[cur] linear :  3.1564870765572213  angular :  -9.889990927134317

</code></pre></div></div>

<ul>
  <li>정상적인 주행 상황에서 가끔씩 값이 크게 튀는 로그가 발견되었다. 실제 로봇은 부드럽게 움직였지만 로그의 속도와 각속도는 그렇지 않았다.</li>
</ul>

<h3 id="튀는-로그-분석">튀는 로그 분석</h3>

<p><code class="language-plaintext highlighter-rouge">cmd</code> 는 정상적으로 3과 0으로 유지되는데 <code class="language-plaintext highlighter-rouge">cur</code> 값은 한 번씩 값이 크게 튀는 현상이 있었다. 노이즈 값이 크게 반영되었거나 속도이기 때문에 위치를 시간으로 나누는 과정에서 값이 튀었을 것으로 예상하고 원인을 찾아보았다.</p>

<ul>
  <li>후보
    <ul>
      <li>차분의 분모 dt가 작아져서 값이 순간적으로 크게 보이는 경우
        <ul>
          <li>now() 로 dt를 잡고, 데이터는 stamp 기반일때 시간축이 섞여서
            <ul>
              <li>측정이 찍힌 시간은 stamp로, 콜백이 실행된 시간을 now()로 얻게 되면 위치변화는 stamp 흐름을 따라가는데 dt는 현재 시간으로 재면, 두 시간축이 같은 리듬이 아니기 때문에 dt 범위가 달라져서 변동이 심할 수 있다. 이럴 경우 stamp 기준으로 계산하는 것이 안정적이다.</li>
            </ul>
          </li>
          <li>메세지가 네트워크/큐 때문에 실제 간격과 다르게 한꺼번에 연속으로 콜백되어서 어느 순간에 거의 2개가 동시 처리되면 dt 감소
            <ul>
              <li>dt를 콜백 호출 간격 기반으로 계산하면 어느 순간 한 번에 빠르게 실행되는 경우 dt가 1회성으로 매우 커질 수 있다.</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>플러그인의 실험을 위해 가우시안 노이즈를 <code class="language-plaintext highlighter-rouge">&lt;gaussian_noise&gt;10&lt;/gaussian_noise&gt;</code> 10으로 설정해두어서</li>
    </ul>
  </li>
  <li>결론
    <ul>
      <li>코드를 직접 확인한 결과,</li>
      <li>cur 값은 control_robot.py 에서 출력하는데, 실제로는 control_robot.py 로 구독한 <code class="language-plaintext highlighter-rouge">/robot/odom</code> 의 Odometry.twist 를 그대로 읽어온 값이다. 이 노드 안에서는 dt로 나눠서 속도를 계산하는 코드가 없다. 따라서 dt 관련 후보는 문제가 아니다.</li>
      <li><code class="language-plaintext highlighter-rouge">/robot/odm</code> 은 모델 SDF에서 P3D 플러그인이 퍼블리시 하는데,</li>
      <li>플러그인 설정의 가우시간 노이즈가 테스트를 위해 10으로 높여 놓았는데, 되돌리지 않아서 표준 편차가 10으로 한 번씩 크게 튀는 값이 나오게 되었다.</li>
    </ul>
  </li>
  <li>테스트
    <ul>
      <li>
        <p>가우시안 노이즈 값을 <gaussian_noise>0.01</gaussian_noise>로 두고 다시 제어를 시작해봤다.</p>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [cmd] linear :  -1.5  angular :  0.0
  [cur] linear :  0.07788550724647324  angular :  -0.01895389213057958
  [cmd] linear :  -2.0  angular :  0.0
  [cur] linear :  0.1195186484726249  angular :  -0.0062722198927030355
  [cmd] linear :  -2.0  angular :  0.0
  [cur] linear :  0.1530365989391994  angular :  -0.00861613546351606
  [cmd] linear :  -2.5  angular :  0.0
  [cur] linear :  0.1709270954028069  angular :  -0.003100883572690438
  [cmd] linear :  -3.0  angular :  0.0
  [cur] linear :  0.20240741083392372  angular :  -0.009781222284346372
  [cmd] linear :  -3.0  angular :  0.0
  [cur] linear :  0.23607689382020272  angular :  -0.003795920321737263
  [cmd] linear :  -3.0  angular :  0.0
  [cur] linear :  0.23694622727228343  angular :  -0.014323942045737757
  [cmd] linear :  -3.4999999999999996  angular :  0.0
  [cur] linear :  0.2519330011736734  angular :  0.014088135430624676
  [cmd] linear :  -3.9999999999999996  angular :  0.0
  [cur] linear :  0.2424767872855163  angular :  -0.014139599319290787
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.31398173326248  angular :  0.010898859279758845
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3217806859406334  angular :  0.0033362062504576884
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3175554819382802  angular :  -0.005200905808602651
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3348905921220147  angular :  0.010606165652519546
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3207792354489992  angular :  -0.00294466189502181
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.31710844723395365  angular :  -6.023304307044801e-05
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.33231914549753977  angular :  -0.019434537821900286
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3261831338532742  angular :  0.004577647788463024
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.31274331797214155  angular :  -0.00504699603976933
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3017519289845812  angular :  0.011928417773003512
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.31792136174318825  angular :  -0.00200434917624646
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3153826457173677  angular :  -0.0027232532408877717
  [cmd] linear :  -4.0  angular :  0.0
  [cur] linear :  0.3140751681233642  angular :  -0.0161248175875797
</code></pre></div>        </div>
      </li>
      <li><code class="language-plaintext highlighter-rouge">angular</code> 값이 0.01~0.02 사이에서 움직이는 정상적인 범위가 되었다.</li>
      <li><code class="language-plaintext highlighter-rouge">cmd</code>는 <code class="language-plaintext highlighter-rouge">/cmd_vel</code>을 휠 속도 명령으로 바꾸기 위해 임의 게인을 곱한 ‘제어입력’이고, <code class="language-plaintext highlighter-rouge">cur</code>은 Gazebo가 계산한 ‘실제 차체 속도’ 라서 단위나 의미 자체가 다르기 때문에 값이 같게 보일 필요가 없다.
        <ul>
          <li>Gazebo에서 차량을 제어할 때 휠 반지름/차폭으로 변환식을 써야하지만, 차량 속도가 느려 체감 속도를 맞추려고 임의의 게인을 곱해 <code class="language-plaintext highlighter-rouge">cmd</code>를 계산했다.</li>
        </ul>
      </li>
      <li>현실에서는 관성이나 마찰 때문에 명령을 즉시 그대로 못 따라가기 때문에 매 순간 <code class="language-plaintext highlighter-rouge">cur = cmd</code>가 성립하지는 않는다.</li>
    </ul>
  </li>
</ul>

<h2 id="simulink-통신-연결">Simulink 통신 연결</h2>

<p>매트랩과 simulink에서 만든 제어기를 Gazebo에 적용하기 위해서 UDP 통신 환경을 구축했다.</p>

<p>Instrument Control Toolbox 애드온을 적용하여 UDP recieve/send 블록을 사용한다.</p>

<ul>
  <li>UDP(User Datagram Protocol) 통신이란
    <ul>
      <li>인터넷에서 데이터를 주고받는 전송 계층 프로토콜로 TCP와 다르게 비연결형 방식으로 상대방이 수신중이라는 응답을 하지 않아도 ros2의 토픽처럼 패킷을 그냥 던지는 방식이다.</li>
      <li>중간에 패킷이 손실되거나 순서가 바뀌거나 중복이 될 수도 있지만 보내는 사람은 알 수 없다.</li>
      <li>하지만 전송 속도가 매우 빠르기 때문에 조금 유실 되어도 크게 문제가 없다.</li>
      <li>하나의 IP 안에서도 여러 프로그램이 동시에 UDP를 쓸 수 있도록 포트 번호로 구분한다.</li>
    </ul>
  </li>
</ul>

<h3 id="simulink-통신-테스트">Simulink 통신 테스트</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-3/12.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/13.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/14.png" alt="image.png" /></p>

<ul>
  <li>Solver가 Auto 방식이면
    <ul>
      <li>보통 가변 스텝 솔버를 선택하기 때문에 신호의 변화가 적을 때는 step을 크게 건너뛰고, 변화가 급격할 때만 잘게 쪼갠다.</li>
      <li>계산이 단순할 때, 10초 분량의 데이터를 몇 번의 계산으로 끝내기 때문에 결과를 한 번에 뿌려준다.</li>
    </ul>
  </li>
  <li>Solver가 Fixed-step 방식이면
    <ul>
      <li>정해진 간격마다 무조건 멈추기 때문에 10초 시뮬레이션이라면 step이 0.01 일때 총 1000번의 계산을 강제로 수행한다.</li>
      <li>매 step마다 데이터를 주고받는 과정에서 대기 시간이 발생해서 시뮬레이션 속도가 물리적인 실제 시간과 비슷하게 느려지게 된다.</li>
      <li>완전한 실시간을 사용하고 싶으면 simulate - run 아래 화살표 - simulation pacing 기능을 켜면 실제 시간과 동기화 된다.</li>
    </ul>
  </li>
  <li>IP 주소 설정은
    <ul>
      <li>외부 pc가 아닌 내 컴퓨터로 데이터를 보내기 때문에 로컬을 의미하는 127.0.0.1을 사용했다.</li>
      <li>내 pc에 어떤 통로로 들어오든 포트 7777로 오는 것은 다 받게 하기 위해 로컬 주소를 0.0.0.0을 사용했다.</li>
    </ul>
  </li>
  <li>Byte-order는
    <ul>
      <li>숫자를 저장하거나 보낼 때 큰 단위부터 차례대로 보내는 방식이다.</li>
      <li>123을 1-2-3 순서로 보냈으면 받을 때도 1-2-3 순서로 받기 위해서 big-endian으로 맞춰주었다.</li>
      <li>이 설정이 서로 다르다면 데이터가 깨지거나 말도 안 되는 다른 값이 나오게 된다.</li>
    </ul>
  </li>
</ul>

<h2 id="ros2-통신-연결">Ros2 통신 연결</h2>

<p>이제 Ros2에서도 UDP 통신을 사용하여 Server와 Client 노드를 만들어서 서로 데이터를 잘 주고 받는지 환경 테스트를 해야한다.</p>

<p><img src="/assets/posts/2026-01-10-pegasus-3/15.png" alt="image.png" /></p>

<ul>
  <li>간단하게 udp_server 노드와 udp_client 노드를 만들어서 둘 사이에 통신이 잘 되는지 확인했다.
    <ul>
      <li>테스트로 0.11과 0.22를 송신하고, 서버로부터 그 절반인 0.06과 0.11이 계산되어 나오는 것을 볼 수 있다.</li>
      <li>클라이언트와 서버 모두 송수신이 정상적으로 작동한다.</li>
    </ul>
  </li>
</ul>

<p>Ros2 상호간에 통신이 잘 되는 것을 확인하였고, 우리의 현재 흐름은 아래와 같다.</p>

<p><strong>명령(입력) 경로</strong></p>

<ul>
  <li>MATLAB 제어 알고리즘 → UDP로 (linear, angular) 송신 → ROS2 udp_server가 수신 → ROS2 /cmd_vel(Twist)로 publish → control_robot가 <code class="language-plaintext highlighter-rouge">/cmd_vel</code> subscribe → 휠 명령 publish → Gazebo 모터 플러그인 → 로봇이 움직임</li>
</ul>

<p><strong>상태(출력) 경로</strong></p>

<ul>
  <li>Gazebo → gazebo_ros_p3d가 <code class="language-plaintext highlighter-rouge">/robot/odom publish</code> → ROS2에서 <code class="language-plaintext highlighter-rouge">/robot/odom subscribe</code> → UDP로 상태($v$, $w$, pose)를 MATLAB으로 송신 → MATLAB이 UDP receive</li>
</ul>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="Control" /><category term="Control" /><summary type="html"><![CDATA[Gazebo / Ros2 시뮬레이터]]></summary></entry><entry><title type="html">[Pegasus] PI 제어기 Zero 위치 선정의 트레이드오프</title><link href="https://jiharangrang.github.io/robotics/control/2025/12/30/pegasus-2.html" rel="alternate" type="text/html" title="[Pegasus] PI 제어기 Zero 위치 선정의 트레이드오프" /><published>2025-12-30T00:00:00+09:00</published><updated>2025-12-30T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/control/2025/12/30/pegasus-2</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/control/2025/12/30/pegasus-2.html"><![CDATA[<h2 id="시스템-요구-사항-정의">시스템 요구 사항 정의</h2>

<ul>
  <li>
    <p>내가 목표로 삼을 제어 스펙이다. 실제 플랜트의 움직임을 먼저 분석한 후, 세틀링 타임이 어느 정도 짧아지면서 반응성이 높아지면 좋을지, 정상 상태 오차가 얼마나 있으면 좋을지 등을 결정한다.</p>
  </li>
  <li>$T_s$ : 1s</li>
  <li>%$OS$ : 10%</li>
  <li>$e_{\infty}$ : 0</li>
</ul>

<p>아래는 계산을 위한 실제 파라미터들이다. 인터넷에 나와 있는 자료를 활용 했으며, 일부 값들은 이상적인 조건에서 역연산 되어 계산되었다.</p>

<table>
  <thead>
    <tr>
      <th><strong>파라미터</strong></th>
      <th><strong>값</strong></th>
      <th><strong>단위</strong></th>
      <th><strong>파라미터</strong></th>
      <th><strong>값</strong></th>
      <th><strong>단위</strong></th>
      <th><strong>파라미터</strong></th>
      <th><strong>값</strong></th>
      <th><strong>단위</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>$m_b$</td>
      <td><strong>102</strong></td>
      <td>$kg$</td>
      <td>$r$</td>
      <td>0.08</td>
      <td>$m$</td>
      <td>$b$</td>
      <td>0.01</td>
      <td>$N$$\cdot m \cdot s$</td>
    </tr>
    <tr>
      <td>$m_w$</td>
      <td>4</td>
      <td>$kg$</td>
      <td>$R$</td>
      <td>4.18</td>
      <td>$\Omega$</td>
      <td>$N$</td>
      <td>25</td>
      <td>.</td>
    </tr>
    <tr>
      <td>$I_b$</td>
      <td>7.8412</td>
      <td>$kg \cdot m^2$</td>
      <td>$L$</td>
      <td>0.012</td>
      <td>$H$</td>
      <td>$K_t$</td>
      <td>0.0322</td>
      <td>$N \cdot m/A$</td>
    </tr>
    <tr>
      <td>$I_{wb}$</td>
      <td>0.3664</td>
      <td>$kg \cdot m^2$</td>
      <td>$\omega_s$</td>
      <td>8.0634</td>
      <td>$rad/s$</td>
      <td>$K_e$</td>
      <td>0.0322</td>
      <td>$V/(rad/s)$</td>
    </tr>
    <tr>
      <td>$I_w$</td>
      <td>0.0128</td>
      <td>$kg \cdot m^2$</td>
      <td>$T_s$</td>
      <td>6.4489</td>
      <td>$N \cdot m$</td>
      <td>$V_{in}$</td>
      <td>48</td>
      <td>$V$</td>
    </tr>
    <tr>
      <td>$d$</td>
      <td><strong>0.375</strong></td>
      <td>$m$</td>
      <td>$\omega_m$</td>
      <td>201.585</td>
      <td>$rad/s$</td>
      <td>$I_s$</td>
      <td>8</td>
      <td>$A$</td>
    </tr>
    <tr>
      <td>$l$</td>
      <td><strong>0.3</strong></td>
      <td>$m$</td>
      <td>$T_m$</td>
      <td>0.258</td>
      <td>$N \cdot m$</td>
      <td>$P_s$</td>
      <td>52</td>
      <td>$W$</td>
    </tr>
  </tbody>
</table>

<h2 id="제어기-설계">제어기 설계</h2>

<p>전달함수와 시스템 요구 사항이 정해졌으니, 제어기를 설계할 수 있다.</p>

<p>병진 운동 전달함수부터 제어기 설계를 시작해보자.</p>

<div class="language-matlab highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">% 차체 요잉 회전시 파라미터</span>
<span class="n">mb</span> <span class="o">=</span> <span class="mi">102</span><span class="p">;</span>
<span class="n">mw</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="n">r</span> <span class="o">=</span> <span class="mf">0.08</span><span class="p">;</span>
<span class="n">d</span> <span class="o">=</span> <span class="mf">0.375</span><span class="p">;</span>
<span class="n">l</span> <span class="o">=</span> <span class="mf">0.3</span><span class="p">;</span>

<span class="n">Ib</span> <span class="o">=</span> <span class="mi">1</span><span class="p">/</span><span class="mi">12</span> <span class="o">*</span> <span class="n">mb</span> <span class="o">*</span> <span class="p">(</span><span class="mi">4</span><span class="o">*</span><span class="n">d</span><span class="o">^</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">4</span><span class="o">*</span><span class="n">l</span><span class="o">^</span><span class="mi">2</span><span class="p">);</span>
<span class="n">Iwb</span> <span class="o">=</span> <span class="n">mw</span><span class="o">*</span><span class="n">l</span><span class="o">^</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">1</span><span class="p">/</span><span class="mi">4</span><span class="o">*</span><span class="n">mw</span><span class="o">*</span><span class="n">r</span><span class="o">^</span><span class="mi">2</span><span class="p">;</span>
<span class="n">Iw</span> <span class="o">=</span> <span class="mi">1</span><span class="p">/</span><span class="mi">2</span> <span class="o">*</span> <span class="n">mw</span> <span class="o">*</span> <span class="n">r</span><span class="o">^</span><span class="mi">2</span><span class="p">;</span>
<span class="n">Ieq</span> <span class="o">=</span> <span class="n">Ib</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">Iwb</span><span class="p">;</span>

<span class="n">M</span> <span class="o">=</span> <span class="n">mb</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">mw</span><span class="p">;</span>
<span class="n">b</span> <span class="o">=</span> <span class="mf">0.01</span><span class="p">;</span>

<span class="c1">% DC 모터 파라미터</span>
<span class="n">N</span> <span class="o">=</span> <span class="mi">25</span><span class="p">;</span>
<span class="n">V_in</span> <span class="o">=</span> <span class="mi">48</span><span class="p">;</span>
<span class="n">P_s</span> <span class="o">=</span> <span class="mi">52</span><span class="p">;</span>
<span class="n">I_s</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span>
<span class="n">w_s</span> <span class="o">=</span> <span class="mi">77</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">*</span> <span class="nb">pi</span> <span class="p">/</span> <span class="mi">60</span><span class="p">;</span>
<span class="n">T_s</span> <span class="o">=</span> <span class="n">P_s</span> <span class="p">/</span> <span class="n">w_s</span><span class="p">;</span>

<span class="n">T_m</span> <span class="o">=</span> <span class="n">T_s</span> <span class="p">/</span> <span class="n">N</span><span class="p">;</span>
<span class="n">w_m</span> <span class="o">=</span> <span class="n">w_s</span> <span class="o">*</span> <span class="n">N</span><span class="p">;</span>

<span class="n">K_t</span> <span class="o">=</span> <span class="n">T_m</span> <span class="p">/</span> <span class="n">I_s</span><span class="p">;</span>
<span class="n">K_e</span> <span class="o">=</span> <span class="n">K_t</span><span class="p">;</span>
<span class="n">R</span> <span class="o">=</span> <span class="mf">4.18</span><span class="p">;</span>
<span class="n">L</span> <span class="o">=</span> <span class="mf">0.012</span><span class="p">;</span>

<span class="c1">% 병진 운동 전달함수</span>
<span class="n">num</span> <span class="o">=</span> <span class="p">[</span><span class="mi">2</span> <span class="o">*</span> <span class="n">V_in</span> <span class="o">*</span> <span class="n">K_t</span> <span class="o">*</span> <span class="n">N</span> <span class="o">*</span> <span class="n">r</span><span class="p">];</span>
<span class="n">den</span> <span class="o">=</span> <span class="p">[(</span><span class="n">M</span> <span class="o">*</span> <span class="n">r</span><span class="o">^</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">Iw</span><span class="p">)</span> <span class="o">*</span> <span class="n">L</span><span class="p">,</span> <span class="p">((</span><span class="n">M</span> <span class="o">*</span> <span class="n">r</span><span class="o">^</span><span class="mi">2</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">Iw</span><span class="p">)</span> <span class="o">*</span> <span class="n">R</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">b</span> <span class="o">*</span> <span class="n">L</span><span class="p">),</span> <span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">b</span> <span class="o">*</span> <span class="n">R</span> <span class="o">+</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">N</span><span class="o">^</span><span class="mi">2</span> <span class="o">*</span> <span class="n">K_e</span> <span class="o">*</span> <span class="n">K_t</span><span class="p">)];</span>

<span class="n">sys</span> <span class="o">=</span> <span class="n">tf</span><span class="p">(</span><span class="n">num</span><span class="p">,</span> <span class="n">den</span><span class="p">);</span>

<span class="nb">disp</span><span class="p">(</span><span class="s1">'================= 병진 운동 전달함수 ================'</span><span class="p">);</span>
<span class="n">sys</span>
<span class="nb">disp</span><span class="p">(</span><span class="s1">'==================================================='</span><span class="p">);</span>
</code></pre></div></div>

<ul>
  <li>
    <p>위와 같이 파라미터들을 정리하고 병진 운동 전달 함수를 구해보면,</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/1.png" alt="image.png" /></p>
  </li>
  <li>
    <p>이제 이 전달함수의 루트 로커스를 그려서 안정성과 근의 움직임을 판별해보자.</p>
  </li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-2/2.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-2/3.png" alt="image.png" /></p>

<ul>
  <li>루트로커스를 보면
    <ul>
      <li>pole이 -347.9, -0.45인 것을 알 수 있다.</li>
      <li>오른쪽 응답은 H=1 인 경우 피드백 시스템일 때의 스텝 응답이다. 루트로커스에도 알 수 있지만, 폐루프 응답을 구해보면 극점이 약 -350과 -0.5 정도 나온다.</li>
      <li>$e^{at}$ 형태에서 a에 -350은 바로 사라지는 것으로 볼 수 있기 때문에 시스템은 원점과 가까운 -0.5에의해 좌우된다.</li>
      <li>따라서 응답이 앞서 예산했듯이 1차 시스템의 응답 형태로 근사 되는 것을 볼 수 있다.</li>
    </ul>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/4.png" alt="image.png" /></p>
  </li>
  <li>폐루프 step input일때 정상상태 오차가 약 0.2 정도 있는 것을 확인 할 수 있다.</li>
  <li>1차 시스템은 구조적으로 오버슈트나 진동이 없기 때문에 이를 억제할 필요가 없다.</li>
  <li>
    <p>따라서 우리는 <strong>PI 제어기</strong>만 설계를 하면 될 것 같다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/5.png" alt="image.png" /></p>
  </li>
  <li>PI 제어기를 설계 할 때, $\frac{1}{s}$ 에의해 Angle Condition이 만족되지 않아 응답이 달라질 수 있는데
    <ul>
      <li>$\theta_{net} = \theta_{zero} - \theta_{pole} \approx 0$ 를 만족하기 위해서 원점 근처에 제로를 추가 해주면 응답을 크게 변화시키지 않으면서 정상상태 오차만 제거된 안정적인 시스템을 만들 수 있다.</li>
    </ul>
  </li>
  <li>우리의 목표는 %OS가 10%인 $\zeta$ = 0.5912일 때이다.
    <ul>
      <li>
        <p>위 그래프는 임의로 제로가 -1, -2.5, -5일때의 루트로커스이고, 제타가 0.5912일 때 게인 값들을 구하면</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/6.png" alt="image.png" /></p>
      </li>
      <li>
        <p>제타 0.5912 근처의 값을 임의로 찍고, 그때의 게인은 0.327, 1.35, 2.65이다.</p>
        <ol>
          <li>제로를 원점에 가깝게 두면, Ts 1초 이내 조건을 달성하기 힘들어진다.
            <ul>
              <li>제로가 원점에 가까우면 시스템 응답에 아주 느린 성분($e^{-0.1t}$ 같은)이 남게 되어 지배적이게 되어 느린 응답을 만든다.
                <ul>
                  <li>제로는 분자항이라서 역라플라스시 직접적으로 저런 지수함수 형태를 만들 수 없지만, 루트로커스에서 제로가 있다면, 게인이 점점 커지는 상황에서는 <strong>폴 중 누군가는 제로 근처에 찍힐 수 밖에 없기 때문에</strong> 폴이 제로 근처에 찍히는데 원점 근처라면, 예시의 지수함수처럼 느린 지배적 응답이 될 수도 있기 때문이다.</li>
                </ul>
              </li>
              <li>극점 바로 위에 제로를 두어 상쇄 시키는 방법도 있지만, 루트 로커스를 원하는 대로 휘게 할 수 없으므로 LHP의 더 안정적이고 빠른영역인 왼쪽으로 루트 로커스를 당겨올 수 없게 된다.</li>
              <li>따라서 극점 -0.5 보다 더 작은 영역에서 오버슈트와 응답 속도를 줄타기 할 수 있는 -1, -2.5, -5를 선정해본 것이다.</li>
            </ul>
          </li>
        </ol>
      </li>
    </ul>
  </li>
  <li>
    <p>이제 이 게인들로 응답 그래프를 그려보면</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/7.png" alt="image.png" /></p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/8.png" alt="image.png" /></p>

    <ul>
      <li>어림 잡기 위해서 루트로커스 그래프 툴팁의 파란 글자 오버슈트 값을 보면서 Gain을 선정했지만, 실제 터미널 결과가 달라서 왜 이런일이 일어났는지 알아 보았다.
        <ul>
          <li>툴팁의 값은 정해진 공식에 값을 대입하는 구조이기 때문에 2차 근사일뿐 실제는 stepinfo를 통해서 구해야 한다는 정보를 매트랩 공식 포럼에서 찾을 수 있었다.</li>
        </ul>
      </li>
      <li>%OS가 적당하면서 $T_s$ 가 1초를 만족해야 하므로 응답이 빠른 제로가 -2.5 일때를 기준으로 설계해보자.</li>
    </ul>
  </li>
  <li>이제 제로를 -2.5로 고정한 후, 전체이득 $K_{pi}$ 게인 값을 조절하면서 목표에 맞춰보자.v
    <ul>
      <li>
        <p>게인 값을 2부터 10까지 순차적으로 할당해서 응답을 그려봤다.</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/9.png" alt="image.png" /></p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/10.png" alt="image.png" /></p>
      </li>
      <li>
        <p>$K_{pi}$ 가 6일때 10% 오버슈트 이내, Ts 1초 이내를 달성했고, $e_{\infty}$ 도 원점에 pole이 추가되면서 0인 것을  확인할 수 있다.</p>
      </li>
    </ul>
  </li>
  <li>최종적으로 $K_{pi}$ = 6을 선정했고 제로는 -2.5를 선정하였다.</li>
  <li>
    <p>따라서 $G_c(s) = K_p + \frac{K_i}{s} = \frac{K_p(s+\frac{K_i}{K_P})}{s} = \frac{6(s+2.5)}{s}$ 이므로 $K_p = 6, \quad K_i = 15$ 를 선정한다.</p>
  </li>
  <li>같은 방법으로 회전 운동 전달함수도 제어기를 설계해보자.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-2/11.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-2/12.png" alt="image.png" /></p>

<ul>
  <li>루트로커스를 보면
    <ul>
      <li>pole이 -347.9, -0.45인 것을 알 수 있다.</li>
      <li>$e_{\infty}$ 도 약 0.07 정도 존재한다.</li>
      <li>
        <p>임의의 제로 -1, -2, -3 일때</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/13.png" alt="image.png" /></p>
      </li>
      <li>
        <p>$K_{pi}$ 게인이 0.09, 0.231, 0.479이므로 이 게인들로 실제 stepinfo를 얻어보면</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/14.png" alt="image.png" /></p>
      </li>
      <li>
        <p>제로가 -3 일때를 기준으로 $K_{pi}$ 게인을 선정해보자.</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/15.png" alt="image.png" /></p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/16.png" alt="image.png" /></p>

        <ul>
          <li>$K_{pi}$ 값이 2부터 목표에 도달한다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>최종적으로 $K_{pi}$ = 2 을 선정했고 제로는 -3를 선정하였다.</li>
  <li>따라서 $G_c(s) = K_p + \frac{K_i}{s} = \frac{K_p(s+\frac{K_i}{K_P})}{s} = \frac{2(s+3)}{s}$ 이므로 $K_p = 2, \quad K_i = 6$ 를 선정한다.</li>
</ul>

<details>
  <summary>코드 전문</summary>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```matlab
% 차체 요잉 회전시 파라미터
mb = 102;
mw = 4;
r = 0.08;
d = 0.375;
l = 0.3;

Ib = 1/12 * mb * (4*d^2 + 4*l^2);
Iwb = mw*l^2 + 1/4*mw*r^2;
Iw = 1/2 * mw * r^2;
Ieq = Ib + 2 * Iwb;

M = mb + 2 * mw;
b = 0.01;

% DC 모터 파라미터
N = 25;
V_in = 48;
P_s = 52;
I_s = 8;
w_s = 77 * 2 * pi / 60;
T_s = P_s / w_s;

T_m = T_s / N;
w_m = w_s * N;

K_t = T_m / I_s;
K_e = K_t;
R = 4.18;
L = 0.012;

% 병진 운동 전달함수
num = [2 * V_in * K_t * N * r];
den = [(M * r^2 + 2 * Iw) * L, ((M * r^2 + 2 * Iw) * R + 2 * b * L), (2 * b * R + 2 * N^2 * K_e * K_t)];

sys = tf(num, den);

disp('================= 병진 운동 전달함수 ================');
sys
disp('===================================================');

% 병진 운동 전달함수 루트 로커스와 폐루프 스텝 응답

sys2 = feedback(sys, 1);

poles = pole(sys);
disp(['극점 개수: ', num2str(length(poles))]);
disp('극점 좌표:');
disp(poles);

figure(1);
rlocus(sys);
title('병진 운동 전달함수 오픈루프 루트 로커스');

figure(2);
step(sys2,100);
title('병진 운동 전달함수 폐루프 스텝 응답');
grid on;

% PI 제어기 설계
% 목표 제타 구하는 식
POS = 0.1; % 10% 오버슈트
zeta = -log(POS)/sqrt(pi^2 + (log(POS))^2)

% 제어기 영점 후보
z_PI = [1 2.5 5];

figure(3);
for i = 1:length(z_PI)
    G_PI = tf([1 z_PI(i)], [1 0]);
    sys_PI = sys * G_PI;
    rlocus(sys_PI);
    sgrid(0.5912, 0.1:1:15);
    axis([-15 3 -8 8]);
    hold on;
end

% K_pi 값에 따른 응답 특성들
K_PI = [0.327 1.35 2.65];

for i = 1:length(z_PI)
    Gc_PI = tf([1 z_PI(i)], [1 0]);
    sys_PI = sys * Gc_PI;
    T_PI(i) = feedback(K_PI(i) * sys_PI, 1);
    data_PI = stepinfo(T_PI(i));
    OS_PI(i) = data_PI.Overshoot;
    Ts_PI(i) = data_PI.SettlingTime;
    Tr_PI(i) = data_PI.RiseTime;
    hold on;
end

figure(4);
for i = 1:length(z_PI)
    step(T_PI(i), 5);
    hold on;
end
hold off;
title('PI 제어기 폐루프 스텝 응답');
legend('제로 값 1', '제로 값 2.5', '제로 값 5');
grid on;

disp('Step information of Closed loop TF');
fprintf('%-10s %-10s %-10s %-10s\n', 'zero', 'pOS', 'Ts', 'Tr');
fprintf('---------------------------------------------------\n');
for i = 1:length(z_PI)
    fprintf('%-10.2f %-10.4f %-10.4f %-10.4f\n', ...
        z_PI(i), OS_PI(i), Ts_PI(i), Tr_PI(i));
end
disp('===================================================');

% 제로 값 선정 후 K_PI 값 구하기
z_PI = 2.5;
K_PI = [2 3 4 5 6 7 8 9 10];
for i = 1:length(K_PI)
    Gc_PI = tf([1 z_PI], [1 0]);
    sys_PI = sys * Gc_PI;
    T_PI(i) = feedback(K_PI(i) * sys_PI, 1);
    data_PI = stepinfo(T_PI(i));
    OS_PI(i) = data_PI.Overshoot;
    Ts_PI(i) = data_PI.SettlingTime;
    Tr_PI(i) = data_PI.RiseTime;
end

figure(5);
for i = 1:length(K_PI)
    step(T_PI(i), 5);
    hold on;
end
hold off;
title('PI 제어기 폐루프 스텝 응답');
legend('K_PI = 2', 'K_PI = 3', 'K_PI = 4', 'K_PI = 5', 'K_PI = 6', 'K_PI = 7', 'K_PI = 8', 'K_PI = 9', 'K_PI = 10');
grid on;

disp('Step information of Closed loop TF');
fprintf('%-10s %-10s %-10s %-10s\n', 'K_PI', 'pOS', 'Ts', 'Tr');
fprintf('---------------------------------------------------\n');
for i = 1:length(K_PI)
    fprintf('%-10.2f %-10.4f %-10.4f %-10.4f\n', ...
        K_PI(i), OS_PI(i), Ts_PI(i), Tr_PI(i));
end
disp('===================================================');

% 회전 운동 전달함수
num = [2 * V_in * r * l * K_t * N];
den = [(Ieq * r^2 + 2 * l^2 * Iw) * L, ((Ieq * r^2 + 2 * l^2 * Iw) * R + 2 * l^2 * b * L), (2 * l^2 * b * R + 2 * N^2 * l^2 * K_e * K_t)];

sys = tf(num, den);

disp('================= 회전 운동 전달함수 ================');
sys
disp('===================================================');

% 회전 운동 전달함수 루트 로커스와 폐루프 스텝 응답

sys2 = feedback(sys, 1);

poles = pole(sys);
disp(['극점 개수: ', num2str(length(poles))]);
disp('극점 좌표:');
disp(poles);

figure(6);
rlocus(sys);
title('회전 운동 전달함수 오픈루프 루트 로커스');

figure(7);
step(sys2, 15);
title('회전 운동 전달함수 폐루프 스텝 응답');
grid on;

% PI 제어기 설계
% 목표 제타 구하는 식
POS = 0.1; % 10% 오버슈트
zeta = -log(POS)/sqrt(pi^2 + (log(POS))^2)

% 제어기 영점 후보
z_PI = [1 2 3];

figure(8);
for i = 1:length(z_PI)
    G_PI = tf([1 z_PI(i)], [1 0]);
    sys_PI = sys * G_PI;
    rlocus(sys_PI);
    sgrid(0.5912, 0.1:1:15);
    axis([-8 3 -4 4]);
    hold on;
end

% K_PI 값에 따른 응답 특성들
K_PI = [0.09 0.231 0.479];

for i = 1:length(z_PI)
    Gc_PI = tf([1 z_PI(i)], [1 0]);
    sys_PI = sys * Gc_PI;
    T_PI(i) = feedback(K_PI(i) * sys_PI, 1);
    data_PI = stepinfo(T_PI(i));
    OS_PI(i) = data_PI.Overshoot;
    Ts_PI(i) = data_PI.SettlingTime;
    Tr_PI(i) = data_PI.RiseTime;
    hold on;
end

figure(9);
for i = 1:length(z_PI)
    step(T_PI(i), 5);
    hold on;
end
hold off;
title('PI 제어기 폐루프 스텝 응답');
legend('제로 값 1', '제로 값 2', '제로 값 3');
grid on;

disp('Step information of Closed loop TF');
fprintf('%-10s %-10s %-10s %-10s\n', 'zero', 'pOS', 'Ts', 'Tr');
fprintf('---------------------------------------------------\n');
for i = 1:length(z_PI)
    fprintf('%-10.2f %-10.4f %-10.4f %-10.4f\n', ...
        z_PI(i), OS_PI(i), Ts_PI(i), Tr_PI(i));
end
disp('===================================================');

% 제로 값 선정 후 K_PI 값 구하기
z_PI = 3;
K_PI = [1 2 3 4 5 6 7 8 9 10];
for i = 1:length(K_PI)
    Gc_PI = tf([1 z_PI], [1 0]);
    sys_PI = sys * Gc_PI;
    T_PI(i) = feedback(K_PI(i) * sys_PI, 1);
    data_PI = stepinfo(T_PI(i));
    OS_PI(i) = data_PI.Overshoot;
    Ts_PI(i) = data_PI.SettlingTime;
    Tr_PI(i) = data_PI.RiseTime;
end

figure(10);
for i = 1:length(K_PI)
    step(T_PI(i), 5);
    hold on;
end
hold off;
title('PI 제어기 폐루프 스텝 응답');
legend('K_PI = 1', 'K_PI = 2', 'K_PI = 3', 'K_PI = 4', 'K_PI = 5', 'K_PI = 6', 'K_PI = 7', 'K_PI = 8', 'K_PI = 9', 'K_PI = 10');
grid on;

disp('Step information of Closed loop TF');
fprintf('%-10s %-10s %-10s %-10s\n', 'K_PI', 'pOS', 'Ts', 'Tr');
fprintf('---------------------------------------------------\n');
for i = 1:length(K_PI)
    fprintf('%-10.2f %-10.4f %-10.4f %-10.4f\n', ...
        K_PI(i), OS_PI(i), Ts_PI(i), Tr_PI(i));
end
disp('===================================================');

```
</code></pre></div>  </div>

</details>

<h2 id="simulink-모델-만들기">Simulink 모델 만들기</h2>

<p><img src="/assets/posts/2026-01-10-pegasus-2/17.png" alt="image.png" /></p>

<ul>
  <li>식(6) <strong>병진 운동</strong> 전달함수를 한 번에 피드백 구조로 만든 다이어그램(상단)과 직접 항 하나하나 다이어그램(하단)으로 모델을 만들었다.
    <ul>
      <li>하단의 첫 피드백은 수식에 있는 뺄셈 연산을 위한 것이 아니라 피드백을 위해 출력 값을 받아와서 입력값과 빼는 것이고</li>
      <li>del_t 부분의 빼기 연산은 오직 수식의 뺄셈 연산을 구현하기 위한 것이다.</li>
    </ul>
  </li>
  <li>
    <p>가장 오른쪽 끝의 scope를 통해 두 모델의 응답을 확인할 수 있다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/18.png" alt="image.png" /></p>

    <ul>
      <li>구분을 위해 하단 다이어그램의 값을 0.1초 지연 후 그려지게 했다.</li>
      <li>두 응답의 결과가 같다는 것으로 부터 모델링이 잘 되었다는 것을 알 수 있다.</li>
    </ul>
  </li>
  <li>subsystem 기능을 이용하여 plant와 pid 제어기를 각각 분리해서 모델링하였다.
    <ul>
      <li>시스템 전달함수 구조
        <ul>
          <li>이 모델에서 ($\delta_t, \delta_r)$는 모터 입력 전압을 $V_{in}$에 대해 정규화한 조작량(무차원)이며, $V_R = V_{in}(\delta_t+\delta_r)$, $V_L = V_{in}(\delta_t-\delta_r)$로 좌/우 바퀴 전압 명령을 구성한다. 따라서 <strong>$K_p$는 속도 오차(m/s 또는 rad/s)를 정규화 입력(무차원)으로 변환하는 단위</strong>를 갖는다.</li>
        </ul>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/19.png" alt="image.png" /></p>
      </li>
      <li>
        <p>pid 제어기 구조</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/20.png" alt="image.png" /></p>
      </li>
      <li>전체 다이어그램
        <ul>
          <li>레퍼런스 입력 : $v, w$</li>
          <li>플랜트 제어 입력 : $\delta_t, \delta_r$</li>
        </ul>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/21.png" alt="image.png" /></p>

        <ul>
          <li>
            <p>최종 병진 운동 응답을 보니 매트랩으로 그린 제어기 설계 후 응답과 동일하게 나왔고, 알맞게 모델링 됐음을 알 수 있다.</p>

            <p><img src="/assets/posts/2026-01-10-pegasus-2/22.png" alt="image.png" /></p>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="파라미터-변화에-따른-응답-변화">파라미터 변화에 따른 응답 변화</h3>

<ul>
  <li>
    <p>$K_P$ 값을 1, 2, 3으로 설정 후 $K_I$ 는 동일하게 6인 경우</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/23.png" alt="image.png" /></p>

    <ul>
      <li>P 게인이 커질수록 반응성도 빠르고 오버슈트도 줄어든다.</li>
    </ul>
  </li>
  <li>
    <p>$K_P$ 값을 2로 설정  $K_I$ 는 동일하게 6, $K_D$ 가 0, 1, 2인 경우</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-2/25.png" alt="image.png" /></p>

    <ul>
      <li>D 제어기를 사용함과 동시에 불안정해진다.</li>
      <li>
        <p>분자에 추가된 s가 step input이 들어오는 시점에 기울기를 거의 $\infty$ 로 보기 때문에 아주 짧은 시간 동안 전압 명령이 매우 커져서 오히려 불안정해지는 것이다.</p>

        <p><img src="/assets/posts/2026-01-10-pegasus-2/26.png" alt="image.png" /></p>
      </li>
    </ul>
  </li>
  <li>
    <p>$K_P$ 값을 2로 설정  $K_I$ 는 6, 7, 8, $K_D$ 가 0 인 경우</p>
    <ul>
      <li>오버슈트가 증가하지만 $T_s$ 는 조금씩 감소한다.</li>
    </ul>
  </li>
</ul>

<h3 id="차동-구동-로봇의-병진과-회전의-decoupling-분석">차동 구동 로봇의 병진과 회전의 Decoupling 분석</h3>

<p>Simulink 모델링 중, 회전 입력 $\delta_r$을 변화시켜 각속도가 발생하고 있음에도 불구하고, 병진 속도의 그래프는 전혀 영향을 받지 않고 일정하게 유지되는 것을 발견했다. 분명 내가 설계한 플랜트 다이어그램 내부적으로는 왼쪽/오른쪽 모터가 $v$ 와 $\omega$ 의 영향을 동시에 받고 있는데 최종 출력단에서는 $\delta_r$ 을 바꿔도 출력 속도 응답 그래프에 영향이 없이 서로 독립적인 것처럼 보이는 이유를 분석해봤다.</p>

<ul>
  <li>
    <p><strong>수식적 원인 분석</strong></p>

    <p><strong>1. 입력 전압단에서의 상쇄</strong></p>

    <ul>
      <li>로봇을 앞으로 밀어주는 직진 힘은 양쪽 모터 전압의 합에 비례한다.</li>
      <li>
        <p>이때 회전 제어 입력 $\delta_r$ 은 우측엔 더해지고 좌측엔 빼지므로, 합을 구하는 순간 소거된다.</p>

        <p>$V_{sum} = V_R + V_L \propto (\delta_t + \mathbf{\delta_r}) + (\delta_t - \mathbf{\delta_r}) = 2\delta_t$</p>
      </li>
      <li>즉, 직진 운동 방정식의 입력항에는 오직 직진 명령 $\delta_t$ 만 살아남게 된다.</li>
    </ul>

    <p><strong>2. 피드백 루프에서의 상쇄</strong></p>

    <ul>
      <li>역기전력(Back-EMF) 피드백 역시 직진 운동에 방해가 되는 요소이므로, <strong>좌우 역기전력의 합</strong>이 전체 시스템의 감속 요인이 된다.</li>
      <li>
        <p>각 바퀴의 속도는 $v$ 와 $\omega$ 의 선형 결합으로 표현되는데, 이를 합치면 회전 성분이 상쇄된다.</p>

        <p>$\begin{aligned} V_{emf, total} &amp;\propto \Omega_R + \Omega_L \ &amp;\propto (v + l\omega) + (v - l\omega) \ &amp;= 2v \end{aligned}$</p>
      </li>
      <li>결과적으로 직진 운동을 방해하는 역기전력 항에서도 $\omega$ 성분은 사라지고 오직 $v$성분만 남는다.</li>
    </ul>
  </li>
  <li><strong>공학적으로</strong>
    <ul>
      <li><strong>시스템의 직교성 (Orthogonality)</strong>
        <ul>
          <li>차동 구동 시스템이 완벽하게 대칭이라고 가정할 때, 병진 운동과 회전 운동은 수학적으로 직교한다.</li>
          <li>이는 x축으로 이동한다고 해서 y축 좌표가 변하지 않는 것과 같은 원리이다.</li>
        </ul>
      </li>
      <li><strong>내부와 외부의 관점 차이</strong>
        <ul>
          <li><strong>Local 관점 (각 모터):</strong> 개별 모터는 $\delta_r$ 과 $\omega$ 의 영향을 받아 전압과 전류가 요동친다. (오른쪽은 힘들어지고, 왼쪽은 편해짐)</li>
          <li><strong>Global 관점 (로봇 몸체):</strong> 로봇 몸체의 직진 가속도는 <strong>두 모터 힘의 총합</strong>으로 결정된다. 내부적으로 에너지가 한쪽으로 쏠릴 뿐, 전체 에너지의 총합(직진 성분)은 변하지 않기 때문에 외부에서 볼 때는 영향이 없는 것처럼 보인다.</li>
        </ul>
      </li>
      <li><strong>줄다리기로 비유하자면</strong>
        <ul>
          <li>수레를 끌 때 오른쪽 사람은 더 세게 당기고, 왼쪽 사람은 살살 당겨도, 두 힘의 합이 일정하다면 수레가 회전할 뿐 앞으로 나가는 속도는 변하지 않는 것과 같다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong>결론</strong>
    <ul>
      <li>선형 시스템 모델에서는 중첩의 원리에 의해 직진과 회전이 완벽하게 분리(Decoupling)된다.</li>
      <li>실제 하드웨어에서는 모터 성능 차이, 배터리 전압 강하 등의 비선형성으로 인해 약간의 간섭이 발생할 수 있다고 한다.</li>
    </ul>
  </li>
</ul>]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="Control" /><category term="Control" /><summary type="html"><![CDATA[시스템 요구 사항 정의]]></summary></entry><entry><title type="html">[Pegasus] PID 제어 프로젝트 시작</title><link href="https://jiharangrang.github.io/robotics/control/2025/12/21/pegasus-1.html" rel="alternate" type="text/html" title="[Pegasus] PID 제어 프로젝트 시작" /><published>2025-12-21T00:00:00+09:00</published><updated>2025-12-21T00:00:00+09:00</updated><id>https://jiharangrang.github.io/robotics/control/2025/12/21/pegasus-1</id><content type="html" xml:base="https://jiharangrang.github.io/robotics/control/2025/12/21/pegasus-1.html"><![CDATA[<h1 id="프로젝트를-시작하며">프로젝트를 시작하며</h1>

<p>대학교 수업 시간에 배운 <strong>‘자동 제어’</strong> 과목은 앞으로 내가 가고 싶은 진로(로봇 제어, 자율주행, 동역학 관련 연구)와 직접적으로 연결되는 분야다. 수업에서는 루트 로커스, Bode 선도, PID 설계 같은 내용을 이론 중심으로 배웠지만, “이 수식들이 실제 로봇에서 어떻게 동작하는지”를 몸으로 느끼기에는 한계가 있었다.</p>

<p>특히 PID 제어기를 설계하는 계산 문제는 충분히 많이 풀었고 수업에서는 1등을 했지만,</p>

<ul>
  <li>내가 설정한 게인이 실제 로봇의 <strong>거동(응답 속도, 오버슈트, 진동)</strong>에 어떤 영향을 주는지,</li>
  <li>
    <p>이론에서 배운 성능 지표들이 <strong>Gazebo 같은 시뮬레이션 환경에서 실제 로봇 움직임과 어떻게 연결되는지</strong></p>

    <p>직접 확인해 볼 기회가 부족하다고 생각했다.</p>
  </li>
</ul>

<p>이 부족한 부분을 채우기 위해, 이번 프로젝트에서는 <strong>하나의 연구 과제를 수행한다는 마음가짐</strong>으로 다음을 목표로 삼았다.</p>

<ul>
  <li>실제 로봇을 대상으로 <strong>수학적 모델링 → PID 설계 → 시뮬레이션 → 실기 적용</strong>까지 한 사이클을 완주한다.</li>
  <li>이 과정에서 필요한 자동제어 이론은 <strong>Top-Down 방식</strong>으로,
    <ul>
      <li>먼저 “어떤 성능을 내야 하는지(응답 시간, 오버슈트 등)”를 정하고</li>
      <li>그 성능을 달성하기 위해 필요한 이론과 도구를 <strong>그때그때 학습</strong>하는 방식으로 정리한다.</li>
    </ul>
  </li>
</ul>

<p>프로젝트 전 과정은 Notion에 정리하고, 이후에는 GitHub 블로그에 포트폴리오 형식으로 게시하여,</p>

<p><strong>이론 + 시뮬레이션 + 실기 적용까지 모두 경험한 자동제어 프로젝트</strong>로 남길 예정이다.</p>

<h2 id="로봇-선정">로봇 선정</h2>

<p><img src="/assets/posts/2026-01-10-pegasus-1/1.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-1/2.jpg" alt="OIP.jpg" /></p>

<p>로봇은 아마존의 페가수스 모델로 선정하였다.</p>

<ul>
  <li>인터넷 제어 강의에서 제공되는 로봇 상세 스펙을 쉽게 참고할 수 있어서 실제 치수 기반의 모델링이 가능했다.</li>
  <li>학부 수준에서 분석하기에 형태가 단순해서 질량, 관성, 구동부 등을 이상화 하기에 적합하다고 생각했다.</li>
</ul>

<p>이 로봇을 기반으로</p>

<ul>
  <li>MATLAB/Simulink를 활용한 모델링 및 제어기 설계,</li>
  <li>Gazebo를 이용한 시뮬레이션 검증까지 연결하는 것을 이 프로젝트의 큰 흐름으로 삼을 예정이다.</li>
</ul>

<h2 id="로봇-스펙">로봇 스펙</h2>

<p><img src="/assets/posts/2026-01-10-pegasus-1/3.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-1/4.png" alt="image.png" /></p>

<ul>
  <li>가로 60cm, 세로 75cm, 높이 19cm</li>
  <li>무게 102kg 가정</li>
  <li>바퀴 2개, 1개당 무게 4kg 가정
    <ul>
      <li>무게는 자료가 나와있지 않아서 적당한 값을 가정했다.</li>
    </ul>
  </li>
  <li>바퀴 지름 16cm</li>
  <li>적재 능력 560kg</li>
  <li>최대 속도 1.3 m/s</li>
</ul>

<h3 id="모터-사양">모터 사양</h3>

<ul>
  <li>정격출력 52W</li>
  <li>전압 30~48V</li>
  <li>정격 전류 8A</li>
  <li>정격 속도 77rpm</li>
  <li>최대 속도 239rpm</li>
</ul>

<h3 id="주요-변수와-fbd">주요 변수와 FBD</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-1/5.png" alt="image.png" /></p>

<p><img src="/assets/posts/2026-01-10-pegasus-1/6.png" alt="image.png" /></p>

<ul>
  <li><strong>$N$</strong>: 수직항력</li>
  <li><strong>$m_b$</strong>: 차체 질량</li>
  <li>$m_w$: 바퀴 질량</li>
  <li>$g$: 중력가속도</li>
  <li><strong>$x, y$</strong>: 좌표계</li>
  <li><strong>$C.G$</strong>: 무게 중심</li>
  <li><strong>$\dot{x},v_b$</strong>: $x$방향 속도</li>
  <li><strong>$\ddot{x},\dot{v_b}$</strong>: $x$방향 가속도</li>
  <li><strong>$\omega_b$</strong>: 차체 각속도</li>
  <li><strong>$\dot{\omega_b}$</strong>: 차체 각가속도</li>
  <li><strong>$F_L$</strong>: 왼쪽 바퀴에 작용하는 힘</li>
  <li><strong>$F_R$</strong>: 오른쪽 바퀴에 작용하는 힘</li>
  <li><strong>$T_L$</strong>: 왼쪽 바퀴에 작용하는 토크</li>
  <li><strong>$T_R$</strong>: 오른쪽 바퀴에 작용하는 토크</li>
  <li><strong>$w_L$</strong>: 왼쪽 바퀴의 회전 속도</li>
  <li><strong>$w_R$</strong>: 오른쪽 바퀴의 회전 속도</li>
  <li>바퀴 1개의 경우를 통해 $N=mg$와 $F_{R,L} = \mu mg$를 얻었다.</li>
</ul>

<h3 id="가정">가정</h3>

<ol>
  <li>모터 손실은 무시한다.</li>
  <li>바퀴는 지면과 W/O Slip 조건을 만족한다.</li>
</ol>

<ul>
  <li>두 번째 가정으로부터 바퀴 중심의 병진 운동 속도 $V$는 반지름 $r$  * 각속도 $w$를 만족하게 된다.</li>
</ul>

<h2 id="운동방정식">운동방정식</h2>

<h3 id="바퀴의-회전-운동-방정식">바퀴의 회전 운동 방정식</h3>

\[\sum M_R : \quad I_w \dot{\omega}_R + b \omega_R + F_R r = T_R\]

\[\sum M_L : \quad I_w \dot{\omega}_L + b \omega_L + F_L r = T_L\]

\[\therefore \quad F_R = \frac{1}{r} (T_R - I_w \dot{\omega}_R - b \omega_R), \quad    F_L = \frac{1}{r} (T_L - I_w \dot{\omega}_L - b \omega_L) \quad (I_w : \frac{1}{2}m_wr^2) \tag1\]

<ul>
  <li>모터 토크, 지면 마찰력, 점성 마찰을 모아 회전 운동 방정식을 세웠다.</li>
  <li>회전 운동방정식으로부터 바퀴에 작용하는 힘을 구할 수 있다.</li>
</ul>

<p><img src="/assets/posts/2026-01-10-pegasus-1/7.png" alt="image.png" /></p>

\[v_b = \frac{v_L + v_R}{2} = \frac{r}{2}(w_L+w_R) \tag2\]

\[w_b = \frac{v_R-v_L}{2l} \tag3\]

<ul>
  <li>하나의 축에 2개의 바퀴가 나란히 있고 차체가 축 중앙에 있는 차동 구동 방식이므로 위의 두 식이 성립한다.</li>
</ul>

<h3 id="차체-병진-운동-방정식-mm_w2m_b">차체 병진 운동 방정식 ($M=m_w+2m_b$)</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-1/8.png" alt="image.png" /></p>

<p>$I_{w}$ : 바퀴 관성 모멘트</p>

\[\sum F_x: \quad F_L + F_R = M \dot{v}_b\]

<ul>
  <li>(1), (2)의 값을 대입한 후 정리하면</li>
</ul>

\[(M + \frac{2 I_w}{r^2}) \dot{v}_b + \frac{2b}{r^2} v_b = \frac{1}{r} (T_R + T_L)\]

<ul>
  <li>$(M + \frac{2 I_w}{r^2})$ 부분을 보면 원래 차체의 질량과 <strong>회전 관성이 더해져서 실제 질량보다 더 무거워진다</strong>는 의미이다.</li>
</ul>

<h3 id="차체-회전-운동-방정식">차체 회전 운동 방정식</h3>

<p><img src="/assets/posts/2026-01-10-pegasus-1/9.png" alt="image.png" /></p>

\[\sum M_b : \quad I_{eq} \dot{\omega}_b = F_R l - F_L l\]

<ul>
  <li>(1), (3)의 값을 대입한 후 정리하면</li>
</ul>

\[(I_{eq} + \frac{2l^2 I_w}{r^2}) \dot{\omega}_b + \frac{2l^2 b}{r^2} \omega_b = \frac{l}{r} (T_R - T_L)\]

<ul>
  <li>
    <p>병진 운동과 마찬가지로 로봇을 제자리에서 돌리려고 할 때, 몸체와 바퀴를 모두 돌려야 하기 때문에 각가속도항 앞의 <strong>관성 모멘트가 증가하는 것</strong>을 볼 수 있다.</p>

    <p><img src="/assets/posts/2026-01-10-pegasus-1/10.png" alt="image.png" /></p>

    <p>$I_{eq}$ : 등가 관성모멘트</p>

    <p>$I_b$  : 차체 관성 모멘트</p>

    <p>$I_{wb}$ : 바퀴의 두 축의 관성 모멘트 합 (by 평행축 정리)</p>

\[I_{eq} = 2I_{wb} + I_b\]

\[I_{eq} = \frac{1}{3} m_b (d^2 + \ell^2) + 2 m_w \ell^2 + \frac{1}{2} m_w r^2\]

    <ul>
      <li>$I_{eq}$ 값은 바퀴가 스스로의 축으로 회전하면서 차체 중심을 기준으로 회전하는 관성과</li>
      <li>차체 몸체를 직육면체로 가정했을 때 몸체의 관성의 합이다.</li>
    </ul>
  </li>
  <li>
    <p>이로써 로봇의 모든 운동방정식을 구했다.</p>
  </li>
</ul>

<h3 id="잠시-모터를-공부하면">잠시 모터를 공부하면</h3>

<p>기계공학과에서는 보통 모터에 나오는 출력 자체를 입력으로 보고 방정식을 차체와 바퀴에 대해서만 구하는 경우가 대부분이었다. 하지만 실제 제어는 전기 입력부터 시작되는 것이기 때문에 기초적인 <strong>모터의 회로 방정식</strong> 정도는 알 필요가 있을 것 같다.</p>

\[V(t) = R i(t) + L \frac{di(t)}{dt} + V_{emf}(t)\]

<ul>
  <li>키르히호프의 전압 법칙이다.</li>
  <li>이 내용을 조금 더 직관적으로 쓰면 아래와 같다.</li>
</ul>

\[V(t) \quad = \quad \underbrace{R i(t)}_{\text{손실}} \quad + \quad \underbrace{L \frac{di}{dt}}_{\text{전기적 관성}} \quad + \quad \underbrace{K_e \omega}_{\text{속도에 의한 반발}}\]

<ul>
  <li>배터리가 전압을 줘서 모터를 돌리려고 한다. 그 결과 전압은 3군데로 나누어 소비된다.</li>
  <li>${R i(t)}$ : 마찰 손실
    <ul>
      <li>전선을 통과하면서 열로 날아가는 에너지이다. 물이 흐르면 무조건 생기는 손실 같은 것이다.</li>
    </ul>
  </li>
  <li>$L \frac{di}{dt}$ : 전기적 관성
    <ul>
      <li>전류가 갑자기 변하는 것을 방해하는 성질. 유체역학의 수격현상처럼 전기 흐름을 갑자기 바꾸려면 힘이 든다.</li>
    </ul>
  </li>
  <li>$K_e \omega$ : 역기전력 (back EMF)
    <ul>
      <li>내가 전기를 써서 모터를 돌리지만, 모터가 돌면서 동시에 발전기 역할도 한다.
  따라서 모터 내부에서 입력 전압을 밀어내는 반대 방향 전압이 생긴다. 그래서 모터는 무한히 빨라진 수 없고, 최고 속도 제한이 생긴다.</li>
    </ul>
  </li>
</ul>

<p><strong>과정</strong></p>

<ol>
  <li>전압을 주면 전류가 흐르려 하고</li>
  <li>전류가 흐르면 토크가 생겨서 모터가 돈다.</li>
  <li>모터가 돌기 시작하면 역기전력이 생겨서 전류 들어오는 걸 방해한다.</li>
  <li>결국 <strong>입력 전압 = 저항 손실 + 역기전력</strong>이 되는 지점에서 모터 속도가 일정해진다.</li>
</ol>

<p><img src="/assets/posts/2026-01-10-pegasus-1/11.png" alt="image.png" /></p>

<ul>
  <li>
    <p>우리의 로봇 시스템이 위와 같은 구조로 모터와 바퀴가 이어져 있을 때</p>

\[V_{\text{emf}}(t) = K_e\omega_m(t)\]

\[\tau_m(t) = K_t i(t) \tag4\]

    <p>위와 같은 전기적 등식이 성립하고,</p>

\[\tau(t) = N\tau_m(t) \tag5\]

\[\omega(t) = \frac{\omega_m(t)}{N}\]
  </li>
  <li>토크는 기어비 때문에 늘어나고, 회전수는 기어비 때문에 줄어든다.</li>
  <li>이제 우리는 제어 이론을 적용하여 로봇을 제어할 것이기 때문에 시스템의 전달함수를 구해야한다.</li>
</ul>

<h2 id="전달함수">전달함수</h2>

<ul>
  <li>선형 시불변(Linear Time-Invariant, LTI) 시스템에서 초기 조건이 0일때, 입력 신호와 출력 신호의 비율을 의미
    <ul>
      <li>선형 시불변이란 간단히 말해서, 선형이고 시간에 영향이 없어서</li>
      <li>아침에 가속 페달 1초 조작에 10km/h 되던 차는 저녁에도 1초 조작에 10km/h 되어야 한다는 것을 의미한다.</li>
    </ul>
  </li>
  <li>
    <p>전달함수가 아래와 같은 형태일 때</p>

\[G(s) = \frac{N(s)}{D(s)} = K \frac{(s - z_1)(s - z_2) \cdots (s - z_m)}{(s - p_1)(s - p_2) \cdots (s - p_n)}\]

    <ul>
      <li>$z_i$ : 시스템의 영점(zeros)</li>
      <li>$p_i$ : 시스템의 극점(poles)</li>
      <li>분모 $D(s)=0$ 을 시스템의 특성 방정식으로 정의</li>
    </ul>
  </li>
</ul>

<h3 id="전기-모터-전달함수">전기 모터 전달함수</h3>

<ul>
  <li>
    <p>앞서 구한 전기 모터의 회로 방정식을 라플라스 변환하여 전달함수를 구해보자.</p>

\[V(t) = Ri(t) + L\frac{di(t)}{dt} + K_eN\omega(t)\]

    <p>라플라스 변환하면,</p>

\[V(s) = RI(s) + LsI(s) + K_eN\Omega(s)\]

    <p>$I(s)$에 대해 정리하고, (4)를 (5)에 대입해 라플라스 변환한 $T(s)$에 대입하면,</p>

\[I(s) = \frac{V(s) - K_eN\Omega(s)}{Ls + R}\]

\[T(s) = K_tNI(s)\]

\[T(s) = \frac{K_tNV(s) - K_eK_tN^2\Omega(s)}{Ls + R}\]
  </li>
  <li>위와 같은 형태로 전기 모터의 전달함수를 구할 수 있다.
    <ul>
      <li>입력 : 전압 $V(s)$</li>
      <li>출력 : 토크 $T(s)$</li>
    </ul>
  </li>
  <li>내가 흔히 보던 전달함수의 형태는 아니다. 분자의 $\Omega(s)$ 는 모터가 회전하는 속도이다.</li>
  <li>위에서 모터 속도가 빨라지면 역기전력이 생기는 것을 알게 되었는데, 
분자의 $-\Omega(s)$ 항은 속도가 빨라질수록 토크를 갉아먹는 반대 힘이 생긴다는 것을 의미한다.
    <ul>
      <li>마치 전기적 피드백처럼 작용하여 모터가 돌면서 속도가 생기다가</li>
      <li>속도가 생기면 다시 역기전력을 만들어서 전류를 줄인다.</li>
    </ul>
  </li>
  <li>현재의 $T(s)$는 엄밀한 의미로는 출력/입력 형태가 아니기 때문에 최종 전달함수가 아닌 상태이다.</li>
</ul>

<h3 id="병진-운동-전달함수">병진 운동 전달함수</h3>

<ul>
  <li>
    <p>앞서 구한 차체 병진 운동 방정식을 라플라스 변환하면</p>

\[\left(M + \frac{2I_w}{r^2}\right)sV_b(s) + \frac{2b}{r^2}V_b(s) = \frac{1}{r}(T_R(s) + T_L(s))\]

\[T_R(s) = \frac{K_t N V_R(s) - K_e K_t N^2 \Omega_R(s)}{Ls + R} \qquad T_L(s) = \frac{K_t N V_L(s) - K_e K_t N^2 \Omega_L(s)}{Ls + R}\]

    <p>위와 같은 식이 되고, 전기 모터의 전달함수 $T_R$, $T_L$ 을 운동 방정식에 대입하고, (2) 식을 사용하여 $\Omega_{R,L}(s)$를 $V_b$ 에대한 식으로 적어주면</p>

\[\left((Mr^2 + 2I_w)Ls^2 + ((Mr^2 + 2I_w)R + 2bL)s + 2bR + 2K_e K_t N^2\right)V_b(s) = K_t N r (V_R(s) + V_L(s)) \tag6\]
  </li>
  <li>식의 상수항을 보면
    <ul>
      <li>$2bR$로 나타난 기계적 마찰과 전기 저항의 곱</li>
      <li>물리적인 마찰이 없더라도 모터 내부의 자기장적인 상호작용 때문에 일어나는 저항력  $2K_eK_tN^2$이 있다.</li>
      <li><strong>공학적으로</strong> 마찰 $b$를 줄이려고 애쓰지만, 실제 제어에서는 $N$이 크다면 모터의 역기전력에 의한 전기적 제동력이 훨씬 클 수 있다.</li>
      <li>따라서 시스템이 예상보다 훨씬 무겁고 뻑뻑할 수 있다는 걸 예측할 수 있다.</li>
    </ul>
  </li>
  <li>식의 2차항을 보면
    <ul>
      <li>$(Mr^2 + 2I_w)L$ 로 나타난 기계적 관성 M과 전기적 관성 L의 곱</li>
      <li>보통 소형 DC 모터에서 $L$은 매우 작은 값을 가지므로 0으로 근사하게 되면, $s^2$항을 고려하지 않은 1차 시스템으로 근사화 할 수 있다.</li>
      <li>1차 시스템으로 근사화하게 되면, 설계가 단순해지고 D 게인을 잡을 때 노이즈에 덜 민감해질 수 있어서 좋다.</li>
    </ul>
  </li>
  <li>기어비 $N$을 보면
    <ul>
      <li>우변 입력 힘은 기어비 $N$에 비례해서 힘이 커지지만</li>
      <li>좌변 저항력  $2K_eK_tN^2$ 도 $N$에 비례해서 커진다.</li>
      <li><strong>공학적으로</strong> 힘을 세게 하려고 기어비를 무작정 높이면 토크는 올라가지만, 관성이나 감쇠가 제곱비로 증가해서 토크는 강한데 반응 속도가 엄청 느려질 수 있다.</li>
    </ul>
  </li>
  <li>
    <p>우리는 물리적 전압 $V$ 를 <strong>소프트웨어적 제어 입력 $\delta$ 로 변환해서</strong> 식을 세우고 싶다.</p>

    <p>왜냐하면 MCU로 모터를 제어할 때, 아날로그 전압을 마음대로 조절해서 내보내는 게 아니라
  PWM을 사용하기 때문에 모터에 걸리는 실제 평균 전압 $V_{motor}$ 는 ($V_{in} =$ 배터리 전압)</p>

\[V_{motor} \approx V_{in} \times \delta\]

    <p>제어 변수의 분리를 위해서 로봇의 움직임을 직진 $\delta_t$ 와 회전 $\delta_r$ 로 나누면</p>

\[V_R = V_{in}(\delta_t + \delta_r) \qquad V_L = V_{in}(\delta_t - \delta_r) \tag7\]

    <p>(6) 식의 우변 $V_R + V_L$  에서 $\delta_r$ 이 사라지므로 직진 성분인 $\delta_t$ 만 남게 된다.</p>

\[\left((Mr^2 + 2I_w)Ls^2 + ((Mr^2 + 2I_w)R + 2bL)s + 2bR + 2K_e K_t N^2\right)V_b(s) = 2K_t N r V_{in} \delta_t(s)\]

    <p>최종적으로 병진 운동 방정식을 전달 함수 형태로 정리하면,</p>

\[\frac{V_b(s)}{\delta_t(s)} = \frac{2K_t N r V_{in}}{(Mr^2 + 2I_w)Ls^2 + ((Mr^2 + 2I_w)R + 2bL)s + 2bR + 2K_e K_t N^2}\]
  </li>
  <li>식의 분자를 보면
    <ul>
      <li><strong>배터리 전압은 시스템의 Gain</strong> 역할을 한다.</li>
      <li>무차원 입력인 $\delta_t$ 를 물리적 전압 차원으로 변환하기 위해 제어 이득 $V_{in}$ 이 포함된다.</li>
      <li>배터리가 꽉 차서 $V_{in}$ 이 높다면 같은 $\delta_t$ 를 줘도 확 튀어 나가고</li>
      <li>배터리가 낮아서 $V_{in}$ 이 낮다면 같은 $\delta_t$ 를 줘도 로봇이 비실 거린다.</li>
    </ul>
  </li>
</ul>

<h3 id="회전-운동-전달함수">회전 운동 전달함수</h3>

<ul>
  <li>
    <p>앞서 구한 차체 회전 운동 방정식을 라플라스 변환하면</p>

\[\left(I_{eq} + \frac{2l^2I_w}{r^2}\right) s\Omega_b(s) + \frac{2l^2b}{r^2} \Omega_b(s) = \frac{l}{r} (T_R(s) - T_L(s))\]

\[\Omega_b(s) = r \frac{\Omega_R(s) - \Omega_L(s)}{2l} \quad \Rightarrow \quad \Omega_R(s) - \Omega_L(s) = \frac{2l\Omega_b(s)}{r}\]

    <p>위와 같은 식에 $T_R(s)$ 와 $T_L(s)$ 를 대입 후 (3) 식을 이용하여 $\Omega_{R,L}(s)$ 를 정리하면</p>

\[\left((I_{eq}r^2 + 2l^2I_w)Ls^2 + ((I_{eq}r^2 + 2l^2I_w)R + 2l^2bL)s + 2l^2bR + 2l^2K_eK_tN^2\right)\Omega_b(s) = rlK_t N(V_R(s) - V_L(s))\]
  </li>
  <li>식의 2차항을 보면
    <ul>
      <li><strong>$I_{eq}r^2$</strong> 은 로봇 몸체를 회전시키는데 드는 관성</li>
      <li>$2l^2I_w$ 은 바퀴의 관성이 로봇 중심에서 거리 $l$ 의 제곱만큼 증폭 된다.</li>
      <li><strong>공학적으로</strong> 직진할 때는 바퀴 관성이 $2I_w$ 였는데, 회전할 때는 $2l^2 I_w$ 가 되는 것을 알 수 있고</li>
      <li>바퀴 간격이 넓은 로봇은 바퀴를 돌려서 몸체를 돌리는 작용 때문에 관성이  거리의 제곱에 비례하게 커져서, 회전 가속 반응이 느릴 수 밖에 없다는 것을 알 수 있다.</li>
    </ul>
  </li>
  <li>식의 1차항을 보면
    <ul>
      <li>$(I_{eq}r^2 + 2l^2I_w)R + 2l^2bL$ 에서 $b$ 와 $L$ 은 보통 값이 작아서 거의 무시되고</li>
      <li>지배적인 항은 앞부분의 (기계적 관성) x (저항)이다.</li>
      <li><strong>공학적으로</strong> 이 항은 시정수와 관련이 있기 때문에</li>
      <li>저항 $R$ 이 클 수록 전류가 늦게 흘러서 토크 생성이 늦어지고, 관성이 클 수록 속도 변화가 느려진다.</li>
      <li>1차항이 클 수록 핸들을 돌렸을 때 로봇이 실제로 돌기까지 걸리는 딜레이가 길어진다는 것을 알 수 있다.
        <ul>
          <li>2차항은 실제 힘은 들어가지만 관성에 의해 무거워서 도는게 느린 느낌이라면, 1차항은 애초에 돌리기 시작하기까지가 오래 걸린다는 의미이다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>식의 상수항을 보면
    <ul>
      <li>$2l^2bR + 2l^2K_eK_tN^2$ 에서 직진 때와 비슷하게 $2l^2K_eK_tN^2$ 항이 전기적 점성 역할을 한다.</li>
      <li>물리적 마찰인 $b$ 가 없어도 모터 회로 자체가 회전을 멈추게 하려는 성질을 가진다.</li>
      <li><strong>공학적으로</strong> 여기도 $l^2$ 이 붙어있기 때문에</li>
      <li>로봇 바퀴 간격이 넓을수록 회전 할 때 바퀴가 더 빨리 돌아야하고, 그만큼 역기전력도 강하게 걸린다.</li>
      <li>따라서 넓은 로봇은 회전시키려면 힘도 많이 들지만, 전압을 끄면 그만큼 큰 제동력이 걸린다는 것을 알 수 있다.</li>
    </ul>
  </li>
  <li>식 정리 전의 우변을 보면
    <ul>
      <li>$rlK_t N (V_R(s) - V_L(s))$ 에서 전압 차이 $V_R(s) - V_L(s)$ 가  $K_t, N$ 과 곱해져 토크가 되고</li>
      <li>바퀴 반경 $r$ 을 나눠 힘이 되고</li>
      <li>
        <p>다시 오른쪽 바퀴와 왼쪽 바퀴가 땅을 미는 힘이 서로 다르면 회전하게 되므로 $l$ 을 곱해서 모멘트를 만든다.</p>

\[\text{최종 회전력} = \underbrace{(T_R - T_L)}_{\text{모터 토크}} \times \underbrace{\frac{1}{r}}_{\text{힘으로 변환}} \times \underbrace{l}_{\text{모멘트로 변환}}\]
      </li>
      <li>식을 정리하기 위해 양변에 $r^2$ 을 곱하면 현재의 우변이 된다.</li>
      <li><strong>공학적으로</strong> $l$ 을 늘리면 돌리는 힘도 강해지지만 돌리기 힘들어지는 정도인 관성은 제곱으로 강해진다.</li>
    </ul>
  </li>
  <li>
    <p>병진 운동의 경우와 마찬가지로 제어 입력 $\delta$ 로 식을 변형하면, 식 (7)을 이용하여</p>

\[\left((I_{eq}r^2 + 2l^2I_w)Ls^2 + ((I_{eq}r^2 + 2l^2I_w)R + 2l^2bL)s + 2l^2bR + 2l^2K_eK_tN^2\right)\Omega_b(s) = 2rlK_t N \delta_r(s)V_{in}\]

\[\frac{\Omega_b(s)}{\delta_r(s)} = \frac{2rlK_t NV_{in}}{(I_{eq}r^2 + 2l^2I_w)Ls^2 + ((I_{eq}r^2 + 2l^2I_w)R + 2l^2bL)s + 2l^2bR + 2l^2K_eK_tN^2}\]
  </li>
  <li>식의 분자를 보면
    <ul>
      <li>바퀴가 클수록 한 바퀴 돌 때 이동 거리가 기니까 회전 속도가 빠르다.</li>
      <li>로봇 폭이 길면 지렛대 효과처럼 돌리는 힘인 토크가 커진다.</li>
      <li>전압에는 당연히 비례한다.</li>
      <li>그렇다면 로봇 폭이 길면 토크가 강하니 회전을 더 잘 하는가? → 분모를 봐야한다.</li>
    </ul>
  </li>
  <li>식의 분모를 보면
    <ul>
      <li>로봇 폭이 길수록 관성이 $l^2$ 에 비례해서 회전 가속이 오히려 손해이다.</li>
    </ul>
  </li>
  <li><strong>결론적으로</strong>
    <ul>
      <li>좁은 골목을 다니는 배달 로봇 같이 회전이 중요하다면 $l$ 을 줄이는게 유리하고, 짐을 나르는 로봇 같이 안정성이 중요하다면 $l$ 을 길게 하는게 유리하다는 것을 알게 되었다.</li>
    </ul>
  </li>
</ul>

<h3 id="전달함수-정리">전달함수 정리</h3>

\[DC 모터 \ 전달함수 \qquad T(s) = \frac{K_tNV(s) - K_eK_tN^2\Omega(s)}{Ls + R}\]

\[병진 \ 운동 \ 전달함수 \quad \frac{V_b(s)}{\delta_t(s)} = \frac{2K_t N r V_{in}}{(Mr^2 + 2I_w)Ls^2 + ((Mr^2 + 2I_w)R + 2bL)s + 2bR + 2K_e K_t N^2}\]

\[회전 \ 운동 \ 전달함수 \quad \frac{\Omega_b(s)}{\delta_r(s)} = \frac{2rlK_t NV_{in}}{(I_{eq}r^2 + 2l^2I_w)Ls^2 + ((I_{eq}r^2 + 2l^2I_w)R + 2l^2bL)s + 2l^2bR + 2l^2K_eK_tN^2}\]]]></content><author><name>Harang Ji</name></author><category term="Robotics" /><category term="Control" /><category term="Control" /><summary type="html"><![CDATA[프로젝트를 시작하며]]></summary></entry></feed>