1

What I want to do

  • Read a graphml file with all node and edge properties that exist in the file (arbitraty in type and count)
  • Operate on the graph
  • Add additional properties
  • Write the Graph to a graphml file again

What I have

Not much, but from what I gathered from other posts and the boost documentation:

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphml.hpp>
#include <fstream>

int main(int argc, char const *argv[]) {
  auto filename = argv[1];
  std::ifstream input_stream;
  input_stream.open(filename, std::ifstream::in);
  boost::adjacency_list<boost::setS, boost::setS> graph;
  boost::dynamic_properties props;
  boost::read_graphml(input_stream, graph, props);
  input_stream.close();
  // do stuff with the graph
  // write it out to a file again
  return 0;
}
 

What's wrong

The code above exits with a property_not_found error. The reason is, that the dynamic_properties were created without a generator_fn to dynamically create a property map whenever a previously unknown property is encountered in the file (as far as I understand it, see the free put function in dynamic_property_map.hpp.

Question

How do I create a boost::dynamic_properties object that knows how to create a dynamic map for vertex / edge attributes that are found in the graphml file? Maybe I don't even understand the use of boost::dynamic_properties correctly, if so, please point me in the right direction.

j-hap
  • 150
  • 1
  • 2
  • 9

1 Answers1

1

Note that the easiest is to use the builtin ignore_other_properties:

Live On Coliru

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphml.hpp>
#include <fstream>

int main() {
    boost::adjacency_list<> g;

    boost::dynamic_properties props(boost::ignore_other_properties);
    {
        std::ifstream input_stream("input.xml");
        read_graphml(input_stream, g, props);
    }

    write_graphml(std::cout, g, props);
}

Given input xml of

<?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="Name" attr.type="string" />
  <graph id="G" edgedefault="directed" parse.nodeids="canonical" parse.edgeids="canonical" parse.order="nodesfirst">
    <node id="n0">
      <data key="key0">A</data>
    </node>
    <node id="n1">
      <data key="key0">D</data>
    </node>
    <node id="n2">
      <data key="key0">B</data>
    </node>
    <node id="n3">
      <data key="key0">C</data>
    </node>
    <edge id="e0" source="n0" target="n1">
    </edge>
    <edge id="e1" source="n2" target="n3">
    </edge>
  </graph>
</graphml>

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">
  <graph id="G" edgedefault="directed" parse.nodeids="free" parse.edgeids="canonical" parse.order="nodesfirst">
    <node id="n0">
    </node>
    <node id="n1">
    </node>
    <node id="n2">
    </node>
    <node id="n3">
    </node>
    <edge id="e0" source="n0" target="n1">
    </edge>
    <edge id="e1" source="n2" target="n3">
    </edge>
  </graph>
</graphml>

Caveats!

Has a few caveats.

  1. Your graph model is not equipped with storage for any properties, so I'll assume you want them to be external.

  2. Your graph model assumes directionality and also forbids duplicate edges. This may not be what you need.

  3. Graphviz read/write will not roundtrip. That is, comments will be gone, and edges and nodes will have arbitrarily different keys in the ML. Of course, that should in principle not matter a lot, but depending on your expectations/requirements may be a deal breaker.

  4. Your graph model cannot even be written without an external vertex_index property, since your choice for a node-based vertex container (setS) does not have an implicit one.

How Would You Do It?

I'll drop the opinionated setS choice because of the drawbacks listed above. Note that we still assume directionality. I'll leave that for you to "fix".

A search from my existing answers shows how to use dynamic_properties with dynamic maps. Adapting to your question:

Live On Coliru

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphml.hpp>
#include <boost/property_map/function_property_map.hpp>
#include <fstream>

using Attributes = std::map<std::string, std::string>;
using DynMap     = boost::shared_ptr<boost::dynamic_property_map>;
using Graph      = boost::adjacency_list<>;
using V          = Graph::vertex_descriptor;
using E          = Graph::edge_descriptor;

static inline auto make_dyn(auto m) -> DynMap {
    using DM = boost::detail::dynamic_property_map_adaptor<decltype(m)>;
    auto sp  = boost::make_shared<DM>(m);
    return boost::static_pointer_cast<boost::dynamic_property_map>(sp);
};

int main() {
    std::map<V, Attributes> va;
    std::map<E, Attributes> ea;

    boost::dynamic_properties props(
        [vamap = boost::make_assoc_property_map(va),
         eamap = boost::make_assoc_property_map(ea)] //
        (std::string const& name, auto&& descriptor, auto&&) -> DynMap {
            if (typeid(V) == descriptor.type()) {
                return make_dyn(boost::make_function_property_map<V>(
                    [=](V v) -> std::string& { return vamap[v][name]; }));
            } else {
                return make_dyn(boost::make_function_property_map<E>(
                    [=](E e) -> std::string& { return eamap[e][name]; }));
            }
        });

    Graph g;
    std::ifstream input_stream("input.xml");
    read_graphml(input_stream, g, props);

    write_graphml(std::cout, g, props);
}

Now retains the Name attributes as expected:

<?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="Name" attr.type="string" />
  <graph id="G" edgedefault="directed" parse.nodeids="free" parse.edgeids="canonical" parse.order="nodesfirst">
    <node id="n0">
      <data key="key0">A</data>
    </node>
    <node id="n1">
      <data key="key0">D</data>
    </node>
    <node id="n2">
      <data key="key0">B</data>
    </node>
    <node id="n3">
      <data key="key0">C</data>
    </node>
    <edge id="e0" source="n0" target="n1">
    </edge>
    <edge id="e1" source="n2" target="n3">
    </edge>
  </graph>
</graphml>

UPDATE: Strongly Typed?

In response to the comment, I added a strongly typed mapper.

It turns out pretty elegant because I kind of "flipped around" the storage making the whole variant access unnecessary (except for storage). The resulting data structures are much harder to use in your own code, that elegance is bit of lie, be warned.

Live On Coliru

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/graphml.hpp>
#include <boost/property_map/function_property_map.hpp>
#include <boost/variant.hpp>
#include <fstream>

using DynMap = boost::shared_ptr<boost::dynamic_property_map>;
using Graph  = boost::adjacency_list<>;
using V      = Graph::vertex_descriptor;
using E      = Graph::edge_descriptor;
using Name   = std::string;

template <typename DescriptorT, typename T> using AttrMap = std::map<DescriptorT, T>;
template <typename DescriptorT, typename... Ts>
using VarAttr    = std::map<Name, boost::variant<AttrMap<DescriptorT, Ts>...>>;
using VSupported = VarAttr<V, std::string, double>;
using ESupported = VarAttr<E, std::string, double>;

template <typename VorE, typename... Ts>
inline DynMap instantiate(VarAttr<VorE, Ts...>& supported, Name const& name,
                          std::type_info const& type) {
    auto match_type = [&]<typename T>() -> DynMap {
        if (type != typeid(T))
            return nullptr; // non-matching

        // emplace the appropriately typed map
        using Attr         = AttrMap<VorE, T>;
        auto& [_, variant] = *supported.emplace(name, Attr()).first;
        auto& instance     = boost::get<Attr>(variant);

        return boost::make_shared<
            boost::detail::dynamic_property_map_adaptor<boost::associative_property_map<Attr>>>(
            instance);
    };

    for (auto& matched : {match_type.template operator()<Ts>()...})
        if (matched)
            return matched;

    return {};
};

int main() {
    VSupported va;
    ESupported ea;

    boost::dynamic_properties props(
        [&](std::string const& name, auto&& descriptor, auto&& value) -> DynMap {
            return typeid(V) == descriptor.type() ? instantiate<V>(va, name, value.type())
                                                  : instantiate<E>(ea, name, value.type());
        });

    Graph g;
    std::ifstream input_stream("input.xml");
    read_graphml(input_stream, g, props);

    props.property("Label", boost::make_function_property_map<E>([](E e) {
                       return boost::lexical_cast<std::string>(e);
                   }));
    write_graphml(std::cout, g, props);
}

When adding a double attribute to edges:

  <key id="key1" for="edge" attr.name="Weight" attr.type="double" />

With data like

    <edge id="e0" source="n0" target="n1">
      <data key="key1">12.3E-2</data>
    </edge>
    <edge id="e1" source="n2" target="n3">
      <data key="key1">23.4E-2</data>
    </edge>

Now prints output:

    <edge id="e0" source="n0" target="n1">
      <data key="key0">(0,1)</data>
      <data key="key2">0.123</data>
    </edge>
    <edge id="e1" source="n2" target="n3">
      <data key="key0">(2,3)</data>
      <data key="key2">0.234</data>
    </edge>

Showing that the attributes are not strings anymore.

enter image description here

sehe
  • 374,641
  • 47
  • 450
  • 633
  • As a bonus, adding a "Label" property to edges after reading: http://coliru.stacked-crooked.com/a/dc09eebcc554cd3e – sehe Feb 09 '23 at 14:23
  • Thanks! You're using `std::string` as value type in the attribute maps. When debugging I see that `generate` is called with a value type of `double` (in my data it's double attributes, but could be anything), so the value type in the map creation should also be deducable, right? – j-hap Feb 09 '23 at 15:04
  • Going over your code I think I got some concepts wrong in my head. Do the attribute maps have to exist already or can the `generate_fn` of `dynamic_properties` create new maps on-the-fly, that are then owned by the dynamic_properties? – j-hap Feb 09 '23 at 15:47
  • You can make the value type strong-typed. However the stated goals don't require it so I would avoid the complication. To the second comment: the maps are already being generated on the fly. (Just witness how many calls to `make_dyn` happen). And yes they're then owned. Witness _that_ by explaining to yourself why `write_graphml` can know about the attributes from the input XML :) – sehe Feb 09 '23 at 16:32
  • 1
    Out of curiosity I added a [strongly typed mapper](http://coliru.stacked-crooked.com/a/10fe9df4b510501f). It turns out pretty elegant because I kind of "flipped around" the storage making the whole variant access less painful (i.e. invisible). However, the resulting data structures are *much* harder to use in your own code. So it's a bit of a false elegance, be warned. Here's my life demo showing doubles being interpreted as doubles: https://imgur.com/a/Ed7wsHe – sehe Feb 10 '23 at 01:38
  • 1
    (Late brainwave to use the aliasing constructor and not use any variant storage: http://coliru.stacked-crooked.com/a/f0fc4f6fb51f727d. This means you cannot access the properties except through `dynamice_properties` which may or may not be what you want). – sehe Feb 10 '23 at 02:16
  • 1
    With a horrible kludge (`std::max` over shared pointers ), golfed to 33 lines of code: http://coliru.stacked-crooked.com/a/4a0a937d6ac1e731 (don't do this :)) – sehe Feb 10 '23 at 02:31
  • wow, thanks so much for the effort! very instructive / educational! – j-hap Feb 10 '23 at 12:49