Package: python-sdformat
PySDF (python-sdformat)PySDF is a set of python bindings forSDFormat 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 fromanyversion 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.Installationpip install python-sdformatUsageThe elements of the binding are grouped following theofficial SDF
specwith 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/xelement has the corresponding classpysdf.Sensor.Imu.AngularVelocity.Xand can - inside an SDF - be accessed asparsed_sensor.imu.angular_velocity.x. The previously mentioned exceptions to
this are:The bindings usesnake_casefor variable names andCamelCasefor class namesIf 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 elementsPose,Frame,Plugin, andIncludeoccur frequently across
several elements and promoted to the top level, i.e., you'd usepysdf.Posenotpysdf.Model.Pose.ExamplesBasic Reading and WritingfrompysdfimportSDFsample_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-tripparsed=SDF.from_xml(sample_sdf)sdf_string=parsed.to_xml()# file round-tripparsed.to_file("sample.sdf")parsed=SDF.from_file("sample.sdf")# prettify/reformat SDF to have nice indentationparsed=SDF.from_file("sample.sdf",remove_blank_text=True)parsed.to_file("sample.sdf",pretty_print=True)Building SDF manuallyfrompysdfimportSDF,Link,Joint,Modelreference_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 ModificationsfrompysdfimportLink,State,Model,Link# Complex elements (with own children) are added on first readelement=Link()element.to_xml()# "<link/>"element.inertialelement.to_xml()# "<link><inertial/></link>"# Simple elements (basic types) are added on first write (__set__):element.inertial.masselement.to_xml()# "<link><inertial/></link>"element.inertial.mass=5.0element.to_xml()# "<link><inertial><mass>5.0</mass></inertial></link>"# default values are populated where applicableassertelement.inertial.inertia.ixx==1.0# Where possible types are converted automaticallyelement=State.Model()element.scale# (1.0, 1.0, 1.0), tupleelement.scale="1 2 3"assertelement.scale==(1.0,2.0,3.0)element.scale=[5,5,5]assertelement.scale==(5.0,5.0,5.0)# Inserting children works from sequences of kwargselement=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 ExamplefrompysdfimportSDF,Linksample_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.modelmodel.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 FilteringYou can callelement.iter()to recursively iterate over all child elements of
a subtree (breadth-first).iter()also accepts afilterkwarg 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., usefilter="pose"to
select all pose elements in a SDF orfilter="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.frompysdfimportSDFimportnumpyasnp# taken from:# https://github.com/ignitionrobotics/sdformat/blob/sdf12/test/sdf/joint_nested_parent_child.sdflarge_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 onelement=SDF.from_xml(large_example,remove_blank_text=True)element.version="1.9"# v1.9 supports pose/@degrees# convert all poses to degreesforposeinelement.iter("pose"):pose.degrees=Truepose_ndarray=np.fromstring(pose.text,count=6,sep=" ")rotation_rad=pose_ndarray[3:]rotation_deg=rotation_rad/(2*np.pi)*360pose_ndarray[3:]=rotation_degpose.text=" ".join(map(str,pose_ndarray))# turn on self-collision for all links in the nested modelforlinkinelement.iter("model/model/link"):link.self_collide=True# turn self-collision off (using a different syntax)forlinkinelement.model.iter("link"):link.self_collide=False# offset all links by some vectorforposeinelement.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)