Search code examples
c++boostboost-graph

Boost: List the vertex and edge attributes specified in GraphML


I would like to be able to read custom graphml files using the Boost Library's read_graphml. This however requires that I specify a-priori the properties/attribute names when reading the file.

Is there a way to instead list the attributes specified in the file or check if a particular attribute exists? I can probably do this by parsing the file but was wondering if it could be done via Boost.


Solution

  • I have the following starter concept:

    Live On Coliru

    #include <boost/graph/graphml.hpp>
    #include <boost/core/demangle.hpp>
    using namespace boost;
    
    using Graph = adjacency_list<vecS, vecS, undirectedS>;
    using Vertex = Graph::vertex_descriptor;
    using Edge = Graph::edge_descriptor;
    
    struct MyGraph {
        Graph g;
        dynamic_properties dp { [=](auto const&... args) { return detect_properties(dp, args...); } };
    
        using Name = std::string;
        using EdgePropMap = std::map<Edge, std::string>;
        std::map<Name, std::shared_ptr<EdgePropMap> > _edge_properties;
    
        void read(std::istream& graphml) {
            ::boost::read_graphml(graphml, g, dp);
        }
      private:
        boost::shared_ptr<boost::dynamic_property_map> detect_properties(dynamic_properties& dp, Name const& name, boost::any const& key, boost::any const& value) {
            auto value_type = core::demangled_name(value.type());
    
            if (key.type() == typeid(Graph)) {
                std::cout << "Vertex property: " << name << ", " << value_type << "\n" << std::flush;
                //dp.property(name, boost::make_vector_property_map<Graph>(identity_property_map{}));
                //return dp.lower_bound(name)->second;
            }
            else if (key.type() == typeid(Edge)) {
                std::cout << "Edge property: " << name << ", " << value_type << "\n" << std::flush;
                if (value.type() == typeid(std::string)) {
                    auto& map = *_edge_properties.emplace(name, std::make_shared<EdgePropMap>()).first->second;
                    dp.property(name, boost::make_assoc_property_map(map));
                    return dp.lower_bound(name)->second;
                } else {
                    std::cerr << "Value type (" << value_type << ") not supported\n";
                }
            }
            else if (key.type() == typeid(Vertex)) {
                std::cout << "Vertex property: " << name << ", " << value_type << "\n" << std::flush;
                /*if (value.type() == typeid(std::string))*/ {
                    dp.property(name, boost::make_vector_property_map<std::string>(get(vertex_index, g)));
                    return dp.lower_bound(name)->second;
                }
            } else {
                std::cout << "Unknown property (" << core::demangled_name(key.type()) << ") " << name << ", " << value_type << "\n" << std::flush;
            }
            return nullptr;
        }
    };
    
    int main() {
        MyGraph g;
        g.read(std::cin);
    }
    

    For example with the sample input of this graphml the output is (Live On Coliru):

    Vertex property: color, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
    Edge property: weight, double
    Value type (double) not supported
    Edge property: weight, double
    Value type (double) not supported
    Edge property: weight, double
    Value type (double) not supported
    Edge property: weight, double
    Value type (double) not supported
    

    Improving...

    It becomes a little bit more complicated with setting up the edge property maps or when you wish to support disparate value types with them. I'd suggest using dynamic_properties as the sole access to the dynamic property maps, because then we can use shared_ptr to erase the mapped types of _edge_properties:

    Live On Coliru

    #include <boost/graph/graphml.hpp>
    #include <boost/core/demangle.hpp>
    using namespace boost;
    
    using Graph = adjacency_list<vecS, vecS, undirectedS>;
    using Vertex = Graph::vertex_descriptor;
    using Edge = Graph::edge_descriptor;
    
    struct MyGraph {
        Graph g;
        dynamic_properties dp { [=](auto const&... args) { return detect_properties(args...); } };
    
        using Name = std::string;
        std::map<Name, std::shared_ptr<void> > _edge_properties;
    
        void read(std::istream& graphml) {
            ::boost::read_graphml(graphml, g, dp);
        }
      private:
        template<typename Map>
        auto add_property(const std::string& name, Map pmap)
        {
            boost::shared_ptr<dynamic_property_map> pm(
                    boost::static_pointer_cast<dynamic_property_map>(
                        boost::make_shared<detail::dynamic_property_map_adaptor<Map> >(pmap)));
            dp.insert(name, pm);
            return pm;
        }
    
        template <typename V>
        auto add_edge_property(std::string const& name) {
            auto map = std::make_shared<std::map<Edge, V> >();
            _edge_properties.emplace(name, map);
            return add_property(name, boost::make_assoc_property_map(*map));
        }
    
        template <typename V>
        auto add_vertex_property(std::string const& name) {
            // NOTE, if vertex_index isn't present you might want to use
            // make_assoc_property_map as with the edge properties
            return add_property(name, boost::make_vector_property_map<V>(get(vertex_index, g)));
        }
    
        boost::shared_ptr<dynamic_property_map> detect_properties(Name const& name, boost::any const& key, boost::any const& value) {
            auto value_type = core::demangled_name(value.type());
    
            if (key.type() == typeid(Graph)) {
                std::cout << "Graph property detected: " << name << ", " << value_type << "\n" << std::flush;
                //dp.property(name, boost::make_vector_property_map<Graph>(identity_property_map{}));
                //return dp.lower_bound(name)->second;
            }
            else if (key.type() == typeid(Edge)) {
                std::cout << "Edge property detected: " << name << ", " << value_type << "\n" << std::flush;
                if (value.type() == typeid(std::string)) {
                    return add_edge_property<std::string>(name);
                } else if (value.type() == typeid(double)) {
                    return add_edge_property<double>(name);
                } else {
                    std::cerr << "Value type (" << value_type << ") not supported\n";
                }
            }
            else if (key.type() == typeid(Vertex)) {
                std::cout << "Vertex property detected: " << name << ", " << value_type << "\n" << std::flush;
                if (value.type() == typeid(std::string)) {
                    return add_vertex_property<std::string>(name);
                } else if (value.type() == typeid(double)) {
                    return add_vertex_property<double>(name);
                } else {
                    std::cerr << "Value type (" << value_type << ") not supported\n";
                }
            } else {
                std::cout << "Unknown property (" << core::demangled_name(key.type()) << ") " << name << ", " << value_type << "\n" << std::flush;
            }
            return nullptr;
        }
    };
    
    int main() {
        MyGraph g;
        g.read(std::cin);
    }
    

    Now prints:

    Vertex property detected: color, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >
    Edge property detected: weight, double
    

    Notes:

    The second version is actually a lot safer in that the first one "mis-used" lower_bound to find the property that was just added by name. This could break badly if there were Edge/Vertex properties by the same name.

    The second version adds a helper function that avoids that inaccuracy (add_property).