Repository: aica-technology/control-libraries
Pull request #240: feat: add functionality to create a robot model from string
Author: login: bpapaspyros
Created: 2025-03-24T16:24:56Z | Merged: 2025-04-08T10:50:24Z

<!-- Pull Request guidelines:
1. Give the PR a relevant and descriptive title, using one of the supported commit types:
   [ build, ci, chore, docs, feat, fix, perf, refactor, release, revert, style, test ]
2. Link the PR to the parent issue, which should already describe and motivate the PR
3. Explain how the PR addresses the parent issue
4. Add any supporting resources (screenshots, test results, etc)
5. Help the reviewer by suggesting the method and estimated time to review
6. If applicable, make a checklist of tasks that should be completed before merging
7. If applicable, mention related issues (blocked by / blocks)
8. Post creation actions: tag reviewers and link the PR to its parent issue under the Development section
-->

## Description

<!-- Required: link the parent issue -->

- #200 

<!-- Required: explain how the PR addresses the parent issue -->
This PR solves the issue by refactoring the model class to now hold both a `urdf_` and a `urdf_path_` member variable. The implemented logic is that we don't change the current interface, but try to load the urdf as a path and if that fails we try to use it as an XML to build the model (this creates a side effect, see comments).

I am opening this PR for visibility  but I expect that we should discuss the direction a bit more.

<!-- Optional: add additional resources (links, screenshots, test results, etc)
## Supporting information
-->

## Review guidelines

<!-- Required: estimate how long a review should take -->
Estimated Time of Review: 5 minutes

<!-- Optional: provide more suggestions such as "Review by commit", "Read this documentation first", etc -->

## Checklist before merging:

- [x] Confirm that the relevant changelog(s) are up-to-date in case of any user-facing changes

<!-- Optional: define further tasks that should be completed before merging
- [x] Task 1
- [ ] Task 2
-->

<!-- Optional: link related issues
## Related issues
### Blocked by:
- #0

### Blocks:
- #0
-->


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2f5d14a6..cd48128fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ Release Versions
 - feat: implement serialization helpers for trajectory classes (#228)
 - chore: clean up includes (#236)
 - feat: expose pinocchio::Data through getter (#238)
+- feat: add functionality to create a robot model from string (#200)
 
 ## 9.1.0
 
diff --git a/python/source/robot_model/bind_model.cpp b/python/source/robot_model/bind_model.cpp
index 0a1e1704d..61c82e69d 100644
--- a/python/source/robot_model/bind_model.cpp
+++ b/python/source/robot_model/bind_model.cpp
@@ -39,11 +39,11 @@ void model(py::module_& m) {
         };
   }
   return new Model(robot_name, urdf_path, callback_cpp);
-  }), "Constructor that creates a robot model instance with a name, URDF path, and an optional custom mesh loader callback. This constructor loads the Robot Geometries.", 
+  }), "Constructor that creates a robot model instance with a name, URDF XML string or path, and an optional custom mesh loader callback. This constructor loads the Robot Geometries.", 
   py::arg("robot_name"), py::arg("urdf_path"), py::arg("meshloader_callback"));
 
 
