All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.onosproject.common.DefaultTopology Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
/*
 * Copyright 2015-present Open Networking Foundation
 *
 * Licensed 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.onosproject.common;

import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSetMultimap.Builder;
import org.onlab.graph.DefaultEdgeWeigher;
import org.onlab.graph.DijkstraGraphSearch;
import org.onlab.graph.DisjointPathPair;
import org.onlab.graph.GraphPathSearch;
import org.onlab.graph.GraphPathSearch.Result;
import org.onlab.graph.KShortestPathsSearch;
import org.onlab.graph.LazyKShortestPathsSearch;
import org.onlab.graph.ScalarWeight;
import org.onlab.graph.SrlgGraphSearch;
import org.onlab.graph.SuurballeGraphSearch;
import org.onlab.graph.TarjanGraphSearch;
import org.onlab.graph.TarjanGraphSearch.SccResult;
import org.onlab.graph.Weight;
import org.onosproject.net.AbstractModel;
import org.onosproject.net.ConnectPoint;
import org.onosproject.net.DefaultDisjointPath;
import org.onosproject.net.DefaultPath;
import org.onosproject.net.DeviceId;
import org.onosproject.net.DisjointPath;
import org.onosproject.net.Link;
import org.onosproject.net.Link.Type;
import org.onosproject.net.Path;
import org.onosproject.net.provider.ProviderId;
import org.onosproject.net.topology.ClusterId;
import org.onosproject.net.topology.DefaultTopologyCluster;
import org.onosproject.net.topology.DefaultTopologyVertex;
import org.onosproject.net.topology.GraphDescription;
import org.onosproject.net.topology.HopCountLinkWeigher;
import org.onosproject.net.topology.LinkWeigher;
import org.onosproject.net.topology.Topology;
import org.onosproject.net.topology.TopologyCluster;
import org.onosproject.net.topology.TopologyEdge;
import org.onosproject.net.topology.TopologyGraph;
import org.onosproject.net.topology.TopologyVertex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static org.onlab.graph.GraphPathSearch.ALL_PATHS;
import static org.onlab.util.Tools.isNullOrEmpty;
import static org.onosproject.core.CoreService.CORE_PROVIDER_ID;
import static org.onosproject.net.Link.State.INACTIVE;
import static org.onosproject.net.Link.Type.INDIRECT;

/**
 * Default implementation of the topology descriptor. This carries the backing
 * topology data.
 */
public class DefaultTopology extends AbstractModel implements Topology {

    private static final Logger log = LoggerFactory.getLogger(DefaultTopology.class);

    private static final DijkstraGraphSearch DIJKSTRA =
            new DijkstraGraphSearch<>();
    private static final TarjanGraphSearch TARJAN =
            new TarjanGraphSearch<>();
    private static final SuurballeGraphSearch SUURBALLE =
            new SuurballeGraphSearch<>();
    private static final KShortestPathsSearch KSHORTEST =
            new KShortestPathsSearch<>();
    private static final LazyKShortestPathsSearch LAZY_KSHORTEST =
            new LazyKShortestPathsSearch<>();


    private static int defaultMaxPaths = ALL_PATHS;
    private static LinkWeigher defaultLinkWeigher = null;
    private static GraphPathSearch defaultGraphPathSearch = null;

    private final long time;
    private final long creationTime;
    private final long computeCost;
    private final TopologyGraph graph;

    private final LinkWeigher hopCountWeigher;

    private final Supplier> clusterResults;
    private final Supplier> clusters;
    private final Supplier> infrastructurePoints;
    private final Supplier> broadcastSets;
    private final Function broadcastFunction;
    private final Supplier clusterIndexes;

    /**
     * Sets the default maximum path count to be used when computing paths. If
     * -1 is specified, the builtin default ALL_PATHS, signifying
     * that all available paths should be returned is used.
     *
     * @param maxPaths new default max path count
     */
    public static synchronized void setDefaultMaxPaths(int maxPaths) {
        log.info("Setting new default maximum paths count to {}", maxPaths);
        defaultMaxPaths = maxPaths;
    }

