Create a Graph in C-API, export for use in Python

Hello igraph users,

I’ve been using igraph effectively for visualizing neutrons in a fission chain
(see this tweet of mine https://twitter.com/abnowack/status/1574508829647118337)

I would like to speed this process up. Currently on the C side, I create an igraph_t object from a function such as “igraph_t MyDataAnalysis(MyData *data)”

To expose it to Python I use pybind11 to create a python dictionaries of the vertexes and edges. In a python script, this is wrapped and an igraph.IGraph object is created from the dictionaries.

Instead I would like to operate on the igraph_t object created from my C code directly. My goal is to expose a wrapper around igraph_t to Python, which allows me to use the IGraph python interface as well as additional compiled functions I’ve written

Is this possible? Is there an example? I’m unsure of how the “plumbing” of exposing the igraph_t to act as igraph.Graph would work.

The reason is I am hoping to wrap the igraph_t object for calculating some quantities, and I’d like to create scripts using that on the python side.

Hello,

I’m not sure I quite understand your use-case; do you construct your graphs on the C side first and do you want to wrap them in an “official” igraph.Graph object on the Python side?

The constructor on the Python side has a hidden __ptr argument that you can use to create a Graph object backed by an already-constructed igraph_t struct, but note that the constructed Graph object will then take ownership of the graph (you should not call igraph_destroy() on it any more). It’s not tested much as it is not being used any more in the core of the library, but it’s still there, so I’d start with this. Start reading the code of the constructor from here to understand what’s going on behind the scenes.

There’s also the other way round: if you have an igraph.Graph object on the Python side, it has a _raw_pointer hidden property that gives you the address of the wrapped igraph_t object, which you can then cast into an igraph_t* on the C side.

Yes, I am constructing a graph on the C side, and want to be able to see them as igraph.Graph in Python. Going the other way as you described will also be useful.

Using the __ptr argument and _raw_pointer property sounds promising, I will try those out!

Thanks

I’m able to pass around an igraph_t* to and from Python/C, and I can instantiate an igraph.Graph() from it using the __ptr method. I can call some functions, such as vcount(), ecount() etc, however calling other functions such as print() or add_vertex crashes.

I’m not sure why this is, I’m still stumbling around but posting in case there’s a hint about what’s going on.

This is the relevant portion on the C/C++ side, which uses pybind11.

    m.def("test_c_to_python", []()
          {
            igraph_integer_t num_vertices = 102;
            igraph_integer_t num_edges = 101;
            igraph_real_t diameter;
            igraph_t *g = reinterpret_cast<igraph_t*>(igraph_malloc(sizeof(igraph_t)));

            igraph_rng_seed(igraph_rng_default(), 42);

            igraph_erdos_renyi_game(g, IGRAPH_ERDOS_RENYI_GNM,
                                    num_vertices, num_edges,
                                    IGRAPH_UNDIRECTED, IGRAPH_NO_LOOPS);

            // print igraph vertices and edges
            std::cout << "test_c_to_python" << std::endl;
            std::cout << "vcount: " << igraph_vcount(g) << std::endl;
            std::cout << "ecount: " << igraph_ecount(g) << std::endl;

            auto c = py::capsule(g, "__igraph_t", 0);

            return c;
        });

And using it in Python,

>>> import fermiac, igraph             
>>> 
>>> a = fermiac.test_c_to_python()
test_c_to_python
vcount: 102
ecount: 101
>>>
>>> ga = igraph.Graph(__ptr=a)
>>>
>>> ga.vcount()
102
>>>
>>> ga.ecount()
101
>>>
>>> print(ga)

Python then terminates without an error message.

After some digging it looks like the attr member of my created igraph_t object is uninitialized, and that is causing at least the print statement to crash.

So my question now is simpler.

What initialization steps of an igraph_t object are expected to have been done, when setting to an igraph.Graph object in Python?

I need some kind of vertex attributes, would I need edge and graph attributes? What functions should I be using?

Spent more time reading about attributes and handlers. My understanding is that the C and Python interfaces have their own separate handlers for attributes, and they are not compatible.

I’m not sure it’s possible to do what I had planned. To create an igraph_t object in C, with attributes and all, and be able to pass the pointer to this object to Python and igraph.Graph(__ptr) and use the igraph_t as an igraph.Graph object.

I’m also assuming it’s not possible to go the other way, to pass an igraph.Graph object from Python into a C function accepting an igraph_t and perform operations with attributes as well.

Yes, at some point you need to decide whether you want to initialize the attribute pointer (void* attr) in a way that I think I would at least need to export a function from the Python interface that initializes the attribute handlers for an already-constructed graph in a way that is suitable for the Python interface. Would that solve the problem for you (at least partially)?

I think that would partially solve my issue, yes. Ideally I’d like an igraph.Graph object to handle an igraph_t with an initialized attribute pointer. This can be done using a function from the Python interface, as long as I can still manipulate them within C.