-  c.def(py::init<const std::string&, const std::string&>(), "Constructor that creates a robot model instance with a name and URDF path. This constructor doesn't loads the Robot Geometries.",
+  c.def(py::init<const std::string&, const std::string&>(), "Constructor that creates a robot model instance with a name and URDF XML string or path. This constructor doesn't loads the Robot Geometries.",
         py::arg("robot_name"),
         py::arg("urdf_path")
   );
@@ -52,7 +52,8 @@ void model(py::module_& m) {
 
   c.def("get_robot_name", &Model::get_robot_name, "Getter of the robot name.");
   c.def("set_robot_name", &Model::set_robot_name, "Setter of the robot name.", "robot_name"_a);
-  c.def("get_urdf_path", &Model::get_urdf_path, "Getter of the URDF path.");
+  c.def("get_urdf", &Model::get_urdf, "Getter of the URDF XML string.");
+  c.def("get_urdf_path", &Model::get_urdf_path, "Getter of the URDF path if it was provided for construction instead of an XML string.");
   c.def("get_number_of_joints", &Model::get_number_of_joints, "Getter of the number of joints.");
   c.def("get_joint_frames", &Model::get_joint_frames, "Getter of the joint frames of the model.");
   c.def("get_frames", &Model::get_frames, "Getter of the frames of the model.");
diff --git a/python/test/model/test_model.py b/python/test/model/test_model.py
index aefa1c324..2e818f561 100644
--- a/python/test/model/test_model.py
+++ b/python/test/model/test_model.py
@@ -67,8 +67,27 @@ def test_get_set_name(self):
         self.assertEqual(self.robot_model.get_robot_name(), "robot")
 
     def test_get_urdf_path(self):
+        self.assertFalse(self.robot_model.get_urdf_path() is None)
         self.assertEqual(self.robot_model.get_urdf_path(), self.urdf_path)
 
+        with self.assertRaises(Exception):
+            foo_model = Model("foo", "invalid_path.urdf")
+
+        invalid_xml = '''
+            <?xml version="1.0"? encoding="UTF-8"?>
+            <robot name="foo">
+            </robot>'''
+        with self.assertRaises(Exception):
+            foo_model = Model("foo", invalid_xml)
+
+        string_urdf = '''
+            <?xml version="1.0"? encoding="UTF-8"?>
+            <robot name="foo">
+                <link name="foo_link"/>
+            </robot>'''
+        foo_model = Model("foo", string_urdf)
+        self.assertTrue(foo_model.get_urdf_path() is None)
+
     def test_number_of_joints(self):
         self.assertEqual(self.robot_model.get_number_of_joints(), 7)
 
diff --git a/source/robot_model/include/robot_model/Model.hpp b/source/robot_model/include/robot_model/Model.hpp
index b73917db3..a9d8465d0 100644
--- a/source/robot_model/include/robot_model/Model.hpp
+++ b/source/robot_model/include/robot_model/Model.hpp
@@ -49,6 +49,7 @@ struct InverseKinematicsParameters {
 class Model {
 private:
   std::string robot_name_;         ///< name of the robot
+  std::string urdf_;               ///< urdf (XML string)
   std::string urdf_path_;          ///< path to the urdf file
   std::vector<std::string> frames_;///< name of the frames
   pinocchio::Model robot_model_;   ///< the robot model with pinocchio
@@ -67,6 +68,7 @@ class Model {
 
   /**
    * @brief Initialize the pinocchio geometry model from the URDF and the package paths
+   * @param urdf the URDF (XML string or filepath) of the robot
    */
   void init_geom_model(std::string urdf);
 
@@ -192,9 +194,10 @@ class Model {
    * @details If the URDF contains references to collision geometry meshes, they will not be loaded into memory.
    * To enable collision detection, use the alternate constructor.
    * @param robot_name the name to associate with the model
-   * @param urdf_path the path to the URDF file
+   * @param urdf the URDF (XML string or filepath) of the robot
+   * @throws std::runtime_error if the URDF file cannot be loaded or is invalid
    */
-  explicit Model(const std::string& robot_name, const std::string& urdf_path);
+  explicit Model(const std::string& robot_name, const std::string& urdf);
 
   /**
    * @brief Construct a robot model with collision geometries from a URDF file
@@ -204,11 +207,11 @@ class Model {
    * the optional meshloader_callback function should be defined to return an absolute path to a package
    * given the package name.
    * @param robot_name the name to associate with the model
-   * @param urdf_path the path to the URDF file
+   * @param urdf the URDF (XML string or filepath) of the robot
    * @param meshloader_callback optional callback to resolve the absolute package path from a package name
    */
   explicit Model(
-      const std::string& robot_name, const std::string& urdf_path,
+      const std::string& robot_name, const std::string& urdf,
       const std::optional<std::function<std::string(const std::string&)>>& meshloader_callback
   );
 
@@ -283,10 +286,16 @@ class Model {
   void set_robot_name(const std::string& robot_name);
 
   /**
-   * @brief Getter of the URDF path
+   * @brief Getter of the URDF string
+   * @return the URDF
+   */
+  const std::string& get_urdf() const;
+
+  /**
+   * @brief Getter of the URDF path if it was provided for construction instead of an XML string
    * @return the URDF path
    */
-  const std::string& get_urdf_path() const;
+  std::optional<std::reference_wrapper<const std::string>> get_urdf_path() const;
 
   /**
    * @brief Getter of the number of joints
@@ -593,6 +602,7 @@ inline const std::string& Model::get_robot_name() const {
 inline void swap(Model& first, Model& second) {
   using std::swap;
   swap(first.robot_name_, second.robot_name_);
+  swap(first.urdf_, second.urdf_);
   swap(first.urdf_path_, second.urdf_path_);
   swap(first.frames_, second.frames_);
   swap(first.robot_model_, second.robot_model_);
@@ -614,8 +624,16 @@ inline void Model::set_robot_name(const std::string& robot_name) {
   this->robot_name_ = robot_name;
 }
 
-inline const std::string& Model::get_urdf_path() const {
-  return this->urdf_path_;
+inline const std::string& Model::get_urdf() const {
+  return this->urdf_;
+}
+
+inline std::optional<std::reference_wrapper<const std::string>> Model::get_urdf_path() const {
+  if (this->urdf_path_.empty()) {
+    return std::nullopt;
+  } else {
+    return this->urdf_path_;
+  }
 }
 
 inline unsigned int Model::get_number_of_joints() const {
diff --git a/source/robot_model/src/Model.cpp b/source/robot_model/src/Model.cpp
index 86bba6db8..fc545c21b 100644
--- a/source/robot_model/src/Model.cpp
+++ b/source/robot_model/src/Model.cpp
@@ -5,6 +5,7 @@
 
 #include <pinocchio/algorithm/frames.hpp>
 #include <pinocchio/algorithm/joint-configuration.hpp>
+#include <stdexcept>
 
 #include "robot_model/exceptions/CollisionGeometryException.hpp"
 #include "robot_model/exceptions/FrameNotFoundException.hpp"
@@ -13,23 +14,23 @@
 
 namespace robot_model {
 Model::Model(
-    const std::string& robot_name, const std::string& urdf_path,
+    const std::string& robot_name, const std::string& urdf,
     const std::optional<std::function<std::string(const std::string&)>>& meshloader_callback
 )
     : robot_name_(robot_name),
-      urdf_path_(urdf_path),
+      urdf_(urdf),
       meshloader_callback_(meshloader_callback),
       load_collision_geometries_(true) {
   this->init_model();
 }
 
-Model::Model(const std::string& robot_name, const std::string& urdf_path)
-    : robot_name_(robot_name), urdf_path_(urdf_path) {
+Model::Model(const std::string& robot_name, const std::string& urdf) : robot_name_(robot_name), urdf_(urdf) {
   this->init_model();
 }
 
 Model::Model(const Model& other)
     : robot_name_(other.robot_name_),
+      urdf_(other.urdf_),
       urdf_path_(other.urdf_path_),
       frames_(other.frames_),
       robot_model_(other.robot_model_),
@@ -89,19 +90,23 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
+    this->urdf_path_ = this->get_urdf();
+    std::stringstream buffer;
+    buffer << file_stream.rdbuf();
+    this->urdf_ = buffer.str();
   }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
 
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  try {
+    pinocchio::urdf::buildModelFromXML(this->urdf_, this->robot_model_);
+  } catch (const std::invalid_argument& ex) {
+    throw std::runtime_error("Failed to initialize model from URDF: " + std::string(ex.what()));
+  }
   this->robot_data_ = pinocchio::Data(this->robot_model_);
 
   if (this->load_collision_geometries_) {
-    this->init_geom_model(urdf);
+    this->init_geom_model(this->urdf_);
   }
 
   // get the frames
diff --git a/source/robot_model/test/tests/test_model.cpp b/source/robot_model/test/tests/test_model.cpp
index bf3be159e..280317cb2 100644
--- a/source/robot_model/test/tests/test_model.cpp
+++ b/source/robot_model/test/tests/test_model.cpp
@@ -42,7 +42,31 @@ TEST_F(RobotModelTest, TestSetName) {
 }
 
 TEST_F(RobotModelTest, TestGetUrdfPath) {
-  EXPECT_EQ(franka->get_urdf_path(), urdf_path);
+  auto path = franka->get_urdf_path();
+  ASSERT_TRUE(path.has_value());
+  EXPECT_STREQ(path->get().c_str(), urdf_path.c_str());
+
+  EXPECT_THROW(std::make_unique<Model>("invalid_path", "invalid_path.urdf"), std::runtime_error);
+
+  // clang-format off
+  auto invalid_xml = 
+    "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+    "<robot name=\"foo\">"
+    "</robot>";
+  // clang-format on
+  EXPECT_THROW(std::make_unique<Model>("invalid_path", invalid_xml), std::runtime_error);
+
+  // clang-format off
+  auto string_urdf = 
+    "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+    "<robot name=\"foo\">"
+    "  <link name=\"foo_link\"/>"
+    "</robot>";
+  // clang-format on
+  std::unique_ptr<Model> foo;
+  EXPECT_NO_THROW(foo = std::make_unique<Model>("string_urdf", string_urdf));
+  auto path2 = foo->get_urdf_path();
+  ASSERT_FALSE(path2.has_value());
 }
 
 TEST_F(RobotModelTest, TestCopyConstructor) {


Reviews:
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T16:26:49Z
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T16:28:14Z
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-24T16:35:40Z
user: login: domire8
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T05:23:01Z
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T06:16:02Z
user: login: domire8
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T06:25:32Z
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T06:28:01Z
user: login: domire8
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T06:45:24Z
user: login: domire8
body: Sorry reviewed yesterday night and forgot to submit
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-04T06:45:42Z
user: login: bpapaspyros
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-04T06:48:58Z
user: login: domire8
body: Can you check and update the python bindings docstrings?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:45:24Z
user: login: domire8
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-08T09:07:19Z

Review comments:
diff_hunk: @@ -282,11 +284,17 @@ class Model {
    */
   void set_robot_name(const std::string& robot_name);
 
+  /**
+   * @brief Getter of the URDF string
+   * @return the URDF
+   */
+  const std::string& get_urdf() const;
+
   /**
    * @brief Getter of the URDF path
    * @return the URDF path
    */
-  const std::string& get_urdf_path() const;
+  std::optional<std::reference_wrapper<const std::string>> get_urdf_path() const;
path: source/robot_model/include/robot_model/Model.hpp
user: login: bpapaspyros
body: I went for this because I think it's nicer when you want to indirectly check if the urdf was initialized from a file, but we could simply return an empty string and agree this means there was not file. 

The optional would return `None` for python bindings which might or not be ok for us.
created_at: 2025-03-24T16:26:49Z
author_association: NONE
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: bpapaspyros
body: in the previous version, if that was not the case we would throw. Now we try to parse this as an XML. This holds the risk (which was the case before to be fair), that the string content is not a valid XML and we create an empty pinocchio model (hence the todo comment in lines 101-102). The motivation is to keep the API from breaking as much as possible.
created_at: 2025-03-24T16:28:14Z
author_association: NONE
diff_hunk: @@ -42,7 +42,9 @@ TEST_F(RobotModelTest, TestSetName) {
 }
 
 TEST_F(RobotModelTest, TestGetUrdfPath) {
-  EXPECT_EQ(franka->get_urdf_path(), urdf_path);
+  auto path = franka->get_urdf_path();
+  ASSERT_TRUE(path.has_value());
+  EXPECT_STREQ(path->get().c_str(), urdf_path.c_str());
path: source/robot_model/test/tests/test_model.cpp
user: login: bpapaspyros
body: this is kind of the side effect with using `std::optional`, we'll have to pull out the value when using `get_urdf_path`
created_at: 2025-03-24T16:35:40Z
author_association: NONE
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: domire8
body: Hmm I think your perspective here on a fully breaking API would be interesting. I wonder if this doesn't maybe make the API less clear now. For example, we could just have a urdf argument that we treat first as xml string then as filepath and whatever it is will be returned in `get_urdf`. What do you think
created_at: 2025-04-02T05:23:01Z
author_association: MEMBER
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: bpapaspyros
body: Yes, I think that's a bit what I would also consider. I still think it may eventually be useful to keep the filepath if the urdf was initialized by a file (we don't currently use it like that at all, but I would prefer to have it than not I think)
created_at: 2025-04-02T06:16:01Z
author_association: NONE
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: domire8
body: So in the sense that `get_urdf_path` would be an optional?
created_at: 2025-04-02T06:25:32Z
author_association: MEMBER
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: bpapaspyros
body: Yes. We could just return emtpy string though. But it's a bit more verbose like that
created_at: 2025-04-02T06:28:01Z
author_association: NONE
diff_hunk: @@ -89,19 +89,24 @@ std::vector<std::string> Model::resolve_package_paths_in_urdf(std::string& urdf)
 }
 
 void Model::init_model() {
-  std::ifstream file_stream(this->get_urdf_path());
-  if (!file_stream.is_open()) {
-    throw std::runtime_error("Unable to open file: " + this->get_urdf_path());
-  }
-  std::stringstream buffer;
-  buffer << file_stream.rdbuf();
-  auto urdf = buffer.str();
-
-  pinocchio::urdf::buildModelFromXML(urdf, this->robot_model_);
+  std::ifstream file_stream(this->get_urdf());
+  if (file_stream.is_open()) {
path: source/robot_model/src/Model.cpp
user: login: domire8
body: :+1:
created_at: 2025-04-02T06:45:24Z
author_association: MEMBER
diff_hunk: @@ -49,6 +49,7 @@ struct InverseKinematicsParameters {
 class Model {
 private:
   std::string robot_name_;         ///< name of the robot
+  std::string urdf_;               ///< urdf file (XML string)
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: Feel like the description is slightly off because it has nothing to do with a file anymore
created_at: 2025-04-03T18:38:21Z
author_association: MEMBER
diff_hunk: @@ -192,9 +194,9 @@ class Model {
    * @details If the URDF contains references to collision geometry meshes, they will not be loaded into memory.
    * To enable collision detection, use the alternate constructor.
    * @param robot_name the name to associate with the model
-   * @param urdf_path the path to the URDF file
+   * @param urdf_ the URDF (XML string) of the robot
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: Here too, should be clear that it can be a filepath or xml already. And a `@throws` tag would be good too
created_at: 2025-04-03T18:39:16Z
author_association: MEMBER
diff_hunk: @@ -282,11 +284,17 @@ class Model {
    */
   void set_robot_name(const std::string& robot_name);
 
+  /**
+   * @brief Getter of the URDF string
+   * @return the URDF
+   */
+  const std::string& get_urdf() const;
+
   /**
    * @brief Getter of the URDF path
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: ```suggestion
   * @brief Getter of the URDF path if it was constructed with path.. or something
```
created_at: 2025-04-03T18:39:42Z
author_association: MEMBER
diff_hunk: @@ -42,7 +42,9 @@ TEST_F(RobotModelTest, TestSetName) {
 }
 
 TEST_F(RobotModelTest, TestGetUrdfPath) {
-  EXPECT_EQ(franka->get_urdf_path(), urdf_path);
+  auto path = franka->get_urdf_path();
+  ASSERT_TRUE(path.has_value());
+  EXPECT_STREQ(path->get().c_str(), urdf_path.c_str());
path: source/robot_model/test/tests/test_model.cpp
user: login: domire8
body: Can you add a test that an invalid urdf throws?
created_at: 2025-04-03T18:40:57Z
author_association: MEMBER
diff_hunk: @@ -67,6 +67,7 @@ def test_get_set_name(self):
         self.assertEqual(self.robot_model.get_robot_name(), "robot")
 
     def test_get_urdf_path(self):
+        self.assertFalse(self.robot_model.get_urdf_path() is None)
path: python/test/model/test_model.py
user: login: domire8
body: So assuming that the urdf path is empty, a std::optional would return None?
created_at: 2025-04-03T18:42:03Z
author_association: MEMBER
diff_hunk: @@ -67,6 +67,7 @@ def test_get_set_name(self):
         self.assertEqual(self.robot_model.get_robot_name(), "robot")
 
     def test_get_urdf_path(self):
+        self.assertFalse(self.robot_model.get_urdf_path() is None)
path: python/test/model/test_model.py
user: login: bpapaspyros
body: Yes, I'll add some wider tests to actually have this happen once
created_at: 2025-04-04T06:48:58Z
author_association: NONE
diff_hunk: @@ -192,9 +194,10 @@ class Model {
    * @details If the URDF contains references to collision geometry meshes, they will not be loaded into memory.
    * To enable collision detection, use the alternate constructor.
    * @param robot_name the name to associate with the model
-   * @param urdf_path the path to the URDF file
+   * @param urdf_ the URDF (XML string of filepath) of the robot
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: ```suggestion
   * @param urdf the URDF (XML string or filepath) of the robot
```
created_at: 2025-04-07T14:43:42Z
author_association: MEMBER
diff_hunk: @@ -67,6 +68,7 @@ class Model {
 
   /**
    * @brief Initialize the pinocchio geometry model from the URDF and the package paths
+   * @param urdf the URDF string
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: ```suggestion
   * @param urdf the URDF (XML string or filepath) of the robot
```
created_at: 2025-04-07T14:43:53Z
author_association: MEMBER
diff_hunk: @@ -204,11 +207,11 @@ class Model {
    * the optional meshloader_callback function should be defined to return an absolute path to a package
    * given the package name.
    * @param robot_name the name to associate with the model
-   * @param urdf_path the path to the URDF file
+   * @param urdf_ the URDF (XML string) of the robot
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: ```suggestion
   * @param urdf the URDF (XML string or filepath) of the robot
```
created_at: 2025-04-07T14:44:18Z
author_association: MEMBER
diff_hunk: @@ -283,10 +286,16 @@ class Model {
   void set_robot_name(const std::string& robot_name);
 
   /**
-   * @brief Getter of the URDF path
+   * @brief Getter of the URDF string
+   * @return the URDF
+   */
+  const std::string& get_urdf() const;
+
+  /**
+   * @brief Getter of the URDF path if it was provided instead of an XML string
path: source/robot_model/include/robot_model/Model.hpp
user: login: domire8
body: ```suggestion
   * @brief Getter of the URDF path if it was provided for construction instead of an XML string
```
created_at: 2025-04-07T14:45:11Z
author_association: MEMBER
diff_hunk: @@ -52,6 +52,7 @@ void model(py::module_& m) {
 
   c.def("get_robot_name", &Model::get_robot_name, "Getter of the robot name.");
   c.def("set_robot_name", &Model::set_robot_name, "Setter of the robot name.", "robot_name"_a);
+  c.def("get_urdf", &Model::get_urdf, "Getter of the URDF XML string.");
   c.def("get_urdf_path", &Model::get_urdf_path, "Getter of the URDF path.");
path: python/source/robot_model/bind_model.cpp
user: login: domire8
body: ```suggestion
  c.def("get_urdf_path", &Model::get_urdf_path, "Getter of the URDF path if it was provided for construction instead of an XML string.");
```
created_at: 2025-04-08T09:07:09Z
author_association: MEMBER

PR comments:
user: login: bpapaspyros
created_at: 2025-04-04T06:23:23Z
body: @domire8 this should also be ready now after https://github.com/aica-technology/control-libraries/pull/240/commits/aef670b8de4c72e5494756a4658b0b998fa11a24
author_association: NONE
user: login: bpapaspyros
created_at: 2025-04-07T14:48:50Z
body: > Can you check and update the python bindings docstrings?

hm, yes indeed I didn't check those. Will follow up, thanks
author_association: NONE