Search code examples
c++boostboost-graphgraphml

How to read GraphML file to Boost Graph with custom Vertex Property?


I am trying to create a boost graph of robot joint configuration space. For that I am using custom VertexProperty.

struct VertexProperties {
    std::vector<double> joint_angles;
    
    VertexProperties() : joint_angles(3){}
    
};

I have saved the graph using write_graphML with below implementation

std::string BoostGraph::vertexJointAngles(Vertex& v){
   
    std::ostringstream sstream;
    std::vector<double> jointAngles(graph[v].joint_angles);

    for (std::size_t i = 0; i < jointAngles.size(); i++){
                 sstream  << jointAngles[i];
                 if(i != jointAngles.size()-1) sstream << ',';
        }

    return sstream.str();
}


```
void BoostGraph::printGraphML(std::ostream &out){

    boost::function_property_map<std::function<std::string(Vertex)>, Vertex> jointAngles([this](Vertex v)
                                                                                        {
                                                                                            return vertexJointAngles(v);
                                                                                        });

                                                                                      });   
    boost::dynamic_properties dp;

    dp.property("Joint_Angles", jointAngles);
    dp.property("edge_weight", boost::get(&EdgeProperties::weight, graph));
    
    boost::write_graphml(out, graph, dp);
}

Now I need to read the graph again in another BoostGraph class instance for query and A* search. How can I convert the joint angles saved as string in graphml to vertex property?

I tried the below implementation ( I am just starting in Boost) but its not working.

std::vector<double> BoostGraph::readJointAngles(std::string joint_angle_string){
    std::stringstream ss;

    ss << joint_angle_string;

    std::vector<double> joint_angle;
    double number;
    while (ss >> number)
        joint_angle.push_back(number);

    return joint_angle;
}

void BoostGraph::readGraphGraphML(std::istream& file){
    boost::dynamic_properties dp;

    boost::function_property_map<std::function<std::vector<double>(std::string)>, Vertex> jointAngles([this](std::string v)
                                                                                        {
                                                                                            return readJointAngles(v);
                                                                                        });
    dp.property("Joint_Angles", jointAngles );                                                                                   
    dp.property("edge_weight", boost::get(&EdgeProperties::weight, graph));
    boost::read_graphml(file, graph, dp);
}

Solution

  • Good starting point there. Nice job.

    Therefore I'm going to jump immediately to the pain point:

    Writable property maps have to return something that models lvalue semantics. Your readJointAngles assumes a "procedural" conversion task instead. That's not a property map.

    My first thought is to stay close to the concept of the property map by combining the read and write into a proxy class. The proxy class can be VertexProperties itself, but as I can imagine VertexProperties having multiple members with similar requirements, let me do the "pure" thing and have it separate here:

    struct JointAngleWrapper {
        std::vector<double>& _ref;
    
        friend std::ostream& operator<<(std::ostream& os, JointAngleWrapper wrapper) {
            for (auto sep = ""; auto el : wrapper._ref)
                os << std::exchange(sep, ",") << el;
            return os;
        }
        friend std::istream& operator>>(std::istream& is, JointAngleWrapper wrapper) {
            std::vector<double> tmp;
    
            while (is >> tmp.emplace_back())
                if (char comma{}; !(is >> comma) || comma != ',')
                    break;
    
            wrapper._ref = std::move(tmp);
    
            is.clear();
            return is;
        }
    };
    

    Now you can replace the function property-map:

        auto jointAngles = boost::make_function_property_map<Vertex>(
            [ja = get(&VertexProperties::joint_angles, graph)](Vertex v) {
                return JointAngleWrapper{ja[v]};
            });
        dp.property("Joint_Angles", jointAngles);
    

    And the output will still be identical. Actually, we have a more light-weight property map abstraction that fits the bill even more elegantly:

        auto jointAngles = make_transform_value_property_map(
            [](std::vector<double>& ja) { return JointAngleWrapper{ja}; },
            get(&VertexProperties::joint_angles, graph));
    

    A Road Bump

    However, function_property_map is still not an lvalue-property map. That's sad.

    Turns out I forgot about this snag. I've written about this rather central issue with read_graphml (or with dynamic_properties, really) before:

    The approach I took there was to create my own ReadWritePropertyMap. Which is sensible, given that it essentially merges into what we already have above (JointAngleWrapper).

    template <typename Prop> struct JA {
        Prop inner;
        JA(Prop map) : inner(map) { }
    
        // traits
        using value_type = std::string;
        using reference  = std::string;
        using key_type   = typename boost::property_traits<Prop>::key_type;
        using category   = boost::read_write_property_map_tag;
    
        friend std::string get(JA map, key_type const& key) {
            std::ostringstream oss;
            for (auto sep = ""; auto el : get(map.inner, key))
                oss << std::exchange(sep, ",") << el;
            return oss.str();
        }
        friend void put(JA map, key_type const& key, value_type const& value) {
            auto& v = get(map.inner, key);
            v.clear();
    
            std::istringstream iss(value);
            while (iss >> v.emplace_back())
                if (char comma{}; !(iss >> comma) || comma != ',')
                    break;
        }
    };
    

    Now you can use it with success:

    struct BoostGraph {
        Graph graph;
    
        auto properties() {
            boost::dynamic_properties dp;
            dp.property("Joint_Angles", JA{get(&VertexProperties::joint_angles, graph)});
            dp.property("edge_weight", get(&EdgeProperties::weight, graph));
            return dp;
        }
    
        void printGraphML(std::ostream& out) {
            write_graphml(out, graph, properties());
        }
    
        void readGraphGraphML(std::istream& file) {
            auto dp = properties();
            read_graphml(file, graph, dp);
        }
    };
    

    Which successfully roundtrips a "random" graph:

    #include <boost/graph/random.hpp>
    #include <random>
    
    int main() {
        std::stringstream file; // fake file in memory
        {
            BoostGraph g;
            std::mt19937 prng{42};
            generate_random_graph(g.graph, 5, 2, prng);
    
            for (auto v : boost::make_iterator_range(vertices(g.graph)))
                for (auto& angle : g.graph[v].joint_angles)
                    angle = std::uniform_real_distribution<double>(-M_PI, +M_PI)(prng);
    
            g.printGraphML(file);
        }
    
        {
            BoostGraph roundtrip;
            roundtrip.readGraphGraphML(file);
            roundtrip.printGraphML(std::cout);
        }
    }
    

    Prints

    <?xml version="1.0" encoding="UTF-8"?>  
    <graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">
      <key id="key0" for="node" attr.name="Joint_Angles" attr.type="string" />
      <key id="key1" for="edge" attr.name="edge_weight" attr.type="double" />
      <graph id="G" edgedefault="directed" parse.nodeids="free" parse.edgeids="canonical" parse.order="nodesfirst">
        <node id="n0">
          <data key="key0">1.75735,0.608528,-0.340343</data>
        </node>
        <node id="n1">
          <data key="key0">-2.51343,-0.256047,-1.04484</data>
        </node>
        <node id="n2">
          <data key="key0">-2.24393,0.94806,-2.78715</data>
        </node>
        <node id="n3">
          <data key="key0">1.39486,2.75551,-3.1367</data>
        </node>
        <node id="n4">
          <data key="key0">3.09266,0.738158,0.701538</data>
        </node>
        <edge id="e0" source="n1" target="n3">
          <data key="key1">0</data>
        </edge>
        <edge id="e1" source="n4" target="n0">
          <data key="key1">0</data>
        </edge>
      </graph>
    </graphml>
    

    Full Listing

    Live On Coliru

    #include <boost/graph/graphml.hpp>
    #include <boost/graph/adjacency_list.hpp>
    #include <boost/property_map/transform_value_property_map.hpp>
    
    struct VertexProperties {
        std::vector<double> joint_angles{0, 0, 0};
    };
    struct EdgeProperties {
        double weight;
    };
    
    using Graph  = boost::adjacency_list<boost::vecS, boost::vecS, boost::directedS,
                                        VertexProperties, EdgeProperties>;
    using Vertex = Graph::vertex_descriptor;
    using Edge   = Graph::edge_descriptor;
    
    template <typename Prop> struct JA {
        Prop inner;
        JA(Prop map) : inner(map) { }
    
        // traits
        using value_type = std::string;
        using reference  = std::string;
        using key_type   = typename boost::property_traits<Prop>::key_type;
        using category   = boost::read_write_property_map_tag;
    
        friend std::string get(JA map, key_type const& key) {
            std::ostringstream oss;
            for (auto sep = ""; auto el : get(map.inner, key))
                oss << std::exchange(sep, ",") << el;
            return oss.str();
        }
        friend void put(JA map, key_type const& key, value_type const& value) {
            auto& v = get(map.inner, key);
            v.clear();
    
            std::istringstream iss(value);
            while (iss >> v.emplace_back())
                if (char comma{}; !(iss >> comma) || comma != ',')
                    break;
        }
    };
    
    struct BoostGraph {
        Graph graph;
    
        auto properties() {
            boost::dynamic_properties dp;
            dp.property("Joint_Angles", JA{get(&VertexProperties::joint_angles, graph)});
            dp.property("edge_weight", get(&EdgeProperties::weight, graph));
            return dp;
        }
    
        void printGraphML(std::ostream& out) {
            write_graphml(out, graph, properties());
        }
    
        void readGraphGraphML(std::istream& file) {
            auto dp = properties();
            read_graphml(file, graph, dp);
        }
    };
    
    #include <boost/graph/random.hpp>
    #include <random>
    
    int main() {
        std::stringstream file; // fake file in memory
        {
            BoostGraph g;
            std::mt19937 prng{42};
            generate_random_graph(g.graph, 5, 2, prng);
    
            for (auto v : boost::make_iterator_range(vertices(g.graph)))
                for (auto& angle : g.graph[v].joint_angles)
                    angle = std::uniform_real_distribution<double>(-M_PI, +M_PI)(prng);
    
            g.printGraphML(file);
        }
    
        {
            BoostGraph roundtrip;
            roundtrip.readGraphGraphML(file);
            roundtrip.printGraphML(std::cout);
        }
    }