これは、このセクションの複数ページの印刷可能なビューです。 印刷するには、ここをクリックしてください.

このページの通常のビューに戻る.

Artefacts ツールキット (ベータ版)

Artefacts ツールキットの使用方法

Artefacts ツールキットは、開発者の生産性を向上させるために設計された Python パッケージです。一般的なテストタスクを簡素化し、Artefacts プラットフォームと統合するヘルパー関数のコレクションを提供します。

ツールキットは以下のモジュールで構成されています:

  • 設定ヘルパー - パラメトリックテストを簡素化し、 artefacts.yaml ファイルから設定を取り込む
  • チャートヘルパー - テストデータから可視化を生成する
  • Gazebo ヘルパー - テスト中に Gazebo シミュレーションとやり取りする
  • Rosbag ヘルパー - ROS バッグファイルからデータを記録、抽出、分析する
  • Rerun ヘルパー - シミュレーション中に Rerun の .rrd ファイルを記録し、それに対してテストのアサーションを行う

インストール

pip install artefacts-toolkit

Artefacts ツールキットを使用したサンプルプロジェクト:

  • Nav2 - Nav2 ナビゲーションスタックと Gazebo を使用したサンプルプロジェクト。

1 - Artefacts ツールキットの設定ヘルパー

Artefacts ツールキットの設定ヘルパーは、 artefacts.yaml Artefacts ツールキットの設定ヘルパーは、

インポート方法:

from artefacts_toolkit.config import get_artefacts_param

関数

関数リファレンス

get_artefacts_param

artefacts.yaml ファイルで設定されたパラメータを返します。 param_type が launchに設定されている場合、ROS の起動引数として使用できるように string 型で返されます。

この関数はパラメトリックテストに特に役立ちます。 artefacts.yaml ファイルでパラメータ値のリストを定義すると(例:launch/world: ["empty.sdf", "bookstore.sdf", "restaurant.sdf"])、 artefacts run コマンドはリスト内の各値に対して1回ずつ、テストを複数回実行します。各テスト実行中、 get_artefacts_param("launch", "world") は自動的にその特定のテスト実行に対する現在のパラメータ値を返します。

これはグリッドベースのテストに特に強力です!それぞれ3つの可能な値を持つ3つのパラメータがある場合、Artefacts は自動的にテストを27回(3 × 3 × 3)実行しますが、起動ファイルのコードは変更されません。

get_artefacts_param(
    param_type,
    param_name,
    default=None,
    is_ros=True
)

パラメータ

パラメータ 型 説明 デフォルト
param_type str パラメータの名前空間/カテゴリ(例:「launch/world」の「launch」) 必須
param_name str T特定のパラメータ名(例:「launch/world」の「world」) 必須
default any artefacts.yamlでパラメータが見つからない場合に返す値 None
is_ros bool パラメータをROS パラメータ形式に変換するかどうか True

戻り値

この関数は次の動作でパラメータ値を返します:

  • param_type が "launch" で is_ros が Trueの場合: artefacts.yaml ファイルの元の型に関係なく、値を strとして返します。これはROS起動引数として使用できるようにするためです。
  • default がNone 以外に設定されている場合、artefacts が要求されたパラメータを見つけられない場合に値が返されます。これは(例えば) artefacts run の代わりに launch_test を使用しているがコード変更を行いたくない場合に便利です。また、 KeyError 例外を防ぎます。
  • その他のすべての場合:YAMLファイルの元の型(例: list、 dict、 int、 float、 strなど)で値を返します。

例

以下の設定が設定された artefacts.yaml ファイルがある場合:

scenarios:
  defaults:
    output_dirs: ["output"]
    metrics:
        - /odometry_error
    params:
      launch/world: ["bookstore.sdf", "empty.sdf"]

テスト起動ファイルでパラメータを次のように取得します:

def generate_test_description():
    try:
        world = get_artefacts_param("launch", "world")
    except FileNotFoundError:
        world = "empty.world"

    run_headless = LaunchConfiguration("run_headless")
    launch_navigation_stack = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            [
                os.path.join(
                    get_package_share_directory("sam_bot_nav2_gz"),
                    "launch",
                    "complete_navigation.launch.py"
                ),
            ]
        ),
        launch_arguments=[("run_headless", run_headless), ("world_file", world)],
    )

...# Rest of launch test file   

これにより、同じテストが2回実行されます - 1回は空のワールドで、1回は書店のワールドで - 実行間で起動ファイルのコードを変更することなく実行されます。

get_artefacts_params

指定された実行に対して artefacts.yaml で設定されたすべてのパラメータを読み込み、それらを dict として提供します。.get() などの通常の辞書メソッドと組み合わせて使用できます。

get_artefacts_params()

パラメータ

なし

戻り値

この関数は、指定された実行のすべてのパラメータを dict 型として返します。

例

以下の設定が設定された artefacts.yaml ファイルがある場合:

scenarios:
    defaults:
        params:
            headless: "True"

ヘッドレスシミュレーション用の launch_description にフラグを次のように設定できます:

@launch_pytest.fixture(scope="module")
def launch_description(rosbag_recording):
    pkg_path = Path(get_package_share_directory("my_package"))
    try:
        headless = get_artefacts_params().get("headless", "False")
    except RuntimeError:
        # artefacts パラメータが利用できない場合、デフォルトで False に設定
        headless = "False"

    start_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            str(pkg_path / "launch" / "mytestfile.launch.py")
        ),
        launch_arguments={
            "headless": headless,
        }.items(),
    )

merge_ros_params_files

2つの ROS2 yaml パラメータファイルを1つにマージし、最初のファイル(source)の値を override の値で上書きします。

merge_ros_params_files(
    source,
    override,
    destination,
    rosify=False
)

パラメータ

パラメータ 型 説明 デフォルト
source str 上書き される 元のパラメータファイルへのパス 必須
override str 上書き する パラメータを含むパラメータファイルへのパス 必須
destination str 新しいパラメータファイルを保存する場所へのパス 必須
rosify bool True の場合、パラメータを ros2 パラメータファイルのネスト形式に変換 False

戻り値

None: 新しく作成されたマージ済みパラメータファイルは destination で指定されたパスで利用可能になります。

例

以下の source と override yaml ファイルがある場合:

# source
outer:
    inner1: 1
    inner2: 
        leaf: keep
flat: src

# override
outer:
    inner2:
        leaf: replaced
        new_leaf: 321
    inner3: added
flat: override

以下のような新しい yaml ファイルが destination に保存されます:

# rosify=False (デフォルト)
outer:
    inner1: 1
    inner2:
        leaf: replaced
        new_leaf: 321
    inner3: added
flat: override

rosify=True の場合、パラメータは適切なノードの ros__parameters の下にネストされます:

#source
controller_server:
    ros__parameters:
        enable_stamped_cmd_vel: true
        controller_frequency: 20.0
        min_x_velocity_threshold: 0.001

#override
controller_server/controller_frequency: 30
controller_server/min_x_velocity_threshold: 0.005
controller_server/min_y_velocity_threshold: 0.5   
    
#destination
controller_server:
    ros__parameters:
        enable_stamped_cmd_vel: true
        controller_frequency: 30
        min_x_velocity_threshold: 0.005
        min_y_velocity_threshold: 0.5

2 - Artefacts ツールキットのチャートヘルパー

Artefacts ツールキットのチャートヘルパーは、テスト実行中にトピックから受信したデータを可視化するのに役立つように設計されています。 インポート方法:

from artefacts_toolkit.chart import make_chart

関数

関数リファレンス

make_chart

2つの提供されたトピックからのデータに基づいてインタラクティブなHTMLチャートを作成します。

make_chart(
    filepath,
    topic_x,
    topic_y,
    axis_x_name="",
    axis_y_name="",
    field_unit=None,
    output_dir="output",
    chart_name="chart",
    file_type="rosbag",
    output_format="html"
)

Parameters

パラメータ 型 説明 デフォルト
filepath str データファイル(rosbag)へのパス 必須
topic_x Union[str, list] x軸のトピック名。時間に対してプロットする場合は “Time” を使用 必須
topic_y Union[str, list] y軸のトピック名。時間に対してプロットする場合は “Time” を使用 必須
axis_x_name str x軸のラベル。単一トピックの場合は自動生成。トピックのリストを使用する場合は指定を推奨 ""
axis_y_name str y軸のラベル。単一トピックの場合は自動生成。トピックのリストを使用する場合は指定を推奨 ""
field_unit str フィールドデータの測定単位(例:「m/s」、「rad」) None
output_dir str チャートが保存されるディレクトリ "output"
chart_name str 生成されるチャートファイルの名前 "chart"
file_type str データファイルのタイプ。現在は “rosbag” をサポート "rosbag"
output_format str 出力ファイルタイプ。“html” または “csv” から選択してください "html"

戻り値

None:

output_format="html" の場合
  • output_dir に plotly html チャート <chart_name>.html を作成しますが、値は返しません。
  • 単一または複数のプロットチャートを作成できます(例を参照)
output_format="csv" の場合
  • output_dir に csv ファイル <chart_name>.csv を作成しますが、値は返しません。ダッシュボードはアップロード時に csv ファイルを自動的にチャートに変換しようとします。ファイルサイズを小さく保ちたい場合に便利です。
  • ダッシュボードは現在、単一トピックのチャートのみ自動作成できます。

例

単一トピック

以下の例では、テストが完了し rosbag が保存された後、すなわちシャットダウン後に make_chart 関数を追加しています。

# my_test_file.launch.py
# test code

...

@launch_testing.post_shutdown_test()
class TestProcOutputAfterShutdown(unittest.TestCase):
    def test_exit_code(self, rosbag_filepath):
        make_chart(
            rosbag_filepath,
            "/odom.pose.pose.position.x",
            "/odom.pose.pose.position.y",
            field_unit="m",
            chart_name="odometry_position",
            output_format="csv"
        )

以下のように、ダッシュボードでチャートとして自動的に表示される csv ファイルが生成されます:

Example Chart

トピック vs 複数トピック
make_chart(
    rosbag_filepath,
    "time",
    ["/critics_cost/ConstraintCritic.data", "/critics_cost/GoalAngleCritic.data", "/critics_cost/GoalCritic.data", "/critics_cost/CostCritic.data", "/critics_cost/PathAlignCritic.data", "/critics_cost/PathAngleCritic.data", "/critics_cost/PathFollowCritic.data", "/critics_cost/PreferForwardCritic.data"],
    axis_x_name="time",
    axis_y_name="critics",
    chart_name="Critics vs Time",
)

以下のようなチャートが生成されます:

Example Chart, topic vs topics

複数トピック vs 複数トピック
 make_chart(
        rosbag_filepath,
        topic_x=["/odom.pose.pose.position.x", "/gt_odom.pose.pose.position.x"],
        topic_y=["/odom.pose.pose.position.y", "/gt_odom.pose.pose.position.y"],
        axis_x_name="x (m)",
        axis_y_name="y (m)",
        chart_name="odom vs gt positions"
    )

以下のようなチャートが生成されます:

Example Chart, topics vs topics

3 - Artefacts ツールキットの Gazebo ヘルパー

Artefacts ツールキットの Gazebo ヘルパーは、テスト実行中に Gazebo シミュレーションとやり取りするための便利な関数を提供します。これらのユーティリティを使用すると、シミュレーションオブジェクトの検査、モデル位置へのアクセス、および Gazebo と ROS2 間のトピックブリッジングが可能になります。

インポート方法:

from artefacts_toolkit.gazebo import bridge, gz

関数

関数リファレンス

bridge.get_camera_bridge

カメラトピック用の gazebo / ros2 トピックブリッジを作成します。

bridge.get_camera_bridge(
    topic_name,
    condition=None
)

パラメータ

パラメータ 型 説明 デフォルト
topic_name str Gazebo から ROS2 へブリッジするカメラトピック名 必須
condition str このブリッジを作成するタイミングを決定するオプションの起動条件 None

戻り値

Node:指定されたカメラトピックの parameter_bridge を実行する ROS 2 Node オブジェクトを返します。このノードは起動記述に含めることができます。

例

次の例では、起動引数に基づいて条件付きで作成されるカメラブリッジを示しています。record_video が “true” に設定されている場合、ブリッジが有効になり、ROS 2 ノードが Gazebo からのカメラ画像をサブスクライブできるようになります。返された Node は、起動プロセスに含めるために LaunchDescription に追加されます。

@pytest.mark.launch_test
def generate_test_description():
    from artefacts_toolkit.gazebo import bridge
    ...

    camera_topic = "/observation_camera/image"
    bag_recorder, rosbag_filepath = get_bag_recorder([camera_topic])
    sim = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(["bringup", "/robot.launch.py"])
    )

    record_video_launch_arg = DeclareLaunchArgument(
        "record_video", default_value="true"
    )
    record_video = LaunchConfiguration("record_video")
    camera_bridge = bridge.get_camera_bridge(camera_topic, condition=IfCondition(record_video))
    
    return LaunchDescription(
        [
            record_video_launch_arg,
            sim,
            camera_bridge,
            controller_process,
            launch_testing.actions.ReadyToTest(),
        ]
    )

gz.add_entity

エンティティ XML 要素を Gazebo ワールド XML 構造に挿入します。これは提供されたワールド XML を直接変更するインプレース操作です。

gz.add_entity(world_xml, entity_xml)

パラメータ

パラメータ 型 説明 デフォルト
world_xml xml.etree.ElementTree.Element <sdf>...</sdf> 形式のワールド XML 構造 必須
entity_xml xml.etree.ElementTree.Element ワールドに挿入するエンティティ XML 要素 必須

戻り値

None: この関数は world_xml をインプレースで変更し、値を返しません。

例

次の例は、既存のワールドファイルにアクターを追加する方法を示しています。gz.load_world でワールドファイルを読み込んだ後、アクターを作成してワールド XML 構造に挿入します。

from artefacts_toolkit.gazebo import gz

# 既存のワールドファイルを読み込む
world_xml = gz.load_world("worlds/env.sdf")

# ウェイポイントでアクターを作成
waypoints = [
    [4, -3.2, 1.0],
    [0.8, -3.2, 1.0],
    [0.8, -6, 1.0],
    [4, -6, 1.0],
]
actor_xml = gz.make_actor("pedestrian_1", waypoints)

# アクターをワールドに追加
gz.add_entity(world_xml, actor_xml)

gz.load_world

Gazebo ワールドファイルを読み込んで解析し、ルート XML 要素を返します。

gz.load_world(world_file)

パラメータ

パラメータ 型 説明 デフォルト
world_file str 読み込む Gazebo ワールドファイルへのパス 必須

戻り値

xml.etree.ElementTree.Element: <sdf>...</sdf> 形式で解析されたワールドファイルのルート XML 要素を返します。

例外

FileNotFoundError: 指定されたワールドファイルが存在しない場合。

例

from artefacts_toolkit.gazebo import gz

# ワールドファイルを読み込む
world_xml = gz.load_world("worlds/env.sdf")

# ワールドを変更してエンティティを追加できます
actor_xml = gz.make_actor("pedestrian", [[0, 0, 0], [1, 1, 0]])
gz.add_entity(world_xml, actor_xml)

gz.get_sim_objects

XML 構造を解析することで Gazebo ワールドファイルからモデル情報を抽出します。この関数は、ワールドファイルで定義されているすべてのモデルの名前と元のポーズを返します。

これは、実行中のシミュレーションに問い合わせる(物理、ランダム性、または相互作用によって位置が変わっている可能性がある)よりも、ワールドファイルで定義されているモデルの初期ポーズを知る必要がある場合に便利です。

gz.get_sim_objects(world_file)

パラメータ

パラメータ 型 説明 デフォルト
world_file str 解析する Gazebo ワールドファイルへのパス 必須

戻り値

tuple: この関数は、アンパックする必要がある2つの値を持つタプルを返します:

  • 辞書のリスト:
objects = [
    {
        "name": "model_1",
        "pose": "0 0 0 0 0 0"
    }, 
    {
        "name": "model_2",
        "pose": "1 2 3 0 0 0"
    },
    ...
]
  • モデル名からポーズへのマッピング辞書:
objects_positions = {
    "model_1": "0 0 0 0 0 0", 
    "model_2": "1 2 3 0 0 0",
    ...
}

例

# Working with the objects_position dict
_, objects_positions = gz.get_sim_objects("world.sdf")
model_pose = objects_positions["model_1"]  # "0 0 0 0 0 0"

# Working with the objects list
objects, _ = gz.get_sim_objects("world.sdf")
for model in objects:
    print(f"{model['name']} is at position {model['pose']}")

gz.get_model_location

実行中の Gazebo シミュレーションでのモデルの現在の (x, y, z) 位置を取得します。ワールドファイルから元の位置を読み取る get_sim_objects とは異なり、この関数はライブシミュレーションに問い合わせてモデルの現在の位置を取得します。

gz.get_model_location(model_name)

パラメータ

パラメータ 型 説明 デフォルト
model_name str シミュレーションで問い合わせるモデルの名前 必須

戻り値

tuple:メートル単位の (x, y, z) 位置を表す3つの浮動小数点値のタプルを返します:

(x_position, y_position, z_position)

例

以下の例は、ピック&プレースタスクを検証するために get_sim_objects と get_model_location を組み合わせる方法を示しています。次の方法を示しています:

  1. ワールドファイルから初期位置を取得する
  2. ロボットがタスクを完了するのを待つ
  3. 初期位置と現在の位置を比較して、どのオブジェクトが移動したかを検出する
class TestProcOutput(unittest.TestCase):
    def test_moved_meatballs(self, proc_output, controller_process):
        # Original locations
        sim_objects, sim_objects_positions = gz.get_sim_objects("worlds/env.sdf") # this includes models poses
        meatball_models = [obj["name"] for obj in sim_objects if "karaage" in obj["name"]]
        #  Wait for the control loop to finish
        proc_output.assertWaitFor("Done with tasks execution", timeout=300)
        picked_meatballs = 0 # karaage moved for more than 10cm
        for meatball in meatball_models:
            x, y, z = gz.get_model_location(meatball)
            x_original, y_original, z_original = sim_objects_positions[meatball]
            dist = ((x - x_original) ** 2 + (y - y_original) ** 2 + (z - z_original) ** 2) ** 0.5
            if dist > 0.1: #10cm
                # Likely to have been picked and moved somewhere else
                picked_meatballs += 1
        self.assertEqual(picked_meatballs, 4)

gz.kill_gazebo

現在実行中の gazebo プロセスを強制終了します。

gz.kill_gazebo()

戻り値

None: この関数は値を返しません。

例

from artefacts_toolkit.gazebo import gz
...

@launch_testing.post_shutdown_test()
class TestProcOutputAfterShutdown(unittest.TestCase):
    def test_exit_code(self, proc_info, controller_process, rosbag_filepath):
        gz.kill_gazebo()
        ...

gz.make_actor

シミュレーションにアクターを追加するために必要な XML を作成します。提供されたウェイポイント、歩行速度、回転速度に基づいて、アクターが移動するパスが作成されます。

gz.make_actor(
    actor_name,
    waypoints,
    walk_speed=0.8,
    rotate_speed=1.8,
    enable_loop=True,
    output_type="xml"
)

パラメータ

パラメータ 型 説明 デフォルト
actor_name str アクターの名前。XML では <actor name="{actor_name}"> として記述されます 必須
waypoints list ウェイポイント座標のリスト。位置のみの場合は [[x,y,z], ...]、位置と回転の場合は [[x,y,z,roll,pitch,yaw], ...] 必須
walk_speed float アクターの歩行速度(メートル/秒) 0.8
rotate_speed float アクターの回転速度(ラジアン/秒) 1.8
enable_loop bool アクターがウェイポイントパスを継続的に繰り返すかどうか True
output_type str xml オブジェクトまたは str 文字列として返すかどうか "xml"

戻り値

output_type は "xml" または "str" に設定できます:

  • xml: Gazebo アクターの XML オブジェクト表現を返します (xml.etree.ElementTree.Element)
  • str: Gazebo アクターの XML 文字列表現を返します
<actor name="{actor_name}">
    <skin>
        <filename>https://fuel.gazebosim.org/1.0/Mingfei/models/actor/tip/files/meshes/walk.dae</filename>
    </skin>
    
    <animation name="walking">
        <filename>https://fuel.gazebosim.org/1.0/Mingfei/models/actor/tip/files/meshes/walk.dae</filename>
        <interpolate_x>true</interpolate_x>
    </animation>

    <script>
        <loop>true / false</loop>
        <delay_start>0.0</delay_start>
        <auto_start>true</auto_start>
        
        <trajectory id="0" type="walking">
            <!-- 提供された入力に基づいて生成されたウェイポイント -->
        </trajectory>
    </script>
</actor>

例

以下のウェイポイントが与えられた場合:

waypoints = [
    [4, -3.2, 1.0],
    [0.8, -3.2, 1.0],
    [0.8, -6, 1.0],
    [4, -6, 1.0],
]
actor_xml = gz.make_actor("my_actor", waypoints, walk_speed=0.8, rotate_speed=1.8, enable_loop=True)

これは以下のXMLを返します:

<actor name="my_actor">
    <skin>
        <filename>https://fuel.gazebosim.org/1.0/Mingfei/models/actor/tip/files/meshes/walk.dae</filename>
    </skin>
    
    <animation name="walking">
        <filename>https://fuel.gazebosim.org/1.0/Mingfei/models/actor/tip/files/meshes/walk.dae</filename>
        <interpolate_x>true</interpolate_x>
    </animation>

    <script>
        <loop>true</loop>
        <delay_start>0.0</delay_start>
        <auto_start>true</auto_start>
        
        <trajectory id="0" type="walking">
            <waypoint>
                <time>0</time>
                <pose>4 -3.2 1.0 0 0 3.141592653589793</pose>
            </waypoint>
            <waypoint>
                <time>4.0</time>
                <pose>0.8 -3.2 1.0 0 0 3.141592653589793</pose>
            </waypoint>
            <waypoint>
                <time>6.617993877991495</time>
                <pose>0.8 -3.2 1.0 0 0 -1.5707963267948966</pose>
            </waypoint>
            <waypoint>
                <time>10.117993877991495</time>
                <pose>0.8 -6 1.0 0 0 -1.5707963267948966</pose>
            </waypoint>
            <waypoint>
                <time>10.99065850398866</time>
                <pose>0.8 -6 1.0 0 0 0.0</pose>
            </waypoint>
            <waypoint>
                <time>14.99065850398866</time>
                <pose>4 -6 1.0 0 0 0.0</pose>
            </waypoint>
            <waypoint>
                <time>15.863323129985826</time>
                <pose>4 -6 1.0 0 0 1.5707963267948966</pose>
            </waypoint>
            <waypoint>
                <time>19.363323129985826</time>
                <pose>4 -3.2 1.0 0 0 1.5707963267948966</pose>
            </waypoint>
            <waypoint>
                <time>20.23598775598299</time>
                <pose>4 -3.2 1.0 0 0 3.141592653589793</pose>
            </waypoint>
            
        </trajectory>
    </script>
</actor>

4 - Artefacts ツールキットの Rosbag ヘルパー

Artefacts ツールキットの Rosbag ヘルパーは、テスト実行中に ROS バッグファイルを作成、記録、およびデータを抽出するための便利な関数を提供します。これらのユーティリティは、分析と可視化のために処理されるトピックデータをキャプチャするのに役立ちます。

インポート方法:

from artefacts_toolkit.rosbag import get_bag_recorder, image_topics, message_topics

関数

関数リファレンス

get_bag_recorder

指定されたトピック名のリストに対する rosbag2 レコーダーを作成し、ノードとファイルパスを返します。

rosbag.get_bag_recorder(
    topic_names,
    directory="rosbags",
    use_sim_time=False
)

パラメータ

パラメータ 型 説明 デフォルト
topic_names list[str] 記録する ROS トピックのリスト  必須
directory str rosbag が保存されるディレクトリ "rosbags"
use_sim_time bool システム時間の代わりにシミュレーション時間を使用するかどうか False

戻り値

tuple: 以下を含むタプルを返します:

  • bag_recorder(ExecuteProcess): レコーダープロセスを実行する Launch Action。
  • rosbag_filepath(str): 作成される rosbag ファイルへのパス。

例

以下の例では、 bag_recorder ヘルパーを使用して rosbag を記録し、 bag_recorder を起動記述に追加し、後で rosbag_filepath をアサーションテスト(別の rosbag ヘルパー get_final_messageを使用)に使用するテスト起動ファイルを示しています:

@pytest.mark.launch_test
def generate_test_description():
    camera_topics = ["/depth_cam/rgb"]
    extra_topics = ["/odom", "/noisy_estimate"]

    bag_recorder, rosbag_filepath = rosbag.get_bag_recorder(
        camera_topics + extra_topics, use_sim_time=False
    )

    test_odometry_node = ExecuteProcess(
        cmd=[
            "python3",
            "src/test_odometry_node.py",
        ]
    )
    return LaunchDescription(
        [
            test_odometry_node,
            launch_testing.actions.ReadyToTest(),
            bag_recorder,
        ]
    ), {
        "test_odometry_node": test_odometry_node,
        "rosbag_filepath": rosbag_filepath,
    }


@launch_testing.post_shutdown_test()
class TestProcOutputAfterShutdown(unittest.TestCase):
    def test_end_position(self, rosbag_filepath):
        final_distance_from_start = message_topics.get_final_message(
            rosbag_filepath, "/distance_from_start.data"
        )

        assert final_distance_from_start < 0.1, (
            f"Final distance from start is {final_distance_from_start}, expected less than 10cm"
        )

image_topics.extract_camera_image

提供されたカメラトピックから最後に記録された画像を返します。

image_topics.extract_camera_image(
    rosbag_file_path,
    camera_topic,
    output_dir="output"
)

パラメータ

パラメータ 型 説明 デフォルト
rosbag_file_path str 記録された rosbag へのパス  必須
camera_topic str 画像を取得する ROS カメラトピックの名前 必須
output_dir str 抽出された画像を保存するディレクトリ "output"

戻り値

None: 画像で指定された output_dir に保存されます。

例

以下の例では、テスト終了後に rosbag から最後のカメラ画像を抽出する方法を示しています。 rosbag.get_rosbag_recorder 関数によって返された rosbag_filepath を使用します。

from artefacts_toolkit.rosbag import rosbag, image_topics
def test_exit_code(
    self, proc_info, test_odometry_node, rosbag_filepath
):
    ...

    image_topics.extract_camera_image(rosbag_filepath, "/depth_cam/rgb")

image_topics.extract_video

提供されたカメラトピックからすべての画像を組み合わせて WebM ビデオを作成します。

image_topics.extract_video(
    bag_path,
    topic_name,
    output_path,
    frame_rate=20
)

パラメータ

パラメータ 型 説明 デフォルト
bag_path str 記録された rosbag へのパス  必須
topic_name str ビデオを作成する ROS カメラトピックの名前 必須
output_path str ビデオが保存されるパス (.webm)        必須
frame_rate int 作成されるビデオに使用するフレームレート 20

戻り値

None: ビデオファイルは指定された output_path に保存されます。

注意

例

以下の例では、テスト終了後に rosbag からビデオを抽出する方法を示していますrosbag.get_rosbag_recorder 関数によって返された rosbag_filepath を使用します。

from artefacts_toolkit.rosbag import rosbag, image_topics
def test_exit_code(
    self, proc_info, test_odometry_node, rosbag_filepath
):
    ...

    image_topics.extract_video(rosbag_filepath, "/depth_cam/rgb", "output/depth_cam.webm")

message_topics.get_final_message

rosbag 内の指定されたトピックから最終メッセージを取得します。オプションでドット表記を使用した属性アクセスが可能です。

message_topics.get_final_message(
    rosbag_filepath,
    topic
)

パラメータ

パラメータ 型 説明 デフォルト
rosbag_filepath str 記録された rosbag へのパス 必須
topic str トピック名。ドット表記を使用してメッセージ属性を掘り下げます(例:"/distance.data")  必須

戻り値

Any: 指定されたメッセージ属性の値を返します。型はアクセスされるフィールドによって異なります。

例

rosbag.get_bag_recorder の説明と同じ例を使用します。アサーションテストを行うために distance_from_start トピックからデータを取得していることに注目してください。

@launch_testing.post_shutdown_test()
class TestProcOutputAfterShutdown(unittest.TestCase):
    def test_end_position(self, rosbag_filepath):
        final_distance_from_start = message_topics.get_final_message(
            rosbag_filepath, "/distance_from_start.data"
        )

        assert final_distance_from_start < 0.1, (
            f"Final distance from start is {final_distance_from_start}, expected less than 10cm"
        )

5 - Artefacts ツールキットの Rerun ヘルパー

Artefacts ツールキットの Rerun ヘルパーは、シミュレーション中に Rerun の .rrd ファイルを記録し、シミュレータ自体ではなくその記録に対してアサーションを行うテストのための便利な関数を提供します。

これによりテストは独立した 2 つの半分に分かれます。シミュレータ(またはその隣のロガーノード)が記録を行い、テスト自体はその記録に対してアサーションを行うため、アサーションを書くのに ROS やシミュレータの知識は必要ありません。

インポート方法:

from artefacts_toolkit.rerun import recorder, reader, video

関数

関数リファレンス

recorder.get_output_dir

記録やその他のテスト出力を書き込むべきディレクトリを返します(存在しなければ作成します)。

recorder.get_output_dir(
    directory=None
)

パラメータ

パラメータ 型 説明 デフォルト
directory str または Path デフォルトの代わりに使用するディレクトリを明示的に指定 None

戻り値

Path: 出力ディレクトリの絶対パス。

例

シミュレータ側とテスト側で同じディレクトリが解決されるため、記録の場所について両者が一致します:

from artefacts_toolkit.rerun import recorder

OUTPUT_FOLDER = recorder.get_output_dir()
rrd_path = OUTPUT_FOLDER / "my-rerun-recording.rrd"

recorder.start_recording

directory/filename に保存される Rerun の記録を開始し、記録ストリームとファイルパスを返します。

recorder.start_recording(
    application_id,
    filename="recording.rrd",
    directory=None,
    handle_sigterm=True
)

パラメータ

パラメータ 型 説明 デフォルト
application_id str Rerun ビューアに表示されるアプリケーション名 必須
filename str または Path .rrd ファイルの名前。絶対パスも指定可能で、その場合 directory は無視されます "recording.rrd"
directory str または Path 保存先ディレクトリ。get_output_dir と同様に解決されます None
handle_sigterm bool プロセスが SIGTERM を受け取ったときに記録を閉じ、クリーンに終了する True

戻り値

tuple: 以下を含むタプルを返します:

  • recording (rerun.RecordingStream): 開始された記録。グローバルな記録としても設定されるため、通常の rr.log(...) や rr.set_time(...) の呼び出しはこの記録に送られます。
  • path (Path): 書き込まれる .rrd ファイルへのパス。

例

以下の例では、シミュレータはベースの姿勢とジョイスティックのコマンドを sim_time タイムライン上に記録し、ミッション終了後にテストハーネスによって停止されます。rr.set_time で独自のタイムラインを設定しておくことで、テストは後で実時間ではなくシミュレーション時間でデータを揃えられます。

import rerun as rr
import simulator as sim
from artefacts_toolkit.rerun import recorder


class RerunSimulator(sim.SimulatorMujoco):
    def _set_sim_time(self):
        rr.set_time("sim_time", duration=float(self.mujoco_data.time))

    def _log_joy(self, joy):
        self._set_sim_time()
        rr.log("cmd/fwd", rr.Scalars(joy.axes[1]))
        rr.log("cmd/yaw", rr.Scalars(joy.axes[2]))

    def _read_state(self):
        super()._read_state()
        q = self.mujoco_data.qpos  # [0:3] base xyz, [3:7] base quat wxyz
        w, x, y, z = q[3:7]
        self._set_sim_time()
        rr.log("base", rr.Transform3D(translation=q[0:3],
                                      quaternion=rr.Quaternion(xyzw=[x, y, z, w])))
        rr.log("base/yaw", rr.Scalars(yaw_from_quat(w, x, y, z)))


def main():
    robot = sim.Robot(sim.RobotType.Tron2, True)
    robot.init("127.0.0.1")

    # robot.init() の後に呼び出す:SDK がシグナルハンドラをリセットするため
    recorder.start_recording("tron2_loop", "recording-loop.rrd")

    RerunSimulator(...).run()

reader.load

記録を一度メモリに読み込み、ファイルを再読み込みすることなく複数のアサーションで共有できるようにします。

reader.load(
    rrd
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str または Path .rrd ファイルへのパス 必須

戻り値

ChunkStore: 読み込まれた記録。他のすべての reader および video 関数は、パスの代わりにこれを受け付けます。

例

from artefacts_toolkit.rerun import reader


@pytest.fixture(scope="module")
def recording(recording_path):
    assert recording_path.exists(), f"Recording not found at {recording_path}"
    return reader.load(recording_path)

reader.get_entity_paths

記録に記録されたすべてのエンティティパスを返します。

reader.get_entity_paths(
    rrd
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須

戻り値

list[str]: エンティティパス。例:["/base", "/base/yaw", "/cmd/fwd"]。

例

assert "/bodies/uwb_tag" in reader.get_entity_paths(recording), "target was never logged"

reader.get_timelines

記録内のタイムライン名を返します。

reader.get_timelines(
    rrd
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須

戻り値

list[str]: タイムライン名。例:["log_time", "sim_time"]。


reader.get_columns

記録のデータ列名を返します。オプションで 1 つのエンティティの列のみに絞り込めます。

reader.get_columns(
    rrd,
    entity=None
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
entity str このエンティティの列のみを一覧する None

戻り値

list[str]: "/entity:Archetype:field" 形式の列名。

例

>>> reader.get_columns(recording, "/base")
['/base:Transform3D:quaternion', '/base:Transform3D:translation']

reader.get_column

1 つの列の時刻と値を、タイムラインで並べ替えて返します。

reader.get_column(
    rrd,
    column,
    timeline=None
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
column str 列名。例:"/base/yaw:Scalars:scalars" 必須
timeline str 並べ替えに使うタイムライン。デフォルトは記録固有のタイムライン(log_time 以外の最初のもの)、なければ log_time None

戻り値

tuple: 以下を含むタプルを返します:

  • times (numpy.ndarray): duration タイムラインでは秒(float)、timestamp タイムラインでは datetime64、sequence タイムラインでは整数。
  • values (numpy.ndarray): スカラーでは形状 (n,)、3 次元ベクトルでは (n, 3)。行ごとにインスタンス数が異なる場合(点群など)は、行ごとの配列を格納したオブジェクト配列。

例

times, yaw = reader.get_column(recording, "/base/yaw:Scalars:scalars")
assert times[-1] > 60.0, "mission ended early"
assert np.abs(yaw).max() <= np.pi

reader.get_final_message

列に最後に記録された値、または静的な列の値を取得します。

reader.get_final_message(
    rrd,
    column
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
column str 列名。例:"/base:Transform3D:translation" 必須

戻り値

Any: アンラップされた値。Scalars では float、平行移動では長さ 3 の配列、複数点の Points3D では (n, 3) の配列。

例

ウェイポイントは Points3D として一度だけ(静的に)記録され、テストはそれらが描く軌道からロボットがどれだけ逸れたかを測定します:

wp_col = "/network/mission_waypoints:Points3D:positions"
assert wp_col in reader.get_columns(recording), f"Could not find {wp_col} in recording"

track = np.vstack(reader.get_final_message(recording, wp_col))[:, :2]  # N x 2

reader.get_message_count

エンティティに対して行われたログ呼び出しの回数を返します。

reader.get_message_count(
    rrd,
    entity
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
entity str エンティティパス。例:"/cam" 必須

戻り値

int: エンティティに記録された(静的でない)行数。存在しない場合は 0。

例

assert reader.get_message_count(recording, "/cam") >= 100, "camera stopped publishing"

reader.to_dataframe

記録を pandas の DataFrame として返します。選択したタイムライン上の時刻ごとに 1 行です。

異なるエンティティは通常、異なるレートで記録されます。姿勢はシミュレーションステップごと、ジョイスティックのコマンドは変化したときだけ、といった具合です。step を指定すると、選択したすべてのエンティティが 1 つの均一な時間グリッドにリサンプリングされ、fill_latest_at により欠損はそれ以前の最新の値で埋められます。そのため疎なコマンドトピックが密な姿勢ストリームと行ごとに揃い、両者を直接比較できます。

reader.to_dataframe(
    rrd,
    contents=None,
    index=None,
    step=None,
    fill_latest_at=True
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
contents str または list[str] 含めるエンティティ:1 つのエンティティなら "/base"、サブツリーなら "/base/**"、またはそれらのリスト。None の場合はすべてのエンティティ None
index str 行インデックスとして使うタイムライン。デフォルトは get_column と同様 None
step float 最初から最後の時刻まで、この秒数間隔の均一なグリッドにリサンプリングする None
fill_latest_at bool 各列の欠損を、それ以前の最新の値で埋める True

戻り値

pandas.DataFrame: タイムライン列が最初に来て(duration タイムラインでは秒の float)、その後に "/entity:Archetype:field" ごとの列が続きます。静的データはすべての行に結合されます。Scalars 列は通常の float64(何も記録されていない箇所は NaN)、ベクトルは配列です。

例

@pytest.fixture(scope="module")
def df(recording_path):
    """Whole recording resampled onto a uniform 20 ms sim-time grid."""
    frame = reader.to_dataframe(recording_path, ["/base/**", "/cmd/**"], index="sim_time", step=0.02)

    out = frame.dropna(subset=["/base:Transform3D:translation"]).reset_index(drop=True)
    out["x"] = [t[0] for t in out["/base:Transform3D:translation"]]
    out["y"] = [t[1] for t in out["/base:Transform3D:translation"]]
    out["yaw"] = out["/base/yaw:Scalars:scalars"]
    out["cmd_fwd"] = out["/cmd/fwd:Scalars:scalars"].fillna(0.0)
    return out


def test_straights_cover_side_length(df, straights):
    for i, (a, b) in enumerate(straights):
        dist = math.hypot(df["x"][b - 1] - df["x"][a], df["y"][b - 1] - df["y"][a])
        assert abs(dist - SIDE_LENGTH) < DIST_TOLERANCE

video.extract_video

記録内のカメラエンティティから MP4 動画を作成します。

video.extract_video(
    rrd,
    entity,
    output_path,
    timeline=None,
    frame_rate=20
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
entity str カメラのエンティティパス。例:"/cam" 必須
output_path str または Path 動画の保存先パス(.mp4) 必須
timeline str フレームの並べ替えに使うタイムライン。デフォルトは get_column と同様 None
frame_rate int rr.Image フレームをエンコードする際のフレームレート 20

戻り値

Path: 保存された動画のパス。

例

from artefacts_toolkit.rerun import recorder, video

video.extract_video(recording, "/cam", recorder.get_output_dir() / "head_camera.mp4")

video.extract_camera_image

カメラエンティティに最後に記録された画像を PNG として保存します。

video.extract_camera_image(
    rrd,
    entity,
    output_dir="output"
)

パラメータ

パラメータ 型 説明 デフォルト
rrd str、Path または ChunkStore 記録へのパス、または読み込み済みの記録 必須
entity str カメラのエンティティパス。例:"/cam" 必須
output_dir str または Path 抽出した画像の保存先ディレクトリ "output"

戻り値

Path: 保存された画像のパス。output_dir/<entity>.last.png で、/ は _ に置き換えられます(例:output/_cam.last.png)。

例

video.extract_camera_image(recording, "/cam", recorder.get_output_dir())