Plugin Parts¶
Prerequisite Reading¶
Familiarity with the concepts covered in the following sections are highly recommended:
Introduction¶
There are four classes of functionality provided by a plugin:
Algorithms (e.g. abstract algorithms and concrete algorithms)
Types (e.g. abstract types, concrete types, and wrappers)
Compilers (see Compiler Plugins for details)
This document will cover recommendations to consider when writing plugins as they relate to the first three classes of functionality. Compiler plugins are more advanced and described in Compiler Plugins.
It is not necessary for a plugin to provide functionality that fits into all of the above, e.g. a plugin with only translators is valid.
Algorithms¶
A plugin can provide algorithms. This can be done in one of two forms, abstract (i.e. a spec) or concrete (i.e. an implementation).
Abstract Algorithm¶
An abstract algorithm is a spec. Providing this alone can be useful because other developers can provide different implementations of it. Read more about abstract algorithms here. However, abstract algorithms cannot be used without at least one implementation, so it’s highly recommended to provide at least 1 concrete implementation when introducing a new abstract algorithm.
Here’s some example code showing how to declare an abstract algorithm:
from metagraph import abstract_algorithm from metagraph.plugins.core.types import Graph @abstract_algorithm("centrality.pagerank") def pagerank( graph: Graph(edge_type="map", edge_dtype={"int", "float"}), damping: float = 0.85, maxiter: int = 50, tolerance: float = 1e-05, ) -> NodeMap: pass
The abstract_algorithm
decorator denotes that the function pagerank
specifies an abstract algorithm. How the
decorator are used will be explained in more detail in the End-to-End Plugin Pathway.
The string “centrality.pagerank” denotes the name of the abstract algorithm that the function pagerank
specifies.
Since an abstract algorithm is merely a spec, there’s no need to specify a body (which is why the body of pagerank
is only pass
).
Take note of the type hints. Type hints are checked at plugin registration time to verify that the signatures of concrete algorithms match the types of the corresponding abstract algorithm.
Items in parentheses following an abstract type are properties which describe necessary conditions
that the algorithm expects. In the case of pagerank
, the input Graph
must have edge_type="map"
which means
there are values associated with the edges (i.e. weights). The edge_dtype={"int", "float"}
property indicates that
the datatype of those weights must be integer or floating point.
Default parameter values are specified in the abstract algorithm and are inherited by all concrete algorithm implementations.
Concrete Algorithm¶
A concrete algorithm is the callable implementation of an abstract algorithm.
Read more about concrete algorithms here.
Here’s an example concrete algorithm implementation using NetworkX of Page Rank.
import networkx as nx from metagraph import concrete_algorithm @concrete_algorithm("centrality.pagerank") def nx_pagerank( graph: NetworkXGraph, damping: float, maxiter: int, tolerance: float ) -> PythonNodeMap: pagerank = nx.pagerank( graph.value, alpha=damping, max_iter=maxiter, tol=tolerance, weight=None ) return PythonNodeMap(pagerank)
The concrete_algorithm
decorator denotes that the function nx_pagerank
is a concrete algorithm. How the decorator
is used will be explained in more detail in the End-to-End Plugin Pathway.
The string “centrality.pagerank” denotes the name of the concrete algorithm that the function nx_pagerank
specifies.
Here are some details about how the body of nx_pagerank
implements Page Rank:
graph
is an instance of the concrete typeNetworkXGraph
, which is intended to wrap a NetworkX graph. The implementation ofNetworkXGraph
is such that thevalue
attribute is anetworkx.Graph
.The returned value is an instance of the concrete type
PythonNodeMap
, which is an implementation of the abstract return typeNodeMap
, specified in the previous section.
Note that all the concrete types in the signature are concrete implementations of the corresponding abstract types in the signature of the abstract implementation.
Abstract properties are not repeated in the concrete signature (e.g. edge_dtype
is not specified). The only
properties which would be indicated in a concrete algorithm are concrete properties – those properties which are
specific to a NetworkXGraph
.
Despite the fact that nx_pagerank
has no default values for damping
, maxiter
, and tolerance
, when the
Metagraph resolver calls “centrality.pagerank”, the default values from the abstract algorithm are applied as needed
before calling nx_pagerank
.
Types¶
When providing algorithms, it’s useful to additionally provide the types that the algorithms use.
Be sure to read the documentation regarding types from the User Guide.
Abstract Types¶
New abstract algorithms may require new abstract types.
Here’s an example of an abstract type declaration:
from metagraph import AbstractType class EdgeMap(AbstractType): properties = { "is_directed": [True, False], "dtype": DTYPE_CHOICES, "has_negative_weights": [True, False], } unambiguous_subcomponents = {EdgeSet}
As shown above, abstract types are classes.
If new abstract types are introduced, it’s highly recommended (but not strictly required) that the plugin provide at least 1 concrete implementation of that type (i.e. a concrete type).
The introduction of new abstract types in a plugin are rare. If a plugin requires a new abstract type, consider proposing it as a core abstract type as well since it might be generally useful. Proposals can be made here.
For more about abstract types, see here.
Concrete Types¶
New concrete algorithms may require different data representations of an existing abstract type or a new abstract type introduced in a plugin.
from metagraph import ConcreteType import pandas as pd class PandasDataFrameType(ConcreteType, abstract=DataFrame): value_type = pd.DataFrame @classmethod def assert_equal(cls, obj1, obj2, aprops1, aprops2, cprops1, cprops2, *, rel_tol=1e-9, abs_tol=0.0): digits_precision = round(-math.log(rel_tol, 10)) pd.testing.assert_frame_equal( obj1, obj2, check_like=True, check_less_precise=digits_precision )
Though concrete types are implemented as classes, they have no instances in Metagraph.
They are classes with attributes and class methods used by the Metagraph resolver to find optimal translations paths.
These classes are merely tools used by the Metagraph resolver to determine how to handle the Python data structures described by the concrete type.
The attribute value_type
is used to associate a Python type with the concrete type.
It’s highly recommended to add an assert_equal
class method for testing purposes.
assert_equal
is a class method that takes two instances of the same concrete type and verifies that they represent
the same underlying data. For example, consider a concrete type for edge list style graphs. Two instances of this
concrete type can represent the same graph but might have their edges in a different order. In this case, assert_equal
would not raise any assertion errors. However, if the edge lists represented different graphs, then an assertion error
would be raised.
For more about concrete types, see here.
Wrappers¶
Since wrappers automatically introduce concrete types, wrappers are also useful to provide in plugins.
class NetworkXEdgeMap(EdgeMapWrapper, abstract=EdgeMap): def __init__( self, nx_graph, weight_label="weight", *, aprops=None ): super().__init__(aprops=aprops) self.value = nx_graph self.weight_label = weight_label self._assert_instance(nx_graph, nx.Graph) class TypeMixin: @classmethod def _compute_abstract_properties( cls, obj, props: Set[str], known_props: Dict[str, Any] ) -> Dict[str, Any]: ... return @classmethod def assert_equal(cls, obj1, obj2, aprops1, aprops2, cprops1, cprops2, *, rel_tol=1e-9, abs_tol=0.0): ... return
It’s conventional to have the underlying data stored in the value
attribute.
If the underlying abstract type has abstract properties, it is required to define _compute_abstract_properties
.
It is recommended to use the inherited _assert_instance
wrapper method to sanity check types.
It can be beneficial to add an assert_equal
class method as it gets inherited by the automatically created
concrete type and is useful for testing purposes.
For more about wrappers, see here.
Translators¶
When a plugin provides new types (which is often necessary when new algorithms are introduced), it’s frequently necessary to provide translators to have the same underlying data operated on by different plugins (see here for the motivation behind translators).
Here’s an example translator:
from metagraph.plugins.graphblas.types import GrblasNodeMap from metagraph.plugins.python.types import PythonNodeMap @translator def nodemap_from_graphblas(x: GrblasNodeMap, **props) -> PythonNodeMap: idx, vals = x.value.to_values() data = dict(zip(idx, vals)) # Python dict is the correct value_type for PythonNodeMap, so no need to wrap it return data
The implementation of translators should be as complicated as required to adequately convert from any version of one type into another type. In many cases, however, the logic can be very simple, as is the case in this example.
The translator
decorator allows the Metagraph resolver to use this translator. How the decorator are used will be
explained in more detail in the End-to-End Plugin Pathway.
Since plugins are more useful when interoperating with other plugins rather than being used in isolation, it’s useful to provide translators that translate to and from concrete types introduced in a new plugin with the rest of the Metagraph plugin ecosystem.
When writing translators, it’s infeasible to write a translator from a single concrete type to every other concrete type due to the explosive number of possible translation paths. Thus, it’s recommended to at least (when possible) write translators to the core Metagraph concrete types. The core concrete types can act as a translation hub to the concrete types introduced in external plugins.
For more about translators, see here.