Skip to main content

Python Parser for SDFormat files.

Project description

PySDF (python-sdformat)

PySDF is a set of python bindings for SDFormat XML. The idea is to provide a method for working with SDF that feels as fast as (or faster than) XML, but with the added convenience of syntax highlighting, auto-completion, validation, and some degree of verification.

The current bindings read SDF from any version and will parse it into a generic representation. Modifications are not validated or verified by default, allowing for more flexibility, but methods for optional validation and verification will be added in the future.

Installation

pip install python-sdformat

Usage

The elements of the binding are grouped following the official SDF spec with a few exceptions. This means that you can find the item you are looking for by following the spec's nesting, e.g., the /sensor/imu/angular_velocity/x element has the corresponding class pysdf.Sensor.Imu.AngularVelocity.X and can - inside an SDF - be accessed as parsed_sensor.imu.angular_velocity.x. The previously mentioned exceptions to this are:

  • The bindings use snake_case for variable names and CamelCase for class names
  • If an element may occur more than once inside its parent, its corresponding attribute will be a tuple its name will be plural, e.g. world.models[idx].
  • The elements Pose, Frame, Plugin, and Include occur frequently across several elements and promoted to the top level, i.e., you'd use pysdf.Pose not pysdf.Model.Pose.

Examples

Basic Reading and Writing

from pysdf import SDF

sample_sdf = """<?xml version="1.0" ?>
<sdf version="1.6">
    <model name="empty_axis">
        <link name="link1" />
        <link name="link2" />
        <joint name="joint" type="fixed">
        <parent>link1</parent>
        <child>link2</child>
        <axis/>
        </joint>
    </model>
</sdf>
"""

# string round-trip
parsed = SDF.from_xml(sample_sdf)
sdf_string = parsed.to_xml()

# file round-trip
parsed.to_file("sample.sdf")
parsed = SDF.from_file("sample.sdf")

# prettify/reformat SDF to have nice indentation
parsed = SDF.from_file("sample.sdf", remove_blank_text=True)
parsed.to_file("sample.sdf", pretty_print=True)

Building SDF manually

from pysdf import SDF, Link, Joint, Model

reference_sdf = """
<sdf version="1.6">
    <model name="empty_axis">
        <link name="link1" />
        <link name="link2" />
        <joint name="joint" type="fixed">
            <parent>link1</parent>
            <child>link2</child>
        </joint>
    </model>
</sdf>
"""

element = SDF(
    Model(
        Link(name="link1"),
        Link(name="link2"),
        Joint(
            Joint.Parent(text="link1"),
            Joint.Child(text="link2"),
            # attributes are set at the end
            # because python only accepts kwargs at the end.
            name="joint",
            type="fixed",
        ),
        name="empty_axis",
    ),
    version="1.6",
)

element.to_file("sample.sdf", pretty_print=True)

Basic Modifications

from pysdf import Link, State, Model, Link

# Complex elements (with own children) are added on first read
element = Link()
element.to_xml()  # "<link/>"
element.inertial
element.to_xml()  # "<link><inertial/></link>"

# Simple elements (basic types) are added on first write (__set__):
element.inertial.mass
element.to_xml()  # "<link><inertial/></link>"
element.inertial.mass = 5.0
element.to_xml() 
# "<link><inertial><mass>5.0</mass></inertial></link>"

# default values are populated where applicable
assert element.inertial.inertia.ixx == 1.0

# Where possible types are converted automatically
element = State.Model()
element.scale  # (1.0, 1.0, 1.0), tuple
element.scale = "1 2 3"
assert element.scale == (1.0, 2.0, 3.0)
element.scale = [5, 5, 5]
assert element.scale == (5.0, 5.0, 5.0)

# Inserting children works from sequences of kwargs
element = Model()
element.add(Link(name="test"))
element.add(Link(name="test2"), Link(name="test3"))
element.to_xml() 
# '<model><link name="test"/><link name="test2"/><link name="test3"/><pose/></model>'

Full Modification Example

from pysdf import SDF, Link

sample_sdf = """<?xml version="1.0" ?>
<sdf version="1.6">
    <model name="empty_axis">
        <link name="link1" />
        <link name="link2" />
        <joint name="joint" type="fixed">
            <parent>link1</parent>
            <child>link2</child>
        </joint>
    </model>
</sdf>
"""

parsed = SDF.from_xml(sample_sdf, remove_blank_text=True)
model = parsed.model

model.name = "modified_model"
model.links[1].add(Link.ParticleEmitter(
    Link.ParticleEmitter.Emitting(text="true"),
    name="my_emitter",
    type="box"
))

parsed.to_file("sample.sdf", pretty_print=True)

Iterating and Filtering

