GraphStream Base Tutorial 4 : Reading from and writing to files and streams

SourceForge.net Logo

In this tutorial we will learn how to read and write graphs from and to files.

Reading and writing the easy way

Reading and writing can be a matter of one method call. Indeed, the Graph class provides two methods : read() and write() that read the snapshot of a graph at a time.

Graph graph = new DefaultGraph();
...
graph.read( "/some/graph/directory/file.dgs" );
...
graph.write( "/some/graph/directory/otherFile.dgs" ); 

These methods provide a high level of automatism. They detect the file format for you. When writing, the extension you give is used. If you do not give an extension, the GraphStream native format, DGS, is used.

When reading a graph, the file is opened to see if its format can be deduced. If this is not the case, GraphStream falls back on the extension of the file to detect its type.

These two methods should work relatively well, and will work any time if you read graphs that were written by GraphStream.

However, GraphStream handles dynamic graphs, and it is not possible to write or read a dynamic graph in one atomic method call, this would not consider the "history" of the graph, what nodes or edges appeared but now disappeared, how attributes stored on graph elements changed, what values they had, what values they have now, etc.

A single method call at one moment in time cannot store this history (it would mean GraphStream must remember every single change in memory which is clearly not feasible). Therefore, other graph reading and writing techniques are available.

Reading and writing dynamic graphs

If you are interested in dynamic graphs, you will probably end up in a program where algorithms do some sort of work on the graph at a given time step, increment to the next time step, do some work, increment anew the time, etc.

In other words you will probably have a program that works iteratively on the graph.

At each step, the graph can change :

  • Node and edges can appear and disappear ;
  • Attributes may appear, disappear and change .

In other word the data representation evolves, the data of the algorithm, simulation, program, that works on your graph changes, etc.

GraphStream provides a method to store all these changes in a file and to read them back. These changes are called event. This is why we often consider the graph not as a static structure in time, but as a flow, a stream of events.

This storage is done using a special file format named DGS (for Dynamic Graph Stream). It does not contain the description of the graph but the description of events on the graph. GraphStream also allows to read dynamic graph from other existing file formats that have been "extended". The DGS format is discussed under.

Reading such a file in one big step would not have a lot of meaning since even for a small graph in terms of number of nodes and edges, the file can be very large, storing each event that have occurred on the graph. The reader would take a lot of time to apply these changes on the graph without need, and we would only obtain the resulting final graph.

The good approach would be to read one event, then let your algorithm do some work on the graph modified by this event, then read another event, etc.

However most often, several events can come together, and a single time step can be composed of a sequence of events. The DGS file format supports this with the notion of steps. The event sequence can be punctuated with step indications that will group them. The time value associated with the step can also be stored.

The I/O API of GraphStream works using GraphReaders and GraphWriters. This is very similar to the way Java handles file with Readers and Writers. However due to the event-based nature of dynamic graph files, the graph readers do not yield data directly, but through a listener.

Therefore, to use a GraphReader you must register a GraphReaderListener in it. This listener will be called for each event occurring on the graph.

Basic dynamic graph reading

Lets see how this work :

import java.util.*;

import org.miv.graphstream.io.*;

public class TutorialBase015 implements GraphReaderListener
{
	public static void
	main( String args[] )
	{
		new TutorialBase015( args );
	}
	
	public
	TutorialBase015( String args[] )
	{
		for( String argument: args )
			parseGraph( argument );
	}
	
	protected void
	parseGraph( String fileName )
	{
		try
		{
			GraphReader reader = GraphReaderFactory.readerFor( fileName );
			
			reader.addGraphReaderListener( this );
			reader.begin( fileName );
			while( reader.nextEvents() ) {}
			reader.end();
		}
		catch( Exception e )
		{
			e.printStackTrace();
			System.exit( 1 );
		}
	}

	public void edgeAdded( String id, String from, String to, boolean directed, Map<String, Object> attributes ) throws GraphParseException
	{
		System.out.printf( "edgeAdded( %s, %s, %s %s%n", id, from, to, directed ? "directed " : "" );
	}

	public void edgeChanged( String id, Map<String, Object> attributes ) throws GraphParseException
	{
		System.out.printf( "edgeChanged %s%n", id );
	}

	public void edgeRemoved( String id ) throws GraphParseException
	{
		System.out.printf( "edgeRemoved %s%n", id );
	}

	public void graphChanged( Map<String, Object> attributes ) throws GraphParseException
	{
		System.out.printf( "graphChanged%n" );
	}

	public void nodeAdded( String id, Map<String, Object> attributes ) throws GraphParseException
	{
		System.out.printf( "nodeAdded %s%n", id );
	}

	public void nodeChanged( String id, Map<String, Object> attributes ) throws GraphParseException
	{
		System.out.printf( "nodeChanged %s%n", id );
	}

	public void nodeRemoved( String id ) throws GraphParseException
	{
		System.out.printf( "nodeRemoved %s%n", id );
	}

	public void stepBegins( double time ) throws GraphParseException
	{
		System.out.printf( "stepBegins %f%n", time );
	}

	public void unknownEventDetected( String unknown ) throws GraphParseException
	{
		System.out.printf( "unknownEventDetected %s%n", unknown );
	}
}

The reading code is very short, we use GraphReaderFactory to ensure we create the correct reader for the given file name. The factory tries to look inside the file to better guess its type. If it fails, it looks at the file name extension.

The reader given by the factory is then ready to be used to read the graph. There are two options, either read the graph in one large operation with the GraphReader.read() method or read it event by event or step by step.

As said above, reading the whole file is not a good idea if the graph is dynamic. Furthermore this is equivalent to a call to Graph.read() so better use it for that.

To read the graph event by event you proceed in four steps. First you register a GraphReaderListener to be notified of each event occurring on the graph when read from the file.

Second you open the file using the GraphReader.begin() method. This method takes as argument the graph file name and does the necessary initialisation. At this step, the graph reader listener can already receive some events.

The graph reading is then done by calling GraphReader.nextEvents() repeatedly. Each time you call it, one (or sometimes more) event is read from the graph file and sent to the listener. Despite the plural of this method, the method tries to read only one single event at a time. However, as this cannot be ensured, the method has a 's'.

The nextEvents() method returns a boolean. It is true as long as there are some events remaining to be read in the dynamic graph file. When reading is finished, you must call GraphReader.end() to close properly the file and eventually to send some events to graph reader listeners.

If you know the dynamic graph file contains steps to group events and define time steps (this is the case of DGS files and some modified file formats like GML), you can use GraphReader.nextStep() instead of GraphReader.nextEvents(). This method will read all the events of the current step and stop before the next one. Therefore when calling this method, your graph reader listener can receive a lot of events.

The remaining part of the program above is the implementation of the GraphReaderListener interface. As this is just a tutorial, here we only print some lovely messages on the console.

Dynamic graph reader and the Graph class

As you can see with the example above. The, the graph readers are not connected at all with the graph classes in the org.miv.graphstream.graph package.

This is not a limitation, but a feature. This allows to read (and write) graphs without instantiating a Graph structure in memory.

However, most of the time you want the graph events read to be reported back to a Graph instance. This can be done easily using the GraphReaderListenerHelper. This helper class implements the GraphReaderListener interface and takes as argument of its constructor a Graph instance. Each time it receives an event, it modifies the graph instance accordingly.

With the code under, you do not need to implement the GraphReaderListener interface :

DefaultGraph g = new DefaultGraph();

GraphReader r = GraphReaderFactory.readerFor( fileName );

GraphReaderListenerHelper l = new GraphReaderListenerHelper( g );
r.addGraphReaderListener( l );

r.begin( fileName );
while( r.nextEvents() ) {}
r.end();

Writing a dynamic graph

The writing of dynamic graph is done using GraphWriters. The writing is a lot simpler. You instantiate the implementation of GraphWriter that maps to the output format of your choice.

Then, you have two choices : writing the whole graph at once with the GraphWriter.write() method, or outputting it so that the dynamics is saved.

To this end, there exist a method for all kinds of events that can be generated in a graph. However, you have to call them by yourself when the event occurs.

As for readers, this is a feature : this allows you to write a graph in any format of your choice without even instantiating a Graph in memory.

As for readers also, there exist helper classes to write an existing graph. Here you have two choices : outputing the a snap shot of the graph with GraphWriterHelper, but this class is almost deprectated by the existence of the Graph.write() method, and DynamicGraphWriterHelper.

This dynamic writer, as its name suggests, this class is able to output the whole graph history of events, not only a snap shot. For this, you have to create the writer helper as soon as you create the graph (or some parts of the graph may miss in the output file). Then it works almost by itself thanks to the listener mechanism.

All you have to do, is to call DynamicGraphWriterHelper.begin() with the graph to output as argument and a filename. Then you modify your graph as usual and the file will be filled up with the corresponding events. When you are done, you merely call DynamicGraphWriterHelper.end() to cleanly close the file and that's all.

Lets see an example :

Graph graph = new DefaultGraph( "Test" );
DynamicGraphWriterHelper writer = new DynamicGraphWriterHelper();

writer.begin( graph, "test.dgs" );

writer.step( 0 );

graph.addNode( "A" );
graph.addNode( "B" );
graph.addNode( "C" );

writer.step( 1 );

graph.addEdge( "AB", "A", "B" );
graph.addEdge( "BC", "B", "C" );
graph.addEdge( "CA", "C", "A" );

writer.step( 2 );

graph.getNode( "A" ).addAttribute( "label", "A" );
graph.getNode( "B" ).addAttribute( "label", "B" );
graph.getNode( "C" ).addAttribute( "label", "C" );

writer.end();

This example also demonstrates how to insert step events into the output graph file.

css xhtml