graph/product: add extended modular product function
diff --git a/graph/product/product.go b/graph/product/product.go
index 354b002..09f81ba 100644
--- a/graph/product/product.go
+++ b/graph/product/product.go
@@ -296,6 +296,60 @@
 	}
 }
 
+// ModularExt constructs the Modular product of a and b in dst with
+// additional control over assessing edge agreement.
+//
+// In addition to the modular product conditions, agree(u₁v₁, u₂v₂) must
+// return true when (u₁~v₁ and u₂~v₂) for an edge to be added between
+// (u₁, u₂) and (v₁, v₂) in dst. If agree is nil, Modular is called.
+//
+// ModularExt is O(n^2) time where n is the order of the Cartesian product
+// of a and b.
+func ModularExt(dst graph.Builder, a, b graph.Graph, agree func(eA, eB graph.Edge) bool) {
+	if agree == nil {
+		Modular(dst, a, b)
+		return
+	}
+
+	_, _, product := cartesianNodes(a, b)
+	if len(product) == 0 {
+		return
+	}
+
+	for _, p := range product {
+		dst.AddNode(p)
+	}
+
+	_, aUndirected := a.(graph.Undirected)
+	_, bUndirected := b.(graph.Undirected)
+	_, dstUndirected := dst.(graph.Undirected)
+	undirected := aUndirected && bUndirected && dstUndirected
+
+	n := len(product)
+	if undirected {
+		n--
+	}
+	for i, u := range product[:n] {
+		var m int
+		if undirected {
+			m = i + 1
+		}
+		for _, v := range product[m:] {
+			if u.A.ID() == v.A.ID() || u.B.ID() == v.B.ID() {
+				// No self-loops.
+				continue
+			}
+			eA := a.Edge(u.A.ID(), v.A.ID())
+			eB := b.Edge(u.B.ID(), v.B.ID())
+			inA := eA != nil
+			inB := eB != nil
+			if (inA && inB && agree(eA, eB)) || (!inA && !inB) {
+				dst.SetEdge(dst.NewEdge(u, v))
+			}
+		}
+	}
+}
+
 // cartesianNodes returns the Cartesian product of the nodes in a and b.
 func cartesianNodes(a, b graph.Graph) (aNodes, bNodes []graph.Node, product []Node) {
 	aNodes = lexicalNodes(a)
diff --git a/graph/product/product_ext_example_test.go b/graph/product/product_ext_example_test.go
new file mode 100644
index 0000000..e868faa
--- /dev/null
+++ b/graph/product/product_ext_example_test.go
@@ -0,0 +1,131 @@
+// Copyright ©2019 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package product_test
+
+import (
+	"fmt"
+
+	"gonum.org/v1/gonum/graph"
+	"gonum.org/v1/gonum/graph/iterator"
+	"gonum.org/v1/gonum/graph/product"
+	"gonum.org/v1/gonum/graph/simple"
+	"gonum.org/v1/gonum/graph/topo"
+)
+
+// person is a graph.Node representing a person.
+type person struct {
+	name string // name is the name of the person.
+	id   int64
+}
+
+// ID satisfies the graph.Node interface.
+func (n person) ID() int64 { return n.id }
+
+func ExampleModularExt_subgraphIsomorphism() {
+	// Extended attributes of the graph can be used to refine
+	// subgraph isomorphism identification. By filtering edge
+	// agreement by weight we can identify social network
+	// motifs within a larger graph.
+	//
+	// This example extracts sources of conflict from the
+	// relationships of Julius Caesar, Mark Antony and
+	// Cleopatra.
+
+	// Make a graph describing people's relationships.
+	//
+	// Edge weight indicates love/animosity.
+	people := simple.NewDirectedGraph()
+	for _, relationship := range []simple.WeightedEdge{
+		{F: person{name: "Julius Caesar", id: 0}, T: person{name: "Cleopatra", id: 1}, W: 1},
+		{F: person{name: "Cleopatra", id: 1}, T: person{name: "Julius Caesar", id: 0}, W: 1},
+		{F: person{name: "Julius Caesar", id: 0}, T: person{name: "Cornelia", id: 3}, W: 1},
+		{F: person{name: "Cornelia", id: 3}, T: person{name: "Julius Caesar", id: 0}, W: 1},
+		{F: person{name: "Mark Antony", id: 2}, T: person{name: "Cleopatra", id: 1}, W: 1},
+		{F: person{name: "Cleopatra", id: 1}, T: person{name: "Mark Antony", id: 2}, W: 1},
+		{F: person{name: "Fulvia", id: 4}, T: person{name: "Mark Antony", id: 2}, W: 1},
+		{F: person{name: "Fulvia", id: 4}, T: person{name: "Cleopatra", id: 1}, W: -1},
+		{F: person{name: "Octavia", id: 5}, T: person{name: "Mark Antony", id: 2}, W: 1},
+		{F: person{name: "Octavia", id: 5}, T: person{name: "Cleopatra", id: 1}, W: -1},
+	} {
+		people.SetEdge(relationship)
+	}
+
+	// Make a graph for the query pattern: a love triangle.
+	pattern := simple.NewDirectedGraph()
+	for _, relationsip := range []simple.WeightedEdge{
+		{F: person{name: "A", id: -1}, T: person{name: "B", id: -2}, W: 1},
+		{F: person{name: "B", id: -2}, T: person{name: "A", id: -1}, W: 1},
+		{F: person{name: "C", id: -3}, T: person{name: "A", id: -1}, W: -1},
+		{F: person{name: "C", id: -3}, T: person{name: "B", id: -2}, W: 1},
+	} {
+		pattern.SetEdge(relationsip)
+	}
+
+	// Produce the modular product of the two graphs.
+	p := simple.NewDirectedGraph()
+	product.ModularExt(p, people, pattern, func(a, b graph.Edge) bool {
+		return a.(simple.WeightedEdge).Weight() == b.(simple.WeightedEdge).Weight()
+	})
+
+	// Find the maximal cliques in the undirected induction
+	// of the modular product.
+	mc := topo.BronKerbosch(undirected{p})
+
+	// Report the cliques that are identical in order to the pattern.
+	fmt.Println("Person — Relationship position:")
+	for _, c := range mc {
+		if len(c) != pattern.Nodes().Len() {
+			continue
+		}
+		for _, p := range c {
+			// Extract the mapping between the
+			// inputs from the product.
+			p := p.(product.Node)
+			people := p.A.(person)
+			pattern := p.B.(person)
+			fmt.Printf(" %s — %s\n", people.name, pattern.name)
+		}
+		fmt.Println()
+	}
+
+	// Unordered output:
+	// Person — Relationship position:
+	//  Cleopatra — A
+	//  Mark Antony — B
+	//  Octavia — C
+	//
+	//  Cleopatra — A
+	//  Mark Antony — B
+	//  Fulvia — C
+}
+
+// undirected converts a directed graph to an undirected graph
+// with edges between nodes only where directed edges exist in
+// both directions in the original graph.
+type undirected struct {
+	graph.Directed
+}
+
+func (g undirected) From(uid int64) graph.Nodes {
+	nodes := graph.NodesOf(g.Directed.From(uid))
+	for i := 0; i < len(nodes); {
+		if g.Directed.Edge(nodes[i].ID(), uid) != nil {
+			i++
+		} else {
+			nodes[i], nodes = nodes[len(nodes)-1], nodes[:len(nodes)-1]
+		}
+	}
+	return iterator.NewOrderedNodes(nodes)
+}
+func (g undirected) Edge(xid, yid int64) graph.Edge {
+	e := g.Directed.Edge(xid, yid)
+	if e != nil && g.Directed.Edge(yid, xid) != nil {
+		return e
+	}
+	return nil
+}
+func (g undirected) EdgeBetween(xid, yid int64) graph.Edge {
+	return g.Edge(xid, yid)
+}
diff --git a/graph/product/product_test.go b/graph/product/product_test.go
index 35490c9..bf24cd8 100644
--- a/graph/product/product_test.go
+++ b/graph/product/product_test.go
@@ -314,6 +314,22 @@
 	}
 }
 
+func TestModularExt(t *testing.T) {
+	for _, test := range productTests {
+		got := simple.NewUndirectedGraph()
+		ModularExt(got, test.a, test.b, func(a, b graph.Edge) bool { return true })
+		gotBytes, _ := dot.Marshal(got, "", "", "  ")
+
+		want := simple.NewUndirectedGraph()
+		naiveModular(want, test.a, test.b)
+		wantBytes, _ := dot.Marshal(want, "", "", "  ")
+
+		if !bytes.Equal(gotBytes, wantBytes) {
+			t.Errorf("unexpected ModularExt product result for %s:\ngot:\n%s\nwant:\n%s", test.name, gotBytes, wantBytes)
+		}
+	}
+}
+
 // (u₁~v₁ and u₂~v₂) or (u₁≁v₁ and u₂≁v₂).
 func naiveModular(dst graph.Builder, a, b graph.Graph) {
 	_, _, product := cartesianNodes(a, b)