티스토리 뷰

연말에 개인 프로젝트를 진행하느라 너무 바빠서, 오랜만에 게시물을 올려본다.

이번 게시물의 목표는 Mujoco 파일을 실행해보는 것이며 코드 분석을 통해 설명하고자 한다.

 

코드 분석을 세세하게 진행하려 하다보니, 글을 쓰는 입장에서 너무 힘들고, 보는 사람도 지루할 것 같아서 github 레포지토리를 하나 만들었다. 이 레포지토리는 일주일에 한 번, 또는 한달에 두 번 정도 업데이트가 될 예정이다.

 

GitHub - jdj2261/lets-do-mujoco

Contribute to jdj2261/lets-do-mujoco development by creating an account on GitHub.

github.com

앞 챕터에서 말한대로 필자는 robosuite 프레임워크를 토대로 코드를 재구성 하였으며, 크게 달라진 것은 없다.

코드를 보고 필요하다고 생각되는 부분만 복사, 붙여넣기한 수준이다.

 

무조코 월드에 arena, panda body, panda gripper, object(table, can)를 추가한 후 실행해 보려고 한다.

실행해보면 아래의 사진처럼 나오게 된다.

Panda robot

아래는 demo 파일의 전체 코드이며, 먼저 코드 흐름을 보자.

전체 코드는 https://github.com/jdj2261/lets-do-mujoco/blob/main/demos/panda/01_panda_load.py 에 있다.

파일을 실행하면 힘 없이 쓰러지는 panda robot을 볼 수 있다.

Panda robot simulation

from mujoco_py import MjSim, MjViewer
import numpy as np

from models.world import MujocoWorldBase
from models.robots import Panda
from models.arenas import BinsArena
from models.objects import CanObject
from models.grippers import PandaGripper

def main():
    world = MujocoWorldBase()

    mujoco_robot = Panda()
    mujoco_robot.set_base_xpos([0.0, 0, 0.913])

    mujoco_arena = BinsArena()
    mujoco_arena.set_origin([0.6, 0, 0])

    gripper = PandaGripper()
    mujoco_robot.add_gripper("panda_right_hand", gripper)

    mujoco_object = CanObject(pos=[0.7, -0.3, 0.87])

    world.merge(mujoco_robot)
    world.merge(mujoco_arena)
    world.merge(mujoco_object, object_type="object")

    model = world.get_model(mode="mujoco_py")
    sim = MjSim(model)
    viewer = MjViewer(sim)
    viewer.vopt.geomgroup[0] = 0

    while True:
        sim.data.ctrl[:7] = np.random.randn(7)
        sim.step()
        viewer.render()

if __name__ == "__main__":
    main()
  • MujocoWorldBase 클래스로 world 객체를 생성하고
  • Panda, BinsArena, PandaGripper, CanOjbect 클래스로 만든 객체를 world에 merge 하였다.
  • 앞 챕터에서 말한 base.xml이라는 파일에 각 요소들을 합치는 과정이다.
  • 그 다음, model로 만들고 sim과 viewer 객체를 생성하여 시뮬레이션을 시작한다.

흐름은 어느 정도 파악됬으니, 해당 클래스를 하나하나 살펴보면 된다.

Panda, BinsArena, PandaGripper, CanObject 클래스는 최상위 클래스로 MujocoXML을 상속한 클래스를 상속하여 이용된다. MujocoXML class 역할이 무엇인지 이해하면 나머지는 쉽게 이해할 것이라 예상된다.

1. MujocoXML class

위 코드에서는 볼 수 없지만, 각 상위 클래스들은 MujocoXML 이라는 부모 클래스를 상속받는다.

이 클래스의 역할은 ElementTree라는 라이브러리를 wrapping하여, 서로 다른 요소들(robot, gripper, object etc..)을 병합하기 위한 클래스이다. 각 요소들의 <worldbody/>, <actuactor/>, <asset/> 태그를 찾아내 한 곳에 모으는 역할을 하는 클래스이다.

ElementTree 라이브러리는 xml 파서를 위한 라이브러리이며, 기본적으로 내장되어 있기에 따로 설치할 필요는 없다.

아래의 페이지로 들어가보면 이 라이브러리에 대한 설명과 예제가 자세하게 나와있다.

 

xml.etree.ElementTree — ElementTree XML API — Python 3.10.1 문서

소스 코드: Lib/xml/etree/ElementTree.py xml.etree.ElementTree 모듈은 XML 데이터를 구문 분석하고 만들기 위한 단순하고 효율적인 API를 구현합니다. 버전 3.3에서 변경: 이 모듈은 가능할 때마다 빠른 구현을

docs.python.org

아래는 MujocoXML class를 구성하는 코드 중 일부이다. 간단하게 설명하면

  • xml 파일의 최상위 루트 태그를 중심으로 하위 태그들을 찾는다.
  • create_default_element 함수는 해당 태그가 있으면 서브 엘리먼트를 리턴하고, 없다면 루트 태그에 새로운 엘리먼트를 추가하여 리턴한다.
  • resolve_asset_dependency 함수는 asset 태그의 모든 파일 경로를 절대 경로로 바꿔주는 함수이다.
import os
import xml.dom.minidom
import xml.etree.ElementTree as ET
import io

OBJECT_TYPE = ["", "object", "collision", "visual"]

class MujocoXML:
    def __init__(self, fname):
        self.file = fname
        self.folder = os.path.dirname(fname)
        self.tree = ET.parse(fname)
        self.root = self.tree.getroot()
        self.name = self.root.get("model")
        self.worldbody = self.create_default_element("worldbody")
        self.actuator = self.create_default_element("actuator")
        self.asset = self.create_default_element("asset")
        self.equality = self.create_default_element("equality")
        self.sensor = self.create_default_element("sensor")
        self.contact = self.create_default_element("contact")
        self.default = self.create_default_element("default")
        self.tendon = self.create_default_element("tendon")
        self.resolve_asset_dependency()

    def resolve_asset_dependency(self):
        for node in self.asset.findall("./*[@file]"):
            file = node.get("file")
            abs_path = os.path.abspath(self.folder)
            abs_path = os.path.join(abs_path, file)
            node.set("file", abs_path)

    def create_default_element(self, name):
        found = self.root.find(name)
        if found is not None:
            return found
        ele = ET.Element(name)
        self.root.append(ele)
        return ele

아래는 MujocoXML class 나머지 코드이고, 간단하게 설명하면

  • merge 함수는 서로 다른 파일의 태그들을 하나의 파일의 태그로 병합하는 함수이다.
    object 파일일 경우, collision 정보를 가져와 worldbody 태그에 병합한다.
  • merge_asset 함수는 서로 다른 asset 태그를 하나의 태그로 병합해주는데, 파일 경로가 중복되지 않도록 한다.
  • get_model 함수는 현재 xml 트리로부터 MjModel 객체를 리턴하는 함수이다.
  • get_xml 함수와 save_model 함수는 현재 xml 트리를 얻어내거나 확인하기 위한 함수이다.
    def merge(self, other, merge_body=True, object_type=""):
        if object_type not in OBJECT_TYPE:
            raise NameError(f"Check the body name, body name is one of {OBJECT_TYPE}")

        self.merge_asset(other)
        if object_type == "":
            if merge_body:
                for body in other.worldbody:
                    self.worldbody.append(body)
            
            for one_actuator in other.actuator:
                self.actuator.append(one_actuator)
            for one_equality in other.equality:
                self.equality.append(one_equality)
            for one_sensor in other.sensor:
                self.sensor.append(one_sensor)
            for one_contact in other.contact:
                self.contact.append(one_contact)
            for one_default in other.default:
                self.default.append(one_default)
            for one_tendon in other.tendon:
                self.tendon.append(one_tendon)
            
        if object_type == "object":
            obj = other.get_collision()
            self.worldbody.append(obj)

    def merge_asset(self, other):
        for asset in other.asset:
            asset_name = asset.get("name")
            asset_type = asset.tag
            pattern = "./{}[@name='{}']".format(asset_type, asset_name)
            if self.asset.find(pattern) is None:
                self.asset.append(asset)

    def get_model(self, mode="mujoco_py"):
        available_modes = ["mujoco_py"]
        with io.StringIO() as string:
            string.write(ET.tostring(self.root, encoding="unicode"))
            if mode == "mujoco_py":
                from mujoco_py import load_model_from_xml

                model = load_model_from_xml(string.getvalue())
                return model
            raise ValueError(
                "Unkown model mode: {}. Available options are: {}".format(
                    mode, ",".join(available_modes)
                )
            )

    def get_xml(self):
        with io.StringIO() as string:
            string.write(ET.tostring(self.root, encoding="unicode"))
            return string.getvalue()

    def save_model(self, fname, pretty=False):
        with open(fname, "w") as f:
            xml_str = ET.tostring(self.root, encoding="unicode")
            if pretty:
                # TODO: get a better pretty print library
                parsed_xml = xml.dom.minidom.parseString(xml_str)
                xml_str = parsed_xml.toprettyxml(newl="")
            f.write(xml_str)

나머지 클래스들은 MujocoXML 클래스를 상속받은 상위 클래스이며, 파싱된 각 태그의 요소를 가지고 필요한 정보를 가공하거나 리턴해주는 역할을 한다. 코드를 읽어보고 궁금한 점 있으면 댓글로 남겨주길 바란다.

나머지에 대해서도 설명을 하려 하였으나, 너무 장황해져서 쓰다 지워버렸다.

 

여기까지 설명을 마치고 다음 챕터로 넘어가려 한다.

다음 챕터는 robot kinematics 관련 파이썬 라이브러리인 pykin에 대해 간략히 소개하고자 한다.

 

긴 글 읽어주셔서 감사드리며, 피드백은 언제나 환영이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31