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

scalax.collection.TraverserImpl.scala Maven / Gradle / Ivy

The newest version!
package scalax.collection

import scala.language.higherKinds
import scala.annotation.{switch, tailrec}
import scala.collection.generic.{FilterMonadic, Growable}
import scala.collection.mutable.{ArrayBuffer, ArrayStack => Stack, Queue, PriorityQueue, Map => MMap}
import scala.math.abs

import scala.collection.FilterableSet
import scala.collection.FilteredSet
import scalax.collection.GraphPredef.{OuterNode, EdgeLikeIn}
import immutable.SortedArraySet
import mutable.{ArraySet, EqHashSet}
import scalax.collection.GraphEdge.DiEdge

/** Default implementation of the graph algorithms to maintain the functionality
 *  defined by [[GraphTraversal]].
 *   
 *  @author Peter Empen
 */
trait TraverserImpl[N, E[X] <: EdgeLikeIn[X]] {
  thisGraph: GraphTraversalImpl[N,E] =>

  import GraphTraversalImpl._
  import GraphTraversal._
  import Visitor._
  import State._
    
  protected[collection] trait Impl[A, +This <: Traverser[A,This] with Impl[A,This]]
      extends Traverser[A,This] {
    thisImpl: This =>

    final protected def apply[U](pred: NodeFilter = noNode, visitor: A => U = empty): Option[NodeT] =
      new Runner[U](pred, visitor)()

    final def findCycle[U](implicit visitor: A => U = empty): Option[Cycle] = {
      cycle(
        withParameters(Parameters(direction = Successors)).Runner(noNode, visitor).dfsWGB(),
        subgraphEdges
      )
    }
    
    final def pathUntil[U](pred: NodeFilter)(implicit visitor: A => U = empty): Option[Path] = {
      val (target, path) = 
          withParameters(parameters.withDirection(Successors)).Runner[U](pred, visitor).dfsStack()
      target map { _ =>
        new AnyEdgeLazyPath(
          new ReverseStackTraversable[DfsInformer.Element[NodeT], NodeT]
              (path, (e: DfsInformer.Element[NodeT]) => e.node),
          subgraphEdges)
      }
    }
    
    final def topologicalSort[U](ignorePredecessors: Boolean = false)
                                (implicit visitor: InnerElem => U = empty): CycleNodeOrTopologicalOrder = {
      val predecessors: MSet[NodeT] =
          if (ignorePredecessors)
            innerNodeTraverser(root, Parameters.Dfs(Predecessors)).to[MSet] -= root
          else MSet.empty
      def ignore(n: NodeT): Boolean = if (ignorePredecessors) predecessors contains n else false
      val (startNodes, inDegrees) =
        forInDegrees(
          innerNodeTraverser(root, Parameters.Dfs(AnyConnected),
                             n => subgraphNodes(n), subgraphEdges),
          includeInDegree = if (ignorePredecessors) ! ignore(_) else anyNode,
          includeAnyway = if (ignorePredecessors) Some(root) else None
        )
      Runner(noNode, empty).topologicalSort(startNodes -- predecessors, inDegrees)
    }

    final def shortestPathTo[T:Numeric, U](potentialSuccessor: NodeT,
                                           weight            : EdgeT => T,
                                           visitor           : A => U): Option[Path] =
      new Runner(noNode, visitor).shortestPathTo(potentialSuccessor, weight)

    /** Contains algorithms and local values to be used by the algorithms.
     *  Last target reusability and best possible run-time performance.
     *
     *  @param stopAt node predicate marking an end condition for the search
     */
    protected final class Runner[U](stopAt: NodeFilter, visitor: A => U) {

      private[this] val addMethod = parameters.direction match {
        case Successors   => Node.addDiSuccessors _
        case Predecessors => Node.addDiPredecessors _
        case AnyConnected => Node.addNeighbors _
      }
  
      private[this] val doNodeFilter = isCustomNodeFilter(subgraphNodes)
      private[this] val doEdgeFilter = isCustomEdgeFilter(subgraphEdges) || maxWeight.isDefined
      private[this] val (doNodeSort, nodeOrdering, reverseNodeOrdering,
                           doEdgeSort, edgeOrdering, reverseEdgeOrdering) =
        ordering match {
          case nO: NodeOrdering    => (true,  nO,   nO.reverse,
                                       false, null, null)
          case eO: EdgeOrdering    => (false, null, null,
                                       true,  eO,   eO.reverse)
          case _ : NoOrdering.type => (false, null, null,
                                       false, null, null)
        }
  
      /* doNodeVisitor:  whether any node visitor is to be called
       * nodeVisitor:    the simple node visitor or empty
       * extNodeVisitor: the extended node visitor or null
       * edgeVisitor:    the edge visitor or empty
       */
      private[this] val (doNodeVisitor, nodeVisitor, extNodeVisitor, edgeVisitor):
                        (Boolean,       NodeT => U,  ExtendedNodeVisitor[U], EdgeT => U) = {
        val nodeVisitor = thisImpl.nodeVisitor(visitor)
        val extNodeVisitor = visitor match {
            case ext: ExtendedNodeVisitor[U] => ext
            case _ => null
        }
        ( isDefined(nodeVisitor),
          if (extNodeVisitor eq null) nodeVisitor else empty[NodeT,U],
          extNodeVisitor,
          thisImpl.edgeVisitor(visitor)
        )
      }
      
      private[this] val filteredNodes = parameters.direction match {
        case Successors   => filteredSuccessors _
        case Predecessors => filteredPredecessors _
        case AnyConnected => filteredNeighbors _
      }

      private[this] def directionEdges(n: NodeT): NodeT => Set[EdgeT] with FilterableSet[EdgeT] = parameters.direction match {
        case Successors   => n outgoingTo _
        case Predecessors => n incomingFrom _
        case AnyConnected => n connectionsWith _
      }
      
      @inline protected[Impl] def apply(): Option[NodeT] =
        if (parameters.kind.isBsf) bfs() else dfs()
      
      @inline private[this] def estimatedNrOfNodes(node: NodeT) = {
        val max = node.edges.size
        if (thisGraph.isHyper) max * 4 else max
      }
      
      def maxDepth = if (parameters.maxDepth > 0) parameters.maxDepth
                     else java.lang.Integer.MAX_VALUE
      
      private[this] def sorted[A <: InnerElem with B, B <: InnerElem: reflect.ClassTag](
          set:      FilterMonadic[A,AnySet[A]],
          maxOrEst: Int, // maximum size of set or negative for an estimate
          ordering: Ordering[A]): AnySet[A] =
        set match {
          case a: ArraySet[A] => a.sorted(ordering)
          case t =>
            @inline def newArray(len: Int): Array[A] = new Array[B](len).asInstanceOf[Array[A]]
            val size = abs(maxOrEst)
            var cnt = 0
            val arr =
              if (maxOrEst >= 0) {
                val arr = newArray(maxOrEst)
                t foreach { a => arr(cnt) = a; cnt += 1 }
                if (maxOrEst > cnt) {
                  val shrinked = newArray(cnt)
                  Array.copy(arr, 0, shrinked, 0, cnt)
                  shrinked
                } else arr
              } else {
                val buf = new ArrayBuffer[A](maxOrEst)
                t foreach { a => buf += a; cnt += 1 }
                val arr = newArray(cnt)
                buf copyToArray arr
                arr
              }
            new SortedArraySet[A](arr)(ordering)
        }
  
      @inline private[this] def sortedNodes(nodes: AnySet[NodeT], maxOrEst: Int, reverse: Boolean): AnySet[NodeT] =
        sorted[NodeT,InnerElem](
            nodes, maxOrEst, if (reverse) reverseNodeOrdering else nodeOrdering)
      
      private[this] def filtered(
          node:      NodeT,
          isVisited: NodeFilter,
          _edges:    FilterMonadic[EdgeT,AnySet[EdgeT]], // already filtered if adequate
          reverse:   Boolean): Traversable[NodeT] = {
        
        val edges = {
          if(doEdgeSort) {
            val maxEdges = node.edges.size
            if(reverse) sorted[EdgeT,InnerElem](_edges, maxEdges, reverseEdgeOrdering)
            else        sorted[EdgeT,InnerElem](_edges, maxEdges, edgeOrdering)
          } else _edges
        }

        val filter = chooseFilter(isVisited)
        val doEdgeVisitor = isDefined(edgeVisitor)
        val estimatedNodes = estimatedNrOfNodes(node)
        def withEdges(withNode: NodeT => Unit) =
          edges foreach { e =>
            addMethod(node, e, withNode)
            if (doEdgeVisitor) edgeVisitor(e)
          }
        if (doEdgeSort) {
          /* The node set to be returned must reflect edge ordering.
           * doEdgeSort and doNodeSort are mutually exclusive.
           */
          val set = new EqHashSet[NodeT](estimatedNodes)
          val succ = new ArrayBuffer[NodeT](estimatedNodes)
          withEdges( n =>
            if (filter(n) && set.add(n)) succ += n
          )
          succ
        } else {
          val succ = new EqHashSet[NodeT](estimatedNodes)
          withEdges( n =>
            if (filter(n)) succ += n
          )
          if(doNodeSort) sortedNodes(succ, succ.size, reverse)
          else succ
        }
      }
      
      private[this] val withEdgeFiltering: Boolean =
        doEdgeFilter || doEdgeSort || isDefined(edgeVisitor) || maxWeight.isDefined
      
      @inline private[this] def chooseFilter(isVisited: NodeFilter): NodeFilter =
        if (doNodeFilter) (n: NodeT) => ! isVisited(n) && subgraphNodes(n)
        else              (n: NodeT) => ! isVisited(n)
        
      private[this] def filtered(
          nodes:     AnySet[NodeT],
          maxNodes:  Int,                             
          isVisited: NodeFilter,
          reverse:   Boolean): AnySet[NodeT] = {
        val filtered = new FilteredSet(nodes, chooseFilter(isVisited))
        if(doNodeSort) sortedNodes(filtered, maxNodes, reverse)
        else filtered
      }

      private[this] def edgeFilter(cumWeight: Double): EdgeFilter = maxWeight.fold(
        ifEmpty = subgraphEdges)(
        w => {
          def weightFilter: EdgeFilter = e => cumWeight + w.edgeWeight(e) <= w.value
          if (isCustomEdgeFilter(subgraphEdges)) e => subgraphEdges(e) && weightFilter(e)
          else weightFilter
        })
        
      private[this] def minWeight(n: NodeT, neighbor: NodeT, cumWeight: Double): Double = maxWeight.fold[Double](
        ifEmpty = throw new MatchError("maxWeight is expected to be defined."))(
        w => w.edgeWeight(directionEdges(n)(neighbor) withFilter edgeFilter(cumWeight) min w.ordering)
      )

      final private[this] def filteredEdges(edges: AnySet[EdgeT], cumWeight: Double): FilterMonadic[EdgeT,AnySet[EdgeT]] =
        if (doEdgeFilter) edges withFilter edgeFilter(cumWeight)
        else edges
        
      private[this] def filteredSuccessors(
          node:        NodeT,
          isVisited:   NodeFilter,
          cumWeight:   Double,
          reverse:     Boolean): Traversable[NodeT] = {
  
        if (withEdgeFiltering)
          filtered(node, isVisited, filteredEdges(node.outgoing, cumWeight), reverse)
        else {
          val succ = node.diSuccessors
          filtered(succ, succ.size, isVisited, reverse)
        }
      }
        
      private[this] def filteredPredecessors(
          node:        NodeT,
          isVisited:   NodeFilter,
          cumWeight:   Double,
          reverse:     Boolean): Traversable[NodeT] = {
        
        if (withEdgeFiltering)
          filtered(node, isVisited, filteredEdges(node.incoming, cumWeight), reverse)
        else
          filtered(node.diPredecessors, - estimatedNrOfNodes(node), isVisited, reverse)
      }
        
      private[this] def filteredNeighbors(
          node:        NodeT,
          isVisited:   NodeFilter,
          cumWeight:   Double,
          reverse:     Boolean): Traversable[NodeT] = {
            
        if (withEdgeFiltering)
          filtered(node, isVisited, filteredEdges(node.edges, cumWeight), reverse)
        else
          filtered(node.neighbors, - estimatedNrOfNodes(node), isVisited, reverse)
      }
  
      protected[collection] def shortestPathTo[T:Numeric](
          potentialSuccessor: NodeT,
          weight            : EdgeT => T): Option[Path] = {
        withHandle() { implicit visitedHandle => 
          val num = implicitly[Numeric[T]] 
          import num._
          @inline def visited(n: NodeT) = n.visited
  
          type NodeWeight = (NodeT,T)
          val weightOrdering = Edge.weightOrdering(weight)
          val dest      = MMap[NodeT,T](root -> zero)
          val mapToPred = MMap[NodeT,NodeT]()
          // not implicit due to issues #4405 and #4407
          object ordNodeWeight extends Ordering[NodeWeight] {
            def compare(x: NodeWeight,
                        y: NodeWeight) = num.compare(y._2, x._2)
          }
          val qNodes = new PriorityQueue[NodeWeight]()(ordNodeWeight) += ((root -> zero))
  
          def sortedAdjacentNodes(node: NodeT): PriorityQueue[NodeWeight] =
            filteredSuccessors(node, visited, Double.NaN, false).foldLeft(
                new PriorityQueue[NodeWeight]()(ordNodeWeight))(
                (q,n) => q += ((n, dest(node) +
                                   weight(node.outgoingTo(n).withFilter(subgraphEdges(_)).
                                          min(weightOrdering))
                               ))
                )
          def relax(pred: NodeT, succ: NodeT) {
            val cost = dest(pred) + weight(pred.outgoingTo(succ).withFilter(subgraphEdges(_)).
                                           min(weightOrdering))
            if(! dest.isDefinedAt(succ) || cost < dest(succ)) {
              dest      += (succ->cost)
              mapToPred += (succ->pred)
            }
          }
          var nodeCnt = 0
          @tailrec def rec(pq: PriorityQueue[NodeWeight]) {
            if(pq.nonEmpty && (pq.head._1 ne potentialSuccessor)) { 
              val nodeWeight = pq.dequeue
              val node = nodeWeight._1
              if (! node.visited) {
                val ordNodes = sortedAdjacentNodes(node)
                pq ++= ordNodes
                @tailrec def loop(pq2: PriorityQueue[NodeWeight]) {
                  if (pq2.nonEmpty) {
                    relax(node, pq2.dequeue._1)
                    loop(pq2)
                  }
                }
                loop(ordNodes)
                
                node.visited = true
                if (doNodeVisitor)
                  if (isDefined(nodeVisitor)) nodeVisitor(node)
                  else {
                    nodeCnt += 1
                    extNodeVisitor(node, nodeCnt, 0,
                      new DijkstraInformer[NodeT,T] {
                        def queueIterator = qNodes.iterator
                        def costsIterator = dest.iterator
                      }
                    )
                  }
              }
              rec(pq)
            }
          }
          rec(qNodes) 
          def traverseMapNodes(map: MMap[NodeT,NodeT]): Option[Path] = {
            map.get(potentialSuccessor).map ( _ =>
              new MinWeightEdgeLazyPath(
                  new MapPathTraversable[NodeT](map, potentialSuccessor, root),
                  subgraphEdges,
                  Edge.weightOrdering(weight))
            ) orElse (
              if(root eq potentialSuccessor) Some(Path.zero(potentialSuccessor)) else None
            )
          }
          traverseMapNodes(mapToPred)
        }
      }
                        
      protected[collection] def bfs(maybeHandle: Option[Handle] = None): Option[NodeT] = {
        withHandle(maybeHandle) { implicit visitedHandle =>
          val untilDepth: Int = maxDepth
          var depth = 0
          var nodeCnt = 0
          import BfsInformer.Element
          val q = Queue[Element[NodeT]](Element(root, depth))
          @inline def visited(n: NodeT) = n.visited  
          def visit(n: NodeT): Unit = {
            n.visited = true
            if (doNodeVisitor)
              if (isDefined(nodeVisitor)) nodeVisitor(n)
              else {
                nodeCnt += 1
                extNodeVisitor(n, nodeCnt, depth,
                  new BfsInformer[NodeT] {
                    def queueIterator = q.iterator 
                  }
                )
              }
          }
          visit(root)
          while (q.nonEmpty) { 
            val Element(prevNode, prevDepth, cumWeight) = q.dequeue
            if (prevDepth < untilDepth) {
              depth = prevDepth + 1
              for (n <- filteredNodes(prevNode, visited, cumWeight, false)) { 
                visit(n)
                if (stopAt(n)) return Some(n)
                q enqueue Element(
                  n,
                  depth,
                  maxWeight.fold(ifEmpty = 0d)(w => cumWeight + minWeight(prevNode, n, cumWeight))
                )  
              }
            }
          }
          None
        }
      }
  
      @inline protected[collection]
      def dfs[U](maybeHandle: Option[Handle] = None): Option[NodeT] = dfsStack(empty, maybeHandle)._1

      /** @return (node stopped at, stack of ...) */
      protected[collection]
      def dfsStack[U](nodeUpVisitor: (NodeT) => U = empty,
                      maybeHandle: Option[Handle] = None): (Option[NodeT], Stack[DfsInformer.Element[NodeT]]) =
        withHandle(maybeHandle) { implicit visitedHandle =>
          val untilDepth: Int = maxDepth
          val doNodeUpVisitor = isDefined(nodeUpVisitor)

          @inline def isVisited(n: NodeT): Boolean = n.visited
          import DfsInformer.Element
          val stack: Stack[Element[NodeT]] = Stack(Element(root, 0))
          val path:  Stack[Element[NodeT]] = Stack()
          var res: Option[NodeT] = None
          var nodeCnt = 0
          root.visited = true
          @tailrec def loop {
            if(stack.nonEmpty) {
              val Element(current, depth, cumWeight) = {
                val popped = stack.pop
                val depth = popped.depth
                if (depth > 0)
                  while (path.head.depth >= depth) {
                    if (doNodeUpVisitor) nodeUpVisitor(path.head.node)
                    path.pop
                  }
                path.push(popped)
                popped
              }
              if (doNodeVisitor)
                if (isDefined(nodeVisitor)) nodeVisitor(current)
                else {
                  nodeCnt += 1
                  extNodeVisitor(current, nodeCnt, depth,
                    new DfsInformer[NodeT] {
                      def stackIterator = stack.iterator
                      def pathIterator  = path .iterator
                    }
                  )
                }
              if (stopAt(current) && (current ne root)) {
                res = Some(current)
              } else {
                if (depth < untilDepth)
                  for (n <- filteredNodes(current, isVisited, cumWeight, true)) {
                    stack push Element(
                      n,
                      depth + 1,
                      maxWeight.fold(ifEmpty = 0d)(w => cumWeight + minWeight(current, n, cumWeight))
                    )
                    n.visited = true
                  }
                loop
              }
            }
          }
          loop
          if (doNodeUpVisitor) path foreach (e => nodeUpVisitor(e.node))
          (res, path)
        }
  
      /** Tail-recursive white-gray-black DFS implementation for cycle detection.
       */
      protected[collection]
      def dfsWGB(globalState: Array[Handle] = Array.empty[Handle]): (Option[NodeT], Stack[CycleStackElem]) = {
        withHandles(2, globalState) { handles =>
          implicit val visitedHandle = handles(0)
          val blackHandle = handles(1)
          
          import WgbInformer.Element
          val stack: Stack[Element[NodeT,EdgeT]] = Stack(Element(root, root, false, Nil))
          val path = Stack.empty[CycleStackElem] // (node, connecting with prev)
          val isDiGraph = thisGraph.isDirected
          def isWhite (node: NodeT) = nonVisited(node)
          def isGray  (node: NodeT) = isVisited(node) && ! (node bit(blackHandle))
          def isBlack (node: NodeT) = node bit(blackHandle)
          def setGray (node: NodeT) { node.visited = true }
          def setBlack(node: NodeT) { node.bit_=(true)(blackHandle) } 
    
          def onNodeDown(node: NodeT) { setGray (node) } 
          def onNodeUp  (node: NodeT) { setBlack(node) }
    
          def isVisited (node: NodeT) = node.visited
          def nonVisited(node: NodeT) = ! isVisited(node)
          var res: Option[NodeT] = None
          var nodeCnt = 0
          @tailrec def loop(pushed: Boolean) {
            if (res.isEmpty)
              if (stack.isEmpty)
                path foreach (t => setBlack(t._1))
              else {
                val Element(poppedNode, poppedPredecessor, poppedExclude, poppedMultiEdges, cumWeight) = stack.pop
                if (! pushed)
                  while ( path.nonEmpty &&
                         (path.head._1 ne root) &&
                         (path.head._1 ne poppedPredecessor)) {
                    val p = path.pop._1
                    if (! isBlack(p))
                      onNodeUp(p) 
                  }
                val exclude: Option[NodeT] = if (poppedExclude) Some(poppedPredecessor) else None
                val current = poppedNode
                path.push((current, poppedMultiEdges))
                if (nonVisited(current)) onNodeDown(current)
                if (doNodeVisitor)
                  if (isDefined(nodeVisitor)) nodeVisitor(current)
                  else {
                    nodeCnt += 1
                    extNodeVisitor(current, nodeCnt, 0,
                        new WgbInformer[NodeT, EdgeT] {
                          def stackIterator = stack.iterator 
                          def pathIterator  = path .iterator 
                      }
                    )
                  }
                if (stopAt(current) && (current ne root))
                  res = Some(current)
                else {
                  var pushed = false
                  for (n <- filteredNodes(current, isBlack(_), cumWeight, true)
                            withFilter (! isBlack(_))) { 
                    if (isGray(n)) {
                      if (exclude.fold(ifEmpty = true)(_ ne n))
                        res = Some(n)
                    } else {
                      val newCumWeight = maxWeight.fold(ifEmpty = 0d)(w => cumWeight + minWeight(current, n, cumWeight))
                      if (isDiGraph)
                        stack.push(Element(n, current, false, Nil, newCumWeight))
                      else {
                        val (excl, multi): (Boolean, Iterable[EdgeT]) = {
                          val conn = current connectionsWith n
                          (conn.size: @switch) match {
                            case 0 => throw new NoSuchElementException
                            case 1 => (true, conn)
                            case _ => (false, conn)
                          }
                        }
                        stack.push(Element(n, current, excl, multi, newCumWeight))
                      }
                      pushed = true
                    }
                  }
                  loop(pushed)
                }
              }
          }
          loop(true)
          (res, path)
        }
      }

      protected[collection] def topologicalSort(
          layer_0: Traversable[NodeT],
          inDegrees: MMap[NodeT,Int],
          maybeHandle: Option[Handle] = None): CycleNodeOrTopologicalOrder =
        withHandle(maybeHandle) { implicit handle =>
          val untilDepth: Int = maxDepth
          val estimatedLayers: Int = expectedMaxNodes(4)
          val estimatedNodesPerLayer: Int = order / estimatedLayers
          val layers = new ArrayBuffer[Layer](estimatedLayers)
          def emptyBuffer: ArrayBuffer[NodeT] = new ArrayBuffer[NodeT](estimatedNodesPerLayer) 
          
          @tailrec def loop(layer: Int, layerNodes: ArrayBuffer[NodeT]): CycleNodeOrTopologicalOrder = {
            layers += Layer(layer, layerNodes)
            val currentLayerNodes = if (doNodeSort) layerNodes.sorted(nodeOrdering) else layerNodes
            val nextLayerNodes = emptyBuffer
            var maybeCycleNode: Option[NodeT] = None
            val nrEnqueued = (0 /: currentLayerNodes) { (i, node) =>
              if (doNodeVisitor) nodeVisitor(node)
              node.visited = true
              (0 /: filteredSuccessors(node, _.visited, Double.NaN, true)) { (nrZeroInDegree, n) =>
                val newInDegree = inDegrees(n) - 1
                inDegrees.update (n, newInDegree)
                if (newInDegree == 0) {
                  nextLayerNodes += n
                  nrZeroInDegree + 1
                } else {
                  maybeCycleNode = maybeCycleNode orElse Some(n)
                  nrZeroInDegree
                }
              } + i
            }
            def resultOnSuccess: CycleNodeOrTopologicalOrder = Right(new TopologicalOrder(layers, identity)) 
            if (nrEnqueued == 0)
              maybeCycleNode.fold(ifEmpty = resultOnSuccess)(cycleNode => Left(cycleNode))
            else if (layers.size == untilDepth)
              resultOnSuccess
            else
              loop(layer + 1, nextLayerNodes)
          }
          
          val startBuffer = layer_0 match {
            case b: ArrayBuffer[NodeT] => b
            case t                     => emptyBuffer ++ t
          }
          loop(0, startBuffer)
        }
      }

    protected[collection] object Runner {
      @inline final def apply[U](stopAt:  NodeFilter,
                                 visitor: A => U): Runner[U] = new Runner[U](stopAt, visitor)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy