Python bindings for SDFormat (2022 Edition)

I took a new approach to creating a python interface for SDFormat and I wanted to share it here to get some feedback on the design.


Reading and writing is a one-liner and version-agnostic (supports all SDF versions):

from pysdf import SDF

element = SDF.from_file("any_version.sdf")
element.to_file("result.sdf")

The result will remain unmodified, with the exception of idempotent XML operations like concatenating white space < inside_a_tag />, which will become <inside_a_tag/>. Any elements that are not part of the official SDF spec will be preserved (“dragged along”). That said, I didn’t test exotic situations like using different XML namespaces yet; if you find an interesting interaction, let me know.

As an alternative to preserving the formatting, you can have pySDF reformat the XML and pretty print it, if you instruct it to do so. This is not idempotent from an XML point of view, but - as far as I can tell - idempotent for SDF.

from pysdf import SDF

# neatly format the SDF
element = SDF.from_file("awesome.sdf", remove_blank_text=True)
element.to_file("awesome_and_formatted.sdf", pretty_print=True)

Elements that are part of the spec are accessible as attributes, are initialized lazily, and will set defaults values where specified:

from pysdf import Link

element = Link()
element.to_xml()  # "<link/>"

assert element.inertial.mass == 1.0

element.inertial.mass = 5.0
element.to_xml() 
# "<link><inertial><mass>5.0</mass></inertial></link>"

You can recursively iterate the SDF tree via element.iter() and - more importantly - filter the result using the filter kwarg to select only certain elements. This allows easy bulk edits:

from pysdf import SDF
import numpy as np

element = SDF.from_file("old.sdf", 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))

# 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("converted.sdf", pretty_print=True)

Finally, you can use pySDF to create SDF from scratch. The syntax here tries to mirror how you’d write SDF and elements are organized according to the nesting shown in the docs of the official spec:

from pysdf import SDF; Link, Joint

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("example.sdf", pretty_print=True)

More examples (and documentation) can be found on GitHub, and you can get the project from PyPi:

pip install python-sdformat

That’s it for my summary; let me know what you think of this approach and what I might be missing.


Note: I posted about a project that creates bindings before. This post is about a separate project; however, they are related in that I took the lessons learned from the previous project to make this one better and much more useful.