    /**
     * Sets the default link-weight to be used when computing paths. If null is
     * specified, the builtin default link-weight measuring hop-counts will be
     * used.
     *
     * @param linkWeigher new default link-weight
     */
    public static synchronized void setDefaultLinkWeigher(LinkWeigher linkWeigher) {
        log.info("Setting new default link-weight function to {}", linkWeigher);
        defaultLinkWeigher = linkWeigher;
    }

    /**
     * Sets the default lpath search algorighm to be used when computing paths.
     * If null is specified, the builtin default Dijkstra will be used.
     *
     * @param graphPathSearch new default algorithm
     */
    public static synchronized void setDefaultGraphPathSearch(
            GraphPathSearch graphPathSearch) {
        log.info("Setting new default graph path algorithm to {}", graphPathSearch);
        defaultGraphPathSearch = graphPathSearch;
    }


    /**
     * Creates a topology descriptor attributed to the specified provider.
     *
     * @param providerId        identity of the provider
     * @param description       data describing the new topology
     * @param broadcastFunction broadcast point function
     */
    public DefaultTopology(ProviderId providerId, GraphDescription description,
                           Function broadcastFunction) {
        super(providerId);
        this.broadcastFunction = broadcastFunction;
        this.time = description.timestamp();
        this.creationTime = description.creationTime();

        // Build the graph
        this.graph = new DefaultTopologyGraph(description.vertexes(),
                                              description.edges());

        this.clusterResults = Suppliers.memoize(this::searchForClusters);
        this.clusters = Suppliers.memoize(this::buildTopologyClusters);

        this.clusterIndexes = Suppliers.memoize(this::buildIndexes);

        this.hopCountWeigher = new HopCountLinkWeigher(graph.getVertexes().size());
        this.broadcastSets = Suppliers.memoize(this::buildBroadcastSets);
        this.infrastructurePoints = Suppliers.memoize(this::findInfrastructurePoints);
        this.computeCost = Math.max(0, System.nanoTime() - time);
    }

    /**
     * Creates a topology descriptor attributed to the specified provider.
     *
     * @param providerId  identity of the provider
     * @param description data describing the new topology
     */
    public DefaultTopology(ProviderId providerId, GraphDescription description) {
        this(providerId, description, null);
    }

    @Override
    public long time() {
        return time;
    }

    @Override
    public long creationTime() {
        return creationTime;
    }

    @Override
    public long computeCost() {
        return computeCost;
    }

    @Override
    public int clusterCount() {
        return clusters.get().size();
    }

    @Override
    public int deviceCount() {
        return graph.getVertexes().size();
    }

    @Override
    public int linkCount() {
        return graph.getEdges().size();
    }

    private ImmutableMap clustersByDevice() {
        return clusterIndexes.get().clustersByDevice;
    }

    private ImmutableSetMultimap devicesByCluster() {
        return clusterIndexes.get().devicesByCluster;
    }

    private ImmutableSetMultimap linksByCluster() {
        return clusterIndexes.get().linksByCluster;
    }

    /**
     * Returns the backing topology graph.
     *
     * @return topology graph
     */
    public TopologyGraph getGraph() {
        return graph;
    }

    /**
     * Returns the set of topology clusters.
     *
     * @return set of clusters
     */
    public Set getClusters() {
        return ImmutableSet.copyOf(clusters.get().values());
    }

    /**
     * Returns the specified topology cluster.
     *
     * @param clusterId cluster identifier
     * @return topology cluster
     */
    public TopologyCluster getCluster(ClusterId clusterId) {
        return clusters.get().get(clusterId);
    }

    /**
     * Returns the topology cluster that contains the given device.
     *
     * @param deviceId device identifier
     * @return topology cluster
     */
    public TopologyCluster getCluster(DeviceId deviceId) {
        return clustersByDevice().get(deviceId);
    }

    /**
     * Returns the set of cluster devices.
     *
     * @param cluster topology cluster
     * @return cluster devices
     */
    public Set getClusterDevices(TopologyCluster cluster) {
        return devicesByCluster().get(cluster);
    }

