/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.flink.graph.library.metric.undirected; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.flink.api.common.accumulators.LongCounter; import org.apache.flink.api.common.accumulators.LongMaximum; import org.apache.flink.api.common.functions.MapFunction; import org.apache.flink.api.common.functions.ReduceFunction; import org.apache.flink.api.common.operators.base.ReduceOperatorBase.CombineHint; import org.apache.flink.api.java.DataSet; import org.apache.flink.api.java.functions.FunctionAnnotation; import org.apache.flink.api.java.tuple.Tuple3; import org.apache.flink.graph.AbstractGraphAnalytic; import org.apache.flink.graph.AnalyticHelper; import org.apache.flink.graph.Edge; import org.apache.flink.graph.Graph; import org.apache.flink.graph.asm.degree.annotate.undirected.EdgeDegreePair; import org.apache.flink.graph.asm.result.PrintableResult; import org.apache.flink.graph.library.metric.undirected.EdgeMetrics.Result; import org.apache.flink.types.LongValue; import java.io.IOException; import java.text.NumberFormat; import static org.apache.flink.api.common.ExecutionConfig.PARALLELISM_DEFAULT; /** * Compute the following edge metrics in an undirected graph: * - number of triangle triplets * - number of rectangle triplets * - maximum number of triangle triplets * - maximum number of rectangle triplets * * @param <K> graph ID type * @param <VV> vertex value type * @param <EV> edge value type */ public class EdgeMetrics<K extends Comparable<K>, VV, EV> extends AbstractGraphAnalytic<K, VV, EV, Result> { private static final String TRIANGLE_TRIPLET_COUNT = "triangleTripletCount"; private static final String RECTANGLE_TRIPLET_COUNT = "rectangleTripletCount"; private static final String MAXIMUM_TRIANGLE_TRIPLETS = "maximumTriangleTriplets"; private static final String MAXIMUM_RECTANGLE_TRIPLETS = "maximumRectangleTriplets"; private EdgeMetricsHelper<K> edgeMetricsHelper; // Optional configuration private boolean reduceOnTargetId = false; private int parallelism = PARALLELISM_DEFAULT; /** * The degree can be counted from either the edge source or target IDs. * By default the source IDs are counted. Reducing on target IDs may * optimize the algorithm if the input edge list is sorted by target ID. * * @param reduceOnTargetId set to {@code true} if the input edge list * is sorted by target ID * @return this */ public EdgeMetrics<K, VV, EV> setReduceOnTargetId(boolean reduceOnTargetId) { this.reduceOnTargetId = reduceOnTargetId; return this; } /** * Override the operator parallelism. * * @param parallelism operator parallelism * @return this */ public EdgeMetrics<K, VV, EV> setParallelism(int parallelism) { this.parallelism = parallelism; return this; } /* * Implementation notes: * * Use aggregator to replace SumEdgeStats when aggregators are rewritten to use * a hash-combineable hashed-reduce. */ @Override public EdgeMetrics<K, VV, EV> run(Graph<K, VV, EV> input) throws Exception { super.run(input); // s, t, (d(s), d(t)) DataSet<Edge<K, Tuple3<EV, LongValue, LongValue>>> edgeDegreePair = input .run(new EdgeDegreePair<K, VV, EV>() .setReduceOnTargetId(reduceOnTargetId) .setParallelism(parallelism)); // s, d(s), count of (u, v) where deg(u) < deg(v) or (deg(u) == deg(v) and u < v) DataSet<Tuple3<K, LongValue, LongValue>> edgeStats = edgeDegreePair .map(new EdgeStats<K, EV>()) .setParallelism(parallelism) .name("Edge stats") .groupBy(0) .reduce(new SumEdgeStats<K>()) .setCombineHint(CombineHint.HASH) .setParallelism(parallelism) .name("Sum edge stats"); edgeMetricsHelper = new EdgeMetricsHelper<>(); edgeStats .output(edgeMetricsHelper) .setParallelism(parallelism) .name("Edge metrics"); return this; } @Override public Result getResult() { long triangleTripletCount = edgeMetricsHelper.getAccumulator(env, TRIANGLE_TRIPLET_COUNT); long rectangleTripletCount = edgeMetricsHelper.getAccumulator(env, RECTANGLE_TRIPLET_COUNT); long maximumTriangleTriplets = edgeMetricsHelper.getAccumulator(env, MAXIMUM_TRIANGLE_TRIPLETS); long maximumRectangleTriplets = edgeMetricsHelper.getAccumulator(env, MAXIMUM_RECTANGLE_TRIPLETS); return new Result(triangleTripletCount, rectangleTripletCount, maximumTriangleTriplets, maximumRectangleTriplets); } /** * Evaluates each edge and emits a tuple containing the source vertex ID, * the source vertex degree, and a value of zero or one indicating the * low-order count. The low-order count is one if the source vertex degree * is less than the target vertex degree or if the degrees are equal and * the source vertex ID compares lower than the target vertex ID; otherwise * the low-order count is zero. * * @param <T> ID type * @param <ET> edge value type */ @FunctionAnnotation.ForwardedFields("0; 2.1->1") private static class EdgeStats<T extends Comparable<T>, ET> implements MapFunction<Edge<T, Tuple3<ET, LongValue, LongValue>>, Tuple3<T, LongValue, LongValue>> { private LongValue zero = new LongValue(0); private LongValue one = new LongValue(1); private Tuple3<T, LongValue, LongValue> output = new Tuple3<>(); @Override public Tuple3<T, LongValue, LongValue> map(Edge<T, Tuple3<ET, LongValue, LongValue>> edge) throws Exception { Tuple3<ET, LongValue, LongValue> degrees = edge.f2; output.f0 = edge.f0; output.f1 = degrees.f1; long sourceDegree = degrees.f1.getValue(); long targetDegree = degrees.f2.getValue(); if (sourceDegree < targetDegree || (sourceDegree == targetDegree && edge.f0.compareTo(edge.f1) < 0)) { output.f2 = one; } else { output.f2 = zero; } return output; } } /** * Sums the low-order counts. * * @param <T> ID type */ private static class SumEdgeStats<T> implements ReduceFunction<Tuple3<T, LongValue, LongValue>> { @Override public Tuple3<T, LongValue, LongValue> reduce(Tuple3<T, LongValue, LongValue> value1, Tuple3<T, LongValue, LongValue> value2) throws Exception { value1.f2.setValue(value1.f2.getValue() + value2.f2.getValue()); return value1; } } /** * Helper class to collect edge metrics. * * @param <T> ID type */ private static class EdgeMetricsHelper<T extends Comparable<T>> extends AnalyticHelper<Tuple3<T, LongValue, LongValue>> { private long triangleTripletCount; private long rectangleTripletCount; private long maximumTriangleTriplets; private long maximumRectangleTriplets; @Override public void writeRecord(Tuple3<T, LongValue, LongValue> record) throws IOException { long degree = record.f1.getValue(); long lowDegree = record.f2.getValue(); long highDegree = degree - lowDegree; long triangleTriplets = lowDegree * (lowDegree - 1) / 2; long rectangleTriplets = triangleTriplets + lowDegree * highDegree; triangleTripletCount += triangleTriplets; rectangleTripletCount += rectangleTriplets; maximumTriangleTriplets = Math.max(maximumTriangleTriplets, triangleTriplets); maximumRectangleTriplets = Math.max(maximumRectangleTriplets, rectangleTriplets); } @Override public void close() throws IOException { addAccumulator(TRIANGLE_TRIPLET_COUNT, new LongCounter(triangleTripletCount)); addAccumulator(RECTANGLE_TRIPLET_COUNT, new LongCounter(rectangleTripletCount)); addAccumulator(MAXIMUM_TRIANGLE_TRIPLETS, new LongMaximum(maximumTriangleTriplets)); addAccumulator(MAXIMUM_RECTANGLE_TRIPLETS, new LongMaximum(maximumRectangleTriplets)); } } /** * Wraps edge metrics. */ public static class Result implements PrintableResult { private long triangleTripletCount; private long rectangleTripletCount; private long maximumTriangleTriplets; private long maximumRectangleTriplets; public Result(long triangleTripletCount, long rectangleTripletCount, long maximumTriangleTriplets, long maximumRectangleTriplets) { this.triangleTripletCount = triangleTripletCount; this.rectangleTripletCount = rectangleTripletCount; this.maximumTriangleTriplets = maximumTriangleTriplets; this.maximumRectangleTriplets = maximumRectangleTriplets; } /** * Get the number of triangle triplets. * * @return number of triangle triplets */ public long getNumberOfTriangleTriplets() { return triangleTripletCount; } /** * Get the number of rectangle triplets. * * @return number of rectangle triplets */ public long getNumberOfRectangleTriplets() { return rectangleTripletCount; } /** * Get the maximum triangle triplets. * * @return maximum triangle triplets */ public long getMaximumTriangleTriplets() { return maximumTriangleTriplets; } /** * Get the maximum rectangle triplets. * * @return maximum rectangle triplets */ public long getMaximumRectangleTriplets() { return maximumRectangleTriplets; } @Override public String toPrintableString() { NumberFormat nf = NumberFormat.getInstance(); return "triangle triplet count: " + nf.format(triangleTripletCount) + "; rectangle triplet count: " + nf.format(rectangleTripletCount) + "; maximum triangle triplets: " + nf.format(maximumTriangleTriplets) + "; maximum rectangle triplets: " + nf.format(maximumRectangleTriplets); } @Override public int hashCode() { return new HashCodeBuilder() .append(triangleTripletCount) .append(rectangleTripletCount) .append(maximumTriangleTriplets) .append(maximumRectangleTriplets) .hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (obj.getClass() != getClass()) { return false; } Result rhs = (Result)obj; return new EqualsBuilder() .append(triangleTripletCount, rhs.triangleTripletCount) .append(rectangleTripletCount, rhs.rectangleTripletCount) .append(maximumTriangleTriplets, rhs.maximumTriangleTriplets) .append(maximumRectangleTriplets, rhs.maximumRectangleTriplets) .isEquals(); } } }