You can call element.iter() to recursively iterate over all child elements of a subtree (breadth-first). iter() also accepts a filter kwarg which allows you to only return children that have a desired path from the caller to the child. The filter matches the tail of the path, path elements are separated by the / character, and any SDF tag is a valid path element. This allows for easy selecting and bulk editing of specific children, e.g., use filter="pose" to select all pose elements in a SDF or filter="model/pose" to select all pose elements that are direct children of a model (the model's pose).

Appologies for the long example SDF, but I thought it would be nice to demonstrate something more real-world.

from pysdf import SDF
import numpy as np

# taken from: 
# https://github.com/ignitionrobotics/sdformat/blob/sdf12/test/sdf/joint_nested_parent_child.sdf
large_example = """
<sdf version="1.8">
  <model name="joint_nested_parent_child">
    <model name="M1">
      <link name="L1">
        <pose>0 0 1 0 0 0</pose>
      </link>
      <link name="L2">
        <pose>1 1 0 0 0 0</pose>
      </link>
      <frame name="F1">
        <pose>1 0 0 0 0 0</pose>
      </frame>
      <model name="M2">
        <pose>0 0 1 1.570796326790 0 0</pose>
        <link name="L1"/>
      </model>
    </model>

    <link name="L1">
      <pose>0 0 10 0 1.57079632679 0</pose>
    </link>

    <frame name="F1" attached_to="M1::L1">
      <pose>1 0 0 0 0 0</pose>
    </frame>

    <!-- Joint with a parent link in a nested model -->
    <joint name="J1" type="fixed">
      <pose>1 0 0 0 0 0</pose>
      <parent>M1::L1</parent>
      <child>L1</child>
    </joint>
    <!-- Joint with a sibling parent frame which is attached to a link in a nested model -->
    <joint name="J2" type="fixed">
      <pose>0 1 0 0 0 0</pose>
      <parent>F1</parent>
      <child>L1</child>
    </joint>
    <!-- Joint with a child link in a nested model -->
    <joint name="J3" type="fixed">
      <pose>0 0 1 0 0 0</pose>
      <parent>L1</parent>
      <child>M1::L2</child>
    </joint>

    <!-- Joint with a child frame in a nested model -->
    <joint name="J4" type="fixed">
      <pose>0 0 1 0 0 0</pose>
      <parent>L1</parent>
      <child>M1::F1</child>
    </joint>

    <!-- Joint with a child model in a nested model -->
    <joint name="J5" type="fixed">
      <pose>0 0 1 0 0 0</pose>
      <parent>L1</parent>
      <child>M1::M2</child>
    </joint>

    <!-- Joint with a nested model frame as a parent  -->
    <joint name="J6" type="fixed">
      <pose>0 0 1 0 0 0</pose>
      <parent>M1::__model__</parent>
      <child>L1</child>
    </joint>

    <!-- Joint with a nested model frame as a child  -->
    <joint name="J7" type="fixed">
      <pose>0 0 1 0 0 0</pose>
      <parent>L1</parent>
      <child>M1::__model__</child>
    </joint>
  </model>
</sdf>
"""

# remove_blank_text strips whitespace to allow neat formatting
# later on
element = SDF.from_xml(large_example, remove_blank_text=True)
element.version = "1.9"  # v1.9 supports pose/@degrees

# convert all poses to degrees
for pose in element.iter("pose"):
    pose.degrees = True
    pose_ndarray = np.fromstring(pose.text, count=6, sep=" ")
    rotation_rad = pose_ndarray[3:]
    rotation_deg = rotation_rad / (2*np.pi) * 360
    pose_ndarray[3:] = rotation_deg
    pose.text = " ".join(map(str, pose_ndarray))

# turn on self-collision for all links in the nested model
for link in element.iter("model/model/link"):
    link.self_collide = True

# turn self-collision off (using a different syntax)
for link in element.models[0].iter("link"):
    link.self_collide = False

# offset all links by some vector
for pose in element.iter("link/pose"):
    pose_ndarray = np.fromstring(pose.text, count=6, sep=" ")
    pose_ndarray[:3] += (0, 0, 1)
    pose.text = " ".join(map(str, pose_ndarray))

element.to_file("sample.sdf", pretty_print=True)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python-sdformat-0.3.0.tar.gz (19.5 kB view details)

Uploaded Source

Built Distribution

python_sdformat-0.3.0-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file python-sdformat-0.3.0.tar.gz.

File metadata

  • Download URL: python-sdformat-0.3.0.tar.gz
  • Upload date:
  • Size: 19.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.8.10 Linux/5.10.102.1-microsoft-standard-WSL2

File hashes

Hashes for python-sdformat-0.3.0.tar.gz
Algorithm Hash digest
SHA256 4bf11a41f1e865e6feebb6aae74f552a2c8c44458d7d4bd8ff55121fff15c3bb
MD5 2ac076b3e1e98fa7a8c442ae909f7d6a
BLAKE2b-256 82c1d4bbe706cd7210c71b653f03ff15f1b3d1b752102c7315d7b3e6c89862cb

See more details on using hashes here.

File details

Details for the file python_sdformat-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: python_sdformat-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 20.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.13 CPython/3.8.10 Linux/5.10.102.1-microsoft-standard-WSL2

File hashes

Hashes for python_sdformat-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bbb7f4c1ccfc90932a25a7f58522c57a28c065cfd50cef307a00e10335a5f2f7
MD5 64e2470ac983cb0992cf15d0187753d4
BLAKE2b-256 e22295b1f0544641640f772c660fe4879b17e1228fc5cac4eec76de82c4eaeb6

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page