    /**
     * Returns the set of cluster links.
     *
     * @param cluster topology cluster
     * @return cluster links
     */
    public Set getClusterLinks(TopologyCluster cluster) {
        return linksByCluster().get(cluster);
    }

    /**
     * Indicates whether the given point is an infrastructure link end-point.
     *
     * @param connectPoint connection point
     * @return true if infrastructure
     */
    public boolean isInfrastructure(ConnectPoint connectPoint) {
        return infrastructurePoints.get().contains(connectPoint);
    }

    /**
     * Indicates whether the given point is part of a broadcast set.
     *
     * @param connectPoint connection point
     * @return true if in broadcast set
     */
    public boolean isBroadcastPoint(ConnectPoint connectPoint) {
        if (broadcastFunction != null) {
            return broadcastFunction.apply(connectPoint);
        }

        // Any non-infrastructure, i.e. edge points are assumed to be OK.
        if (!isInfrastructure(connectPoint)) {
            return true;
        }

        // Find the cluster to which the device belongs.
        TopologyCluster cluster = clustersByDevice().get(connectPoint.deviceId());
        checkArgument(cluster != null,
                      "No cluster found for device %s", connectPoint.deviceId());

        // If the broadcast set is null or empty, or if the point explicitly
        // belongs to it, return true.
        Set points = broadcastSets.get().get(cluster.id());
        return isNullOrEmpty(points) || points.contains(connectPoint);
    }

    /**
     * Returns the size of the cluster broadcast set.
     *
     * @param clusterId cluster identifier
     * @return size of the cluster broadcast set
     */
    public int broadcastSetSize(ClusterId clusterId) {
        return broadcastSets.get().get(clusterId).size();
    }

    /**
     * Returns the set of the cluster broadcast points.
     *
     * @param clusterId cluster identifier
     * @return set of cluster broadcast points
     */
    public Set broadcastPoints(ClusterId clusterId) {
        return broadcastSets.get().get(clusterId);
    }

    /**
     * Returns the set of pre-computed shortest paths between source and
     * destination devices.
     *
     * @param src source device
     * @param dst destination device
     * @return set of shortest paths
     */
    public Set getPaths(DeviceId src, DeviceId dst) {
        return getPaths(src, dst, linkWeight(), defaultMaxPaths);
    }

    /**
     * Computes on-demand the set of shortest paths between source and
     * destination devices.
     *
     * @param src     source device
     * @param dst     destination device
     * @param weigher link weight function
     * @return set of shortest paths
     */
    public Set getPaths(DeviceId src, DeviceId dst, LinkWeigher weigher) {
        return getPaths(src, dst, weigher, defaultMaxPaths);
    }

    /**
     * Computes on-demand the set of shortest paths between source and
     * destination devices, the set of returned paths will be no more than,
     * maxPaths in size.  The first {@code maxPaths} paths will be returned
     * maintaining any ordering guarantees provided by the underlying
     * (default or if no default is specified {@link DijkstraGraphSearch})
     * search. If returning all paths of a given length would exceed
     * {@code maxPaths} a subset of paths of that length will be returned,
     * which paths will be returned depends on the currently specified
     * {@code GraphPathSearch}. See {@link #setDefaultGraphPathSearch}.
     *
     * @param src      source device
     * @param dst      destination device
     * @param weigher  link weight function
     * @param maxPaths maximum number of paths
     * @return set of shortest paths
     */
    public Set getPaths(DeviceId src, DeviceId dst, LinkWeigher weigher,
                              int maxPaths) {
        DefaultTopologyVertex srcV = new DefaultTopologyVertex(src);
        DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst);
        Set vertices = graph.getVertexes();
        if (!vertices.contains(srcV) || !vertices.contains(dstV)) {
            // src or dst not part of the current graph
            return ImmutableSet.of();
        }

        GraphPathSearch.Result result =
                graphPathSearch().search(graph, srcV, dstV, weigher, maxPaths);
        ImmutableSet.Builder builder = ImmutableSet.builder();
        for (org.onlab.graph.Path path : result.paths()) {
            builder.add(networkPath(path));
        }
        return builder.build();
    }

    /**
     * Computes on-demand the k-shortest paths between source and
     * destination devices.
     *
     * @param src      source device
     * @param dst      destination device
     * @param maxPaths maximum number of paths (k)
     * @return set of k-shortest paths
     */
    public Set getKShortestPaths(DeviceId src, DeviceId dst,
                                       int maxPaths) {

        return getKShortestPaths(src, dst, linkWeight(), maxPaths);
    }

    /**
     * Computes on-demand the k-shortest paths between source and
     * destination devices.
     * 

* The first {@code maxPaths} paths will be returned * in ascending order according to the provided {@code weigher} * * @param src source device * @param dst destination device * @param weigher link weight function * @param maxPaths maximum number of paths (k) * @return set of k-shortest paths */ public Set getKShortestPaths(DeviceId src, DeviceId dst, LinkWeigher weigher, int maxPaths) { DefaultTopologyVertex srcV = new DefaultTopologyVertex(src); DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst); Set vertices = graph.getVertexes(); if (!vertices.contains(srcV) || !vertices.contains(dstV)) { // src or dst not part of the current graph return ImmutableSet.of(); } return KSHORTEST.search(graph, srcV, dstV, weigher, maxPaths) .paths().stream() .map(this::networkPath) .collect(ImmutableSet.toImmutableSet()); } /** * Lazily computes on-demand the k-shortest paths between source and * destination devices. * * @param src source device * @param dst destination device * @return stream of k-shortest paths */ public Stream getKShortestPaths(DeviceId src, DeviceId dst) { return getKShortestPaths(src, dst, linkWeight()); } /** * Lazily computes on-demand the k-shortest paths between source and * destination devices. * * @param src source device * @param dst destination device * @param weigher link weight function * @return stream of k-shortest paths */ public Stream getKShortestPaths(DeviceId src, DeviceId dst, LinkWeigher weigher) { DefaultTopologyVertex srcV = new DefaultTopologyVertex(src); DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst); Set vertices = graph.getVertexes(); if (!vertices.contains(srcV) || !vertices.contains(dstV)) { // src or dst not part of the current graph return Stream.empty(); } return LAZY_KSHORTEST.lazyPathSearch(graph, srcV, dstV, weigher) .map(this::networkPath); } /** * Returns the set of pre-computed shortest disjoint path pairs between * source and destination devices. * * @param src source device * @param dst destination device * @return set of shortest disjoint path pairs */ public Set getDisjointPaths(DeviceId src, DeviceId dst) { return getDisjointPaths(src, dst, linkWeight()); } /** * Computes on-demand the set of shortest disjoint path pairs between * source and destination devices. * * @param src source device * @param dst destination device * @param weigher link weight function * @return set of disjoint shortest path pairs */ public Set getDisjointPaths(DeviceId src, DeviceId dst, LinkWeigher weigher) { DefaultTopologyVertex srcV = new DefaultTopologyVertex(src); DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst); Set vertices = graph.getVertexes(); if (!vertices.contains(srcV) || !vertices.contains(dstV)) { // src or dst not part of the current graph return ImmutableSet.of(); } GraphPathSearch.Result result = SUURBALLE.search(graph, srcV, dstV, weigher, defaultMaxPaths); ImmutableSet.Builder builder = ImmutableSet.builder(); for (org.onlab.graph.Path path : result.paths()) { DisjointPath disjointPath = networkDisjointPath((DisjointPathPair) path); if (disjointPath.backup() != null) { builder.add(disjointPath); } } return builder.build(); } /** * Computes on-demand the set of shortest disjoint risk groups path pairs * between source and destination devices. * * @param src source device * @param dst destination device * @param weigher edge weight object * @param riskProfile map representing risk groups for each edge * @return set of shortest disjoint paths */ private Set disjointPaths(DeviceId src, DeviceId dst, LinkWeigher weigher, Map riskProfile) { DefaultTopologyVertex srcV = new DefaultTopologyVertex(src); DefaultTopologyVertex dstV = new DefaultTopologyVertex(dst); Set vertices = graph.getVertexes(); if (!vertices.contains(srcV) || !vertices.contains(dstV)) { // src or dst not part of the current graph return ImmutableSet.of(); } SrlgGraphSearch srlg = new SrlgGraphSearch<>(riskProfile); GraphPathSearch.Result result = srlg.search(graph, srcV, dstV, weigher, defaultMaxPaths); ImmutableSet.Builder builder = ImmutableSet.builder(); for (org.onlab.graph.Path path : result.paths()) { DisjointPath disjointPath = networkDisjointPath((DisjointPathPair) path); if (disjointPath.backup() != null) { builder.add(disjointPath); } } return builder.build(); } /** * Computes on-demand the set of shortest disjoint risk groups path pairs * between source and destination devices. * * @param src source device * @param dst destination device * @param weigher edge weight object * @param riskProfile map representing risk groups for each link * @return set of shortest disjoint paths */ public Set getDisjointPaths(DeviceId src, DeviceId dst, LinkWeigher weigher, Map riskProfile) { Map riskProfile2 = new HashMap<>(); for (Link l : riskProfile.keySet()) { riskProfile2.put(new TopologyEdge() { Link cur = l; @Override public Link link() { return cur; } @Override public TopologyVertex src() { return () -> src; } @Override public TopologyVertex dst() { return () -> dst; } }, riskProfile.get(l)); } return disjointPaths(src, dst, weigher, riskProfile2); } /** * Computes on-demand the set of shortest disjoint risk groups path pairs * between source and destination devices. * * @param src source device * @param dst destination device * @param riskProfile map representing risk groups for each link * @return set of shortest disjoint paths */ public Set getDisjointPaths(DeviceId src, DeviceId dst, Map riskProfile) { return getDisjointPaths(src, dst, linkWeight(), riskProfile); } // Converts graph path to a network path with the same cost. private Path networkPath(org.onlab.graph.Path path) { List links = path.edges().stream().map(TopologyEdge::link) .collect(Collectors.toList()); return new DefaultPath(CORE_PROVIDER_ID, links, path.cost()); } private DisjointPath networkDisjointPath( DisjointPathPair path) { if (!path.hasBackup()) { // There was no secondary path available. return new DefaultDisjointPath(CORE_PROVIDER_ID, (DefaultPath) networkPath(path.primary()), null); } return new DefaultDisjointPath(CORE_PROVIDER_ID, (DefaultPath) networkPath(path.primary()), (DefaultPath) networkPath(path.secondary())); } // Searches for SCC clusters in the network topology graph using Tarjan // algorithm. private SccResult searchForClusters() { return TARJAN.search(graph, new NoIndirectLinksWeigher()); } // Builds the topology clusters and returns the id-cluster bindings. private ImmutableMap buildTopologyClusters() { ImmutableMap.Builder clusterBuilder = ImmutableMap.builder(); SccResult results = clusterResults.get(); // Extract both vertexes and edges from the results; the lists form // pairs along the same index. List> clusterVertexes = results.clusterVertexes(); List> clusterEdges = results.clusterEdges(); // Scan over the lists and create a cluster from the results. for (int i = 0, n = results.clusterCount(); i < n; i++) { Set vertexSet = clusterVertexes.get(i); Set edgeSet = clusterEdges.get(i); ClusterId cid = ClusterId.clusterId(i); DefaultTopologyCluster cluster = new DefaultTopologyCluster(cid, vertexSet.size(), edgeSet.size(), findRoot(vertexSet)); clusterBuilder.put(cid, cluster); } return clusterBuilder.build(); } // Finds the vertex whose device id is the lexicographical minimum in the // specified set. private TopologyVertex findRoot(Set vertexSet) { TopologyVertex minVertex = null; for (TopologyVertex vertex : vertexSet) { if ((minVertex == null) || (vertex.deviceId() .toString().compareTo(minVertex.deviceId().toString()) < 0)) { minVertex = vertex; } } return minVertex; } // Processes a map of broadcast sets for each cluster. private ImmutableSetMultimap buildBroadcastSets() { Builder builder = ImmutableSetMultimap.builder(); for (TopologyCluster cluster : clusters.get().values()) { addClusterBroadcastSet(cluster, builder); } return builder.build(); } // Finds all broadcast points for the cluster. These are those connection // points which lie along the shortest paths between the cluster root and // all other devices within the cluster. private void addClusterBroadcastSet(TopologyCluster cluster, Builder builder) { // Use the graph root search results to build the broadcast set. Result result = DIJKSTRA.search(graph, cluster.root(), null, hopCountWeigher, 1); for (Map.Entry> entry : result.parents().entrySet()) { TopologyVertex vertex = entry.getKey(); // Ignore any parents that lead outside the cluster. if (clustersByDevice().get(vertex.deviceId()) != cluster) { continue; } // Ignore any back-link sets that are empty. Set parents = entry.getValue(); if (parents.isEmpty()) { continue; } // Use the first back-link source and destinations to add to the // broadcast set. Link link = parents.iterator().next().link(); builder.put(cluster.id(), link.src()); builder.put(cluster.id(), link.dst()); } } // Collects and returns an set of all infrastructure link end-points. private ImmutableSet findInfrastructurePoints() { ImmutableSet.Builder builder = ImmutableSet.builder(); for (TopologyEdge edge : graph.getEdges()) { if (edge.link().type() == Type.EDGE) { // exclude EDGE link from infrastructure link // - Device <-> Host // - Device <-> remote domain Device continue; } builder.add(edge.link().src()); builder.add(edge.link().dst()); } return builder.build(); } // Builds cluster-devices, cluster-links and device-cluster indexes. private ClusterIndexes buildIndexes() { // Prepare the index builders ImmutableMap.Builder clusterBuilder = ImmutableMap.builder(); ImmutableSetMultimap.Builder devicesBuilder = ImmutableSetMultimap.builder(); ImmutableSetMultimap.Builder linksBuilder = ImmutableSetMultimap.builder(); // Now scan through all the clusters for (TopologyCluster cluster : clusters.get().values()) { int i = cluster.id().index(); // Scan through all the cluster vertexes. for (TopologyVertex vertex : clusterResults.get().clusterVertexes().get(i)) { devicesBuilder.put(cluster, vertex.deviceId()); clusterBuilder.put(vertex.deviceId(), cluster); } // Scan through all the cluster edges. for (TopologyEdge edge : clusterResults.get().clusterEdges().get(i)) { linksBuilder.put(cluster, edge.link()); } } // Finalize all indexes. return new ClusterIndexes(clusterBuilder.build(), devicesBuilder.build(), linksBuilder.build()); } private GraphPathSearch graphPathSearch() { return defaultGraphPathSearch != null ? defaultGraphPathSearch : DIJKSTRA; } private LinkWeigher linkWeight() { return defaultLinkWeigher != null ? defaultLinkWeigher : hopCountWeigher; } // Link weight for preventing traversal over indirect links. private static class NoIndirectLinksWeigher extends DefaultEdgeWeigher implements LinkWeigher { @Override public Weight weight(TopologyEdge edge) { return (edge.link().state() == INACTIVE) || (edge.link().type() == INDIRECT) ? getNonViableWeight() : new ScalarWeight(HOP_WEIGHT_VALUE); } } static final class ClusterIndexes { final ImmutableMap clustersByDevice; final ImmutableSetMultimap devicesByCluster; final ImmutableSetMultimap linksByCluster; public ClusterIndexes(ImmutableMap clustersByDevice, ImmutableSetMultimap devicesByCluster, ImmutableSetMultimap linksByCluster) { this.clustersByDevice = clustersByDevice; this.devicesByCluster = devicesByCluster; this.linksByCluster = linksByCluster; } } @Override public String toString() { return toStringHelper(this) .add("time", time) .add("creationTime", creationTime) .add("computeCost", computeCost) .add("clusters", clusterCount()) .add("devices", deviceCount()) .add("links", linkCount()).toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy