├── .sbtopts ├── project ├── build.properties └── plugins.sbt ├── bin └── package-and-test.sh ├── link.gif ├── turtle.gif ├── test ├── gdf │ ├── simple.gdf │ ├── simple-dir.gdf │ ├── missing-attr.gdf │ ├── simple-undir.gdf │ ├── turtles-own.gdf │ ├── breeds-own.gdf │ ├── link-breeds.gdf │ ├── builtins.gdf │ └── full.gdf ├── tmp │ └── .gitignore ├── vna │ └── full.vna ├── gml │ └── full.gml ├── missing-attr.graphml ├── missing-attr-type.graphml ├── missing-attr-name.graphml ├── exclude-dangling-link.graphml └── gexf │ └── full.gexf ├── src ├── test │ ├── Tests.scala │ └── org │ │ └── nlogo │ │ └── extensions │ │ └── nw │ │ ├── package.scala │ │ ├── GraphContextTests.scala │ │ ├── ClusteringTests.scala │ │ └── SubscriberTests.scala └── main │ └── org │ └── nlogo │ └── extensions │ └── nw │ ├── prim │ ├── Version.scala │ ├── Centrality.scala │ ├── jung │ │ ├── Clusterers.scala │ │ ├── GraphML.scala │ │ ├── MatrixIO.scala │ │ ├── Generators.scala │ │ └── Centrality.scala │ ├── InRadius.scala │ ├── jgrapht │ │ ├── MaximalCliques.scala │ │ └── Generators.scala │ ├── MeanPathLength.scala │ ├── Clustering.scala │ ├── SetContext.scala │ ├── Generators.scala │ ├── IO.scala │ └── Paths.scala │ ├── algorithms │ ├── InRadius.scala │ ├── MeanPathLength.scala │ ├── ErdosRenyiGenerator.scala │ ├── BreadthFirstSearch.scala │ ├── CentralityMeasurer.scala │ ├── BarabasiAlbertGenerator.scala │ ├── WattsStrogatzGenerator.scala │ ├── PathFinder.scala │ └── Clustering.scala │ ├── gephi │ ├── GephiUtils.scala │ ├── GephiExport.scala │ └── GephiImport.scala │ ├── util │ ├── TurtleSetsConverters.scala │ └── Cache.scala │ ├── GraphContextManager.scala │ ├── jung │ ├── package.scala │ ├── Generators.scala │ ├── DummyGraph.scala │ ├── io │ │ ├── Matrix.scala │ │ ├── GraphMLWriterWithAttribType.scala │ │ ├── GraphMLExport.scala │ │ └── GraphMLImport.scala │ ├── Graphs.scala │ └── Algorithms.scala │ ├── MonitoredAgentSets.scala │ ├── jgrapht │ ├── Generators.scala │ └── Graphs.scala │ ├── Graph.scala │ ├── NetworkExtension.scala │ ├── NetworkExtensionUtil.scala │ └── GraphContext.scala ├── BUILDING.md ├── .gitignore ├── .github └── workflows │ └── main.yml ├── LICENSE.md ├── proguard └── lite.txt └── USING.md /.sbtopts: -------------------------------------------------------------------------------- 1 | -Djava.awt.headless=true 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.7.2 2 | -------------------------------------------------------------------------------- /bin/package-and-test.sh: -------------------------------------------------------------------------------- 1 | sbt package 2 | cd ../.. 3 | sbt "te nw" 4 | cd - -------------------------------------------------------------------------------- /link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetLogo/NW-Extension/HEAD/link.gif -------------------------------------------------------------------------------- /turtle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NetLogo/NW-Extension/HEAD/turtle.gif -------------------------------------------------------------------------------- /test/gdf/simple.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR 2 | 1 3 | 2 4 | edgedef> node1,node2 5 | 1,2 6 | -------------------------------------------------------------------------------- /test/tmp/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /test/gdf/simple-dir.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR 2 | 1 3 | 2 4 | edgedef> node1,node2,directed BOOLEAN 5 | 1,2,true 6 | -------------------------------------------------------------------------------- /test/gdf/missing-attr.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,missing BOOLEAN 2 | 1,true 3 | 2,false 4 | edgedef> node1,node2 5 | 1,2 6 | -------------------------------------------------------------------------------- /test/gdf/simple-undir.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR 2 | 1 3 | 2 4 | edgedef> node1,node2,directed BOOLEAN 5 | 1,2,false 6 | -------------------------------------------------------------------------------- /test/gdf/turtles-own.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,tvar BOOLEAN 2 | 1,true 3 | 2,false 4 | edgedef> node1,node2 5 | 1,2 6 | -------------------------------------------------------------------------------- /test/gdf/breeds-own.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,breed VARCHAR,fur VARCHAR, spots VARCHAR 2 | 1,mice,white, 3 | 2,frogs,,yellow 4 | edgedef> node1,node2 5 | 1,2 6 | -------------------------------------------------------------------------------- /test/gdf/link-breeds.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,tvar BOOLEAN 2 | 1,true 3 | 2,false 4 | edgedef> node1,node2,color,breed,lvar DOUBLE,weight DOUBLE 5 | 1,2,'0,255,0',directed-edges,5, 6 | 2,1,'0,0,255',undirected-edges,,10 7 | -------------------------------------------------------------------------------- /src/test/Tests.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.headless.TestLanguage 6 | 7 | class Tests extends TestLanguage(Seq(new java.io.File("tests.txt").getCanonicalFile)) 8 | -------------------------------------------------------------------------------- /test/gdf/builtins.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,label VARCHAR,width DOUBLE,height DOUBLE,x DOUBLE,y DOUBLE,color VARCHAR,xcor DOUBLE,ycor DOUBLE 2 | 1,"one",4.0,4.0,16.540089,-24.223572,'255,0,0',1,2 3 | 2,"two",4.0,4.0,-16.540089,24.223572,'255,200,0',3,4 4 | edgedef> node1,node2,weight DOUBLE,directed BOOLEAN 5 | 1,2,1.0,false 6 | -------------------------------------------------------------------------------- /test/gdf/full.gdf: -------------------------------------------------------------------------------- 1 | nodedef> name VARCHAR,tvar BOOLEAN,breed VARCHAR,spots BOOLEAN,fur VARCHAR,missing VARCHAR 2 | 1,true,MICE,,white,foo 3 | 2,false,FROGS,true,,foo 4 | 3,false,FROGS,true,,foo 5 | edgedef> node1,node2,color,breed VARCHAR,lvar DOUBLE,weight DOUBLE,directed BOOLEAN 6 | 1,2,'0,255,0',directed-edges,5,,true 7 | 3,1,'0,0,255',undirected-edges,,10,false 8 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | "netlogo-extension-plugin" at "https://dl.cloudsmith.io/public/netlogo/netlogo-extension-plugin/maven/" 3 | , "netlogo-extension-documentation" at "https://dl.cloudsmith.io/public/netlogo/netlogo-extension-documentation/maven/" 4 | ) 5 | 6 | addSbtPlugin("org.nlogo" % "netlogo-extension-plugin" % "7.0.2") 7 | addSbtPlugin("org.nlogo" % "netlogo-extension-documentation" % "0.8.3") 8 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | ## Building 2 | 3 | The extension is written in Scala (version 2.9.2). 4 | 5 | Run `sbt package` to build the extension. 6 | 7 | Unless they are already present, sbt will download the needed Jung and JGraphT jar files from the Internet. 8 | 9 | If the build succeeds, `nw.jar` will be created. To use the extension, this file and all the other jars will need to be in the `extensions/nw` folder under your NetLogo installation. 10 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/Version.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.prim 2 | 3 | import org.nlogo.api 4 | import org.nlogo.core.Syntax._ 5 | import org.nlogo.extensions.nw.NetworkExtension 6 | 7 | class Version(extension: NetworkExtension) 8 | extends api.Reporter { 9 | override def getSyntax = reporterSyntax(ret = StringType) 10 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = extension.version 11 | } 12 | -------------------------------------------------------------------------------- /test/vna/full.vna: -------------------------------------------------------------------------------- 1 | *Node data 2 | ID tvar breed spots fur missing 3 | 1 true MICE "" white foo 4 | 2 false FROGS true "" foo 5 | 3 false FROGS true "" foo 6 | *Node properties 7 | ID x y size color shortlabel 8 | 1 -355.93912 -114.81057 10.0 153 1 9 | 2 -170.28754 214.69585 10.0 153 2 10 | 3 526.2267 -99.88528 10.0 153 3 11 | *Tie data 12 | from to strength breed lvar 13 | 1 2 1.0 directed-edges 5.0 14 | 3 1 10.0 undirected-edges "" 15 | 1 3 10.0 undirected-edges "" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundledFiles 2 | target/ 3 | /extensions/ 4 | /*.jar 5 | /*.zip 6 | 7 | *~ 8 | *.orig 9 | /jung-src/ 10 | /jgrapht-src/ 11 | /nw/ 12 | *.gz 13 | 14 | # eclipse files 15 | .project 16 | .classpath 17 | .cache 18 | .settings 19 | .texlipse 20 | 21 | # latex temp files 22 | *.log 23 | *.aux 24 | 25 | # intellij 26 | .idea* 27 | 28 | # vim 29 | *.swp 30 | 31 | # ensime 32 | .ensime* 33 | 34 | # emacs 35 | \#*\# 36 | \.\#* 37 | 38 | # os x 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /src/test/org/nlogo/extensions/nw/package.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions 4 | 5 | import scala.jdk.CollectionConverters.IterableHasAsScala 6 | 7 | import org.nlogo.workspace.AbstractWorkspace 8 | 9 | package object nw { 10 | 11 | def networkExtension(ws: AbstractWorkspace) = 12 | ws.getExtensionManager.loadedExtensions.asScala.collect { 13 | case ext: NetworkExtension => ext 14 | }.head 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | env: 8 | LIBERICA_URL: https://download.bell-sw.com/java/17.0.3+7/bellsoft-jdk17.0.3+7-linux-amd64-full.tar.gz 9 | 10 | jobs: 11 | build-and-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: olafurpg/setup-scala@v10 16 | with: 17 | java-version: liberica@17=tgz+${{ env.LIBERICA_URL }} 18 | - run: sbt -v update compile 19 | - run: sbt -v test 20 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/Centrality.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.prim 2 | 3 | import org.nlogo.api 4 | import org.nlogo.agent 5 | import org.nlogo.core.Syntax._ 6 | import org.nlogo.extensions.nw.GraphContextProvider 7 | 8 | class EigenvectorCentrality(gcp: GraphContextProvider) extends api.Reporter { 9 | override def getSyntax = reporterSyntax(ret = NumberType, agentClassString = "-T--") 10 | override def report(args: Array[api.Argument], context: api.Context) = { 11 | val gc = gcp.getGraphContext(context.getAgent.world) 12 | gc.eigenvectorCentrality(context.getAgent.asInstanceOf[agent.Turtle]).asInstanceOf[java.lang.Double] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/InRadius.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.algorithms 4 | 5 | import org.nlogo.agent.Turtle 6 | import org.nlogo.api 7 | import org.nlogo.extensions.nw.GraphContext 8 | import org.nlogo.extensions.nw.util.TurtleSetsConverters.toTurtleSet 9 | 10 | object InRadius { 11 | def inRadius(graphContext: GraphContext, start: Turtle, radius: Int, reverse: Boolean = false): api.AgentSet = { 12 | val result: LazyList[Turtle] = 13 | BreadthFirstSearch(graphContext, start, followOut = !reverse, followIn = reverse) 14 | .takeWhile(_.tail.size <= radius) 15 | .map(_.head) 16 | toTurtleSet(result) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Terms of Use 2 | 3 | Copyright 1999-2013 by Uri Wilensky. 4 | 5 | This program is free software; you can redistribute it and/or modify it under the terms of the [GNU General Public License](http://www.gnu.org/copyleft/gpl.html) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 6 | 7 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 8 | 9 | [Jung](http://jung.sourceforge.net/) is licensed under the [BSD license](http://jung.sourceforge.net/license.txt) and [JGraphT](http://jgrapht.org/) is licensed under the [LGPL license](http://jgrapht.org/LGPL.html). 10 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/gephi/GephiUtils.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.gephi 2 | 3 | import org.nlogo.extensions.nw.NetworkExtension 4 | 5 | object GephiUtils { 6 | // Gephi uses OpenIDE's Lookup system, who's default Lookup uses the current context class loader. 7 | // However, this is not the extension's class loader, which is what we want it to use. This is the 8 | // best solution I could come up. Would love to find another one. 9 | // BCH 1/14/2015 10 | def withNWLoaderContext[T] = withClassLoaderContext[T](classOf[NetworkExtension].getClassLoader) 11 | 12 | def withClassLoaderContext[T](loader: ClassLoader)(body: => T) = { 13 | val oldLoader = Thread.currentThread.getContextClassLoader 14 | try { 15 | Thread.currentThread.setContextClassLoader(loader) 16 | body 17 | } finally { 18 | Thread.currentThread.setContextClassLoader(oldLoader) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/MeanPathLength.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.algorithms 4 | 5 | import scala.util.control.Breaks.break 6 | import scala.util.control.Breaks.breakable 7 | 8 | import org.nlogo.agent.Turtle 9 | 10 | object MeanPathLength { 11 | def meanPathLength( 12 | turtles: Iterable[Turtle], 13 | distance: (Turtle, Turtle) => Option[Double]): Option[Double] = { 14 | var sum = 0.0 15 | var n = 0 16 | breakable { 17 | for { 18 | source <- turtles 19 | target <- turtles 20 | if target != source 21 | dist = distance(source, target) 22 | } { 23 | if (dist.isEmpty) { 24 | sum = Double.NaN 25 | break() 26 | } 27 | n += 1 28 | sum += dist.get 29 | } 30 | } 31 | Option(sum / n).filterNot(_.isNaN) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/util/TurtleSetsConverters.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.util 2 | 3 | import java.lang.{ Iterable => JIterable } 4 | 5 | import org.nlogo.api 6 | import org.nlogo.agent.{ Agent, AgentSet, Turtle, World } 7 | import org.nlogo.core.AgentKind 8 | 9 | import scala.jdk.CollectionConverters.IterableHasAsScala 10 | 11 | object TurtleSetsConverters { 12 | 13 | def toTurtleSets(turtleIterables: Iterable[JIterable[Turtle]], rng: java.util.Random): Seq[api.AgentSet] = { 14 | val turtleSets: Seq[api.AgentSet] = 15 | turtleIterables.map(toTurtleSet).toSeq 16 | new scala.util.Random(rng).shuffle(turtleSets) 17 | } 18 | 19 | def toTurtleSet(turtles: java.lang.Iterable[Turtle]): api.AgentSet = 20 | toTurtleSet(turtles.asScala) 21 | 22 | def toTurtleSet(turtles: Iterable[Turtle]): api.AgentSet = 23 | AgentSet.fromArray(AgentKind.Turtle, turtles.toArray[Agent]) 24 | 25 | def emptyTurtleSet(world: World) = 26 | AgentSet.fromArray(AgentKind.Turtle, Array.empty[Agent]) 27 | } 28 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jung/Clusterers.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jung 4 | 5 | import org.nlogo.api 6 | import org.nlogo.api.ScalaConversions.toLogoList 7 | import org.nlogo.core.Syntax._ 8 | import org.nlogo.extensions.nw.GraphContextProvider 9 | import org.nlogo.extensions.nw.util.TurtleSetsConverters.toTurtleSet 10 | 11 | class BicomponentClusters(gcp: GraphContextProvider) 12 | extends api.Reporter { 13 | override def getSyntax = reporterSyntax(ret = ListType) 14 | override def report(args: Array[api.Argument], context: api.Context) = { 15 | val graph = gcp.getGraphContext(context.getAgent.world).asUndirectedJungGraph 16 | toLogoList(graph 17 | .BicomponentClusterer 18 | .clusters(context.getRNG)) 19 | } 20 | } 21 | 22 | class WeakComponentClusters(gcp: GraphContextProvider) 23 | extends api.Reporter { 24 | override def getSyntax = reporterSyntax(ret = ListType) 25 | override def report(args: Array[api.Argument], context: api.Context) = { 26 | val comps = gcp.getGraphContext(context.getAgent.world).components 27 | toLogoList(new scala.util.Random(context.getRNG).shuffle(comps.map(toTurtleSet).toSeq)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/ErdosRenyiGenerator.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.agent 4 | import org.nlogo.agent.{ AgentSet, World } 5 | import org.nlogo.extensions.nw.NetworkExtensionUtil.createTurtle 6 | import org.nlogo.api.MersenneTwisterFast 7 | 8 | object ErdosRenyiGenerator { 9 | def generate( 10 | world: World, 11 | turtleBreed: AgentSet, 12 | linkBreed: AgentSet, 13 | nbTurtles: Int, 14 | connexionProbability: Double, 15 | rng: MersenneTwisterFast): Seq[agent.Turtle] = { 16 | require(nbTurtles > 0, 17 | "A positive number of turtles must be specified.") 18 | require(connexionProbability >= 0 && connexionProbability <= 1.0, 19 | "The connexion probability must be between 0 and 1.") 20 | val turtles = Iterator.fill(nbTurtles)(createTurtle(world, turtleBreed, rng)).toIndexedSeq 21 | def jRange(i: Int) = 22 | if (linkBreed.isDirected) (0 until nbTurtles) filter { _ != i } 23 | else (i + 1) until nbTurtles 24 | for { 25 | i <- 0 until nbTurtles 26 | j <- jRange(i) 27 | if rng.nextDouble() < connexionProbability 28 | } world.linkManager.createLink(turtles(i), turtles(j), linkBreed) 29 | turtles 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/InRadius.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim 4 | 5 | import org.nlogo.agent 6 | import org.nlogo.api 7 | import org.nlogo.core.Syntax._ 8 | import org.nlogo.extensions.nw.algorithms.InRadius._ 9 | import org.nlogo.extensions.nw.GraphContextProvider 10 | 11 | trait InRadiusPrim extends api.Reporter { 12 | val gcp: GraphContextProvider 13 | val reverse: Boolean 14 | override def getSyntax = reporterSyntax( 15 | right = List(NumberType), 16 | ret = TurtlesetType, 17 | agentClassString = "-T--") 18 | override def report(args: Array[api.Argument], context: api.Context) = { 19 | val world = context.getAgent.world.asInstanceOf[agent.World] 20 | val graphContext = gcp.getGraphContext(world) 21 | val source = context.getAgent.asInstanceOf[agent.Turtle] 22 | val radius = args(0).getIntValue 23 | if (radius < 0) throw new api.ExtensionException("radius cannot be negative") 24 | inRadius(graphContext, source, radius, reverse) 25 | } 26 | } 27 | 28 | class TurtlesInRadius(override val gcp: GraphContextProvider) 29 | extends InRadiusPrim { 30 | override val reverse = false 31 | } 32 | 33 | class TurtlesInReverseRadius(override val gcp: GraphContextProvider) 34 | extends InRadiusPrim { 35 | override val reverse = true 36 | } 37 | -------------------------------------------------------------------------------- /test/gml/full.gml: -------------------------------------------------------------------------------- 1 | graph 2 | [ 3 | Creator Gephi 4 | node 5 | [ 6 | id "1" 7 | label "1" 8 | graphics 9 | [ 10 | x -355.93912 11 | y -114.81057 12 | z 0.0 13 | w 10.0 14 | h 10.0 15 | d 10.0 16 | fill "#999999" 17 | ] 18 | tvar "true" 19 | breed "MICE" 20 | fur "white" 21 | missing "foo" 22 | ] 23 | node 24 | [ 25 | id "2" 26 | label "2" 27 | graphics 28 | [ 29 | x -170.28754 30 | y 214.69585 31 | z 0.0 32 | w 10.0 33 | h 10.0 34 | d 10.0 35 | fill "#999999" 36 | ] 37 | tvar "false" 38 | breed "FROGS" 39 | spots "true" 40 | missing "foo" 41 | ] 42 | node 43 | [ 44 | id "3" 45 | label "3" 46 | graphics 47 | [ 48 | x 526.2267 49 | y -99.88528 50 | z 0.0 51 | w 10.0 52 | h 10.0 53 | d 10.0 54 | fill "#999999" 55 | ] 56 | tvar "false" 57 | breed "FROGS" 58 | spots "true" 59 | missing "foo" 60 | ] 61 | edge 62 | [ 63 | id "16" 64 | source "1" 65 | target "2" 66 | value 1.0 67 | directed 1 68 | breed "directed-edges" 69 | lvar "5.0" 70 | ] 71 | edge 72 | [ 73 | id "17" 74 | source "3" 75 | target "1" 76 | value 10.0 77 | directed 0 78 | breed "undirected-edges" 79 | ] 80 | ] 81 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jgrapht/MaximalCliques.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jgrapht 4 | 5 | import org.nlogo.api 6 | import org.nlogo.api.ScalaConversions.toLogoList 7 | import org.nlogo.core.Syntax._ 8 | import org.nlogo.extensions.nw.GraphContextProvider 9 | 10 | trait CliquePrim 11 | extends api.Reporter { 12 | override def getSyntax = reporterSyntax(ret = ListType) 13 | val gcp: GraphContextProvider 14 | def graph(context: api.Context) = { 15 | val gc = gcp.getGraphContext(context.getAgent.world) 16 | if (gc.isDirected) 17 | throw new api.ExtensionException("Current graph must be undirected") 18 | gc.asJGraphTGraph 19 | } 20 | } 21 | 22 | class MaximalCliques(override val gcp: GraphContextProvider) 23 | extends CliquePrim { 24 | override def report(args: Array[api.Argument], context: api.Context) = { 25 | toLogoList( 26 | graph(context) 27 | .BronKerboschCliqueFinder 28 | .allCliques(context.getRNG)) 29 | } 30 | } 31 | 32 | class BiggestMaximalCliques(override val gcp: GraphContextProvider) 33 | extends CliquePrim { 34 | override def report(args: Array[api.Argument], context: api.Context) = { 35 | toLogoList( 36 | graph(context) 37 | .BronKerboschCliqueFinder 38 | .biggestCliques(context.getRNG)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/BreadthFirstSearch.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.algorithms 4 | 5 | import org.nlogo.extensions.nw.GraphContext 6 | import org.nlogo.agent.Turtle 7 | 8 | object BreadthFirstSearch { 9 | /** 10 | * Traverses the network in breadth-first order. 11 | * Each List[Turtle] is a reversed path (destination is head); 12 | * the paths share storage, so total memory usage stays within O(n). 13 | * Adapted from the original network extension written by Seth Tisue 14 | */ 15 | def apply(gc: GraphContext, start: Turtle, followOut: Boolean, followIn: Boolean): LazyList[List[Turtle]] = { 16 | def rawNeighbors(node: Turtle) = 17 | (if (followOut) gc.outNeighbors(node) else Seq.empty[Turtle]) ++ 18 | (if (followIn) gc.inNeighbors(node) else Seq.empty[Turtle]) 19 | 20 | val seen: Turtle => Boolean = { 21 | val memory = collection.mutable.HashSet[Turtle](start) 22 | t => memory(t) || { memory += t; false } 23 | } 24 | def neighbors(turtle: Turtle): Iterable[Turtle] = rawNeighbors(turtle).filterNot(seen) 25 | def nextLayer(layer: LazyList[List[Turtle]]) = 26 | for { 27 | path <- layer 28 | neighbor <- neighbors(path.head) 29 | } yield neighbor :: path 30 | LazyList.iterate(LazyList(List(start)))(nextLayer) 31 | .takeWhile(_.nonEmpty) 32 | .flatten 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/org/nlogo/extensions/nw/GraphContextTests.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.headless.HeadlessWorkspace 6 | import org.scalatest.funsuite.AnyFunSuite 7 | import org.scalatest.GivenWhenThen 8 | 9 | class SetContextTestSuite extends AnyFunSuite with GivenWhenThen { 10 | test("avoid dangling links in context") { 11 | val ws: HeadlessWorkspace = HeadlessWorkspace.newInstance 12 | ws.setModelPath(new java.io.File("tests.txt").getPath) 13 | try { 14 | 15 | Given("two mice linked together and a frog linked to one mouse") 16 | ws.initForTesting(1, "extensions [nw]\n" + HeadlessWorkspace.TestDeclarations) 17 | ws.command("create-mice 2") 18 | ws.command("create-frogs 1") 19 | ws.command("ask one-of mice [ create-links-with other turtles ]") 20 | When("we set the context to `mice links`") 21 | ws.command("nw:set-context mice links") 22 | 23 | val gc = networkExtension(ws).getGraphContext(ws.world) 24 | 25 | Then("the context should contain the two mice") 26 | assertResult(2)(gc.nodes.size) 27 | And("only the one link between them") 28 | assertResult(1)(gc.links.size) 29 | 30 | And("the set of allEdges from all the turtles should be the same as `links`") 31 | assertResult(gc.nodes.flatMap(gc.allEdges(_)).toSeq)(gc.links.toSeq) 32 | 33 | } finally ws.dispose() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/GraphContextManager.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.agent 6 | import org.nlogo.api 7 | 8 | trait GraphContextProvider { 9 | def getGraphContext(world: api.World): GraphContext 10 | def withTempGraphContext(gc: GraphContext)(f: () => Unit): Unit 11 | } 12 | 13 | trait GraphContextManager extends GraphContextProvider { 14 | 15 | private var _graphContext: Option[GraphContext] = None 16 | 17 | override def getGraphContext(world: api.World): GraphContext = { 18 | val w = world.asInstanceOf[agent.World] 19 | val oldGraphContext = _graphContext 20 | _graphContext = _graphContext 21 | .map(_.verify(w)) 22 | .orElse(Some(new GraphContext(w, w.turtles, w.links))) 23 | for { 24 | oldGC <- oldGraphContext 25 | newGC <- _graphContext 26 | if oldGC != newGC 27 | } oldGC.unsubscribe() 28 | _graphContext.get 29 | } 30 | 31 | def setGraphContext(gc: GraphContext): Unit = { 32 | _graphContext.foreach(_.unsubscribe()) 33 | _graphContext = Some(gc) 34 | } 35 | 36 | def withTempGraphContext(gc: GraphContext)(f: () => Unit): Unit = { 37 | val currentContext = _graphContext 38 | _graphContext = Some(gc) 39 | f() 40 | gc.unsubscribe() 41 | _graphContext = currentContext 42 | } 43 | 44 | def clearContext(): Unit = { 45 | _graphContext.foreach(_.unsubscribe()) 46 | _graphContext = None 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/package.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw 2 | 3 | import org.apache.commons.collections15.Factory 4 | import org.apache.commons.collections15.Transformer 5 | import org.nlogo.agent.{AgentSet, Directedness, Turtle, World} 6 | import edu.uci.ics.jung.{graph => jg} 7 | import edu.uci.ics.jung.graph.util.Pair 8 | 9 | package object jung { 10 | 11 | def transformer[A, B](f: A => B) = 12 | new Transformer[A, B]() { 13 | override def transform(a: A): B = f(a) 14 | } 15 | 16 | def factory[A](f: => A) = 17 | new Factory[A]() { 18 | override def create = f 19 | } 20 | 21 | def factoryFor[V, E](linkBreed: AgentSet): Factory[jg.Graph[V, E]] = 22 | if (linkBreed.isDirected) 23 | factory { new jg.DirectedSparseGraph[V, E] } 24 | else 25 | factory { new jg.UndirectedSparseGraph[V, E] } 26 | 27 | def directedFactory[V, E] = jg.DirectedSparseGraph.getFactory[V, E] 28 | def undirectedFactory[V, E] = jg.UndirectedSparseGraph.getFactory[V, E] 29 | def sparseGraphFactory[V, E] = jg.SparseMultigraph.getFactory[V, E] 30 | 31 | def createLink[V](turtles: Map[V, Turtle], endPoints: Pair[V], defaultDirected: Boolean, linkBreed: AgentSet, world: World) = { 32 | if (linkBreed.directed == Directedness.Undetermined) { 33 | linkBreed.setDirected(defaultDirected) 34 | } 35 | world.linkManager.createLink( 36 | turtles(endPoints.getFirst), 37 | turtles(endPoints.getSecond), 38 | linkBreed) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/MeanPathLength.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.prim 2 | 3 | import org.nlogo.api 4 | import org.nlogo.core.Syntax._ 5 | import org.nlogo.extensions.nw.algorithms.MeanPathLength._ 6 | import org.nlogo.agent.Turtle 7 | import org.nlogo.extensions.nw.GraphContextProvider 8 | import org.nlogo.extensions.nw.NetworkExtensionUtil.canonocilizeVar 9 | 10 | class MeanPathLength(gcp: GraphContextProvider) 11 | extends api.Reporter { 12 | override def getSyntax = reporterSyntax(ret = NumberType | BooleanType) 13 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 14 | val gc = gcp.getGraphContext(context.getAgent.world) 15 | val dist = gc.pathFinder.distance(_: Turtle, _: Turtle) 16 | meanPathLength(gc.nodes, dist) 17 | .map(Double.box) 18 | .getOrElse(java.lang.Boolean.FALSE) 19 | } 20 | } 21 | 22 | class MeanWeightedPathLength(gcp: GraphContextProvider) 23 | extends api.Reporter { 24 | override def getSyntax = reporterSyntax( 25 | right = List(StringType | SymbolType), 26 | ret = NumberType | BooleanType) 27 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 28 | val weightVariable = canonocilizeVar(args(0).get) 29 | val gc = gcp.getGraphContext(context.getAgent.world) 30 | val dist = gc.pathFinder.distance(_: Turtle, _: Turtle, Some(weightVariable)) 31 | meanPathLength(gc.nodes, dist) 32 | .map(Double.box) 33 | .getOrElse(java.lang.Boolean.FALSE) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jung/GraphML.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jung 4 | 5 | import org.nlogo.api 6 | import org.nlogo.core.Syntax._ 7 | import org.nlogo.agent 8 | import org.nlogo.extensions.nw.NetworkExtensionUtil.TurtleCreatingCommand 9 | import org.nlogo.extensions.nw.jung.io.GraphMLExport 10 | import org.nlogo.extensions.nw.jung.io.GraphMLImport 11 | import org.nlogo.extensions.nw.GraphContextProvider 12 | 13 | class SaveGraphML(gcp: GraphContextProvider) 14 | extends api.Command { 15 | override def getSyntax = commandSyntax(right = List(StringType)) 16 | override def perform(args: Array[api.Argument], context: api.Context): Unit = { 17 | val fm = context.asInstanceOf[org.nlogo.nvm.ExtensionContext].workspace.fileManager 18 | GraphMLExport.save(gcp.getGraphContext(context.getAgent.world), 19 | fm.attachPrefix(args(0).getString)) 20 | } 21 | } 22 | 23 | class LoadGraphML extends TurtleCreatingCommand { 24 | override def getSyntax = commandSyntax(List(StringType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 25 | def createTurtles(args: Array[api.Argument], context: api.Context) = { 26 | val fm = context.asInstanceOf[org.nlogo.nvm.ExtensionContext].workspace.fileManager 27 | GraphMLImport.load( 28 | fileName = fm.attachPrefix(args(0).getString), 29 | world = context.getAgent.world.asInstanceOf[agent.World], 30 | rng = context.getRNG) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/missing-attr.graphml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | up 31 | 0 32 | 1 33 | 34 | default 35 | turtles 36 | 0 37 | false 38 | 9.9 39 | 0 40 | 5 41 | 0 42 | 1 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/Generators.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung 4 | 5 | import org.nlogo.agent.{ AgentSet, World } 6 | import org.nlogo.api.MersenneTwisterFast 7 | 8 | import edu.uci.ics.jung.algorithms.generators.Lattice2DGenerator 9 | import edu.uci.ics.jung.algorithms.generators.random.KleinbergSmallWorldGenerator 10 | 11 | class Generator( 12 | turtleBreed: AgentSet, 13 | linkBreed: AgentSet, 14 | world: World) { 15 | 16 | type V = DummyGraph.Vertex 17 | type E = DummyGraph.Edge 18 | 19 | lazy val graphFactory = factoryFor[V, E](linkBreed) 20 | lazy val undirectedGraphFactory = undirectedFactory[V, E] 21 | lazy val edgeFactory = DummyGraph.edgeFactory 22 | lazy val vertexFactory = DummyGraph.vertexFactory 23 | 24 | def lattice2D(rowCount: Int, colCount: Int, isToroidal: Boolean, rng: MersenneTwisterFast) = { 25 | val generator = new Lattice2DGenerator( 26 | graphFactory, vertexFactory, edgeFactory, rowCount, colCount, isToroidal).create 27 | 28 | DummyGraph.importToNetLogo(generator, world, turtleBreed, linkBreed, rng, sorted = true) 29 | } 30 | 31 | def kleinbergSmallWorld(rowCount: Int, colCount: Int, 32 | clusteringExponent: Double, isToroidal: Boolean, rng: MersenneTwisterFast) = { 33 | val gen = new KleinbergSmallWorldGenerator( 34 | undirectedGraphFactory, vertexFactory, edgeFactory, 35 | rowCount, colCount, clusteringExponent, isToroidal) 36 | gen.setRandom(rng) 37 | DummyGraph.importToNetLogo(gen.create, world, turtleBreed, linkBreed, rng, sorted = true) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jung/MatrixIO.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jung 4 | 5 | import org.nlogo.api 6 | import org.nlogo.agent.World 7 | import org.nlogo.core.Syntax._ 8 | import org.nlogo.extensions.nw.GraphContextProvider 9 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 10 | import org.nlogo.extensions.nw.NetworkExtensionUtil.TurtleCreatingCommand 11 | import org.nlogo.extensions.nw.jung.io.Matrix 12 | 13 | class SaveMatrix(gcp: GraphContextProvider) 14 | extends api.Command { 15 | override def getSyntax = commandSyntax(List(StringType)) 16 | override def perform(args: Array[api.Argument], context: api.Context): Unit = { 17 | val graph = gcp.getGraphContext(context.getAgent.world).asJungGraph 18 | val fm = context.asInstanceOf[org.nlogo.nvm.ExtensionContext].workspace.fileManager 19 | Matrix.save(graph, fm.attachPrefix(args(0).getString)) 20 | } 21 | } 22 | 23 | class LoadMatrix 24 | extends TurtleCreatingCommand { 25 | override def getSyntax = commandSyntax(List(StringType, TurtlesetType, LinksetType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 26 | def createTurtles(args: Array[api.Argument], context: api.Context) = { 27 | implicit val world = context.world.asInstanceOf[World] 28 | val fm = context.asInstanceOf[org.nlogo.nvm.ExtensionContext].workspace.fileManager 29 | Matrix.load( 30 | filename = fm.attachPrefix(args(0).getString), 31 | turtleBreed = args(1).getAgentSet.requireTurtleBreed, 32 | linkBreed = args(2).getAgentSet.requireLinkBreed, 33 | world = world, 34 | rng = context.getRNG) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/CentralityMeasurer.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.agent.Turtle 4 | 5 | trait CentralityMeasurer { 6 | def inNeighbors(turtle: Turtle): Iterable[Turtle] 7 | def components: Iterable[Iterable[Turtle]] 8 | 9 | // Initializing with in-degree works well with directed graphs, knocking out obviously non-strongly reachable nodes 10 | // immediately. Initializing with all ones can make convergence take much longer. -- BCH 5/12/2014 11 | private def inDegrees(turtles: Iterable[Turtle]) = 12 | turtles.foldLeft(Map.empty[Turtle, Double]) { 13 | (m, t) => m + (t -> inNeighbors(t).size.toDouble) 14 | } 15 | 16 | lazy val eigenvectorCentrality: Map[Turtle, Double] = components.flatMap { turtles => 17 | Iterator.iterate(inDegrees(turtles))((last) => { 18 | val result = last map { 19 | // Leaving the last score allows us to handle networks for which power iteration normally fails, e.g. 0--1--2 20 | // Gephi does this -- BCH 5/12/2014 21 | case (turtle: Turtle, lastScore: Double) => 22 | turtle -> (lastScore + (inNeighbors(turtle) map last).sum) 23 | } 24 | // This is how gephi normalizes -- BCH 5/12/2014 25 | val normalizer = result.values.max 26 | if (normalizer > 0) { 27 | result map { 28 | case (turtle: Turtle, score: Double) => turtle -> score / normalizer 29 | } 30 | } else { 31 | // Everything is disconnected... just give everyone 1s -- BCH 5/12/2014 32 | result map { 33 | case (turtle: Turtle, score: Double) => turtle -> 1.0 34 | } 35 | } 36 | }).drop(100).next() 37 | }.toMap 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/BarabasiAlbertGenerator.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.agent.{AgentSet, Link, Turtle, World} 4 | import org.nlogo.api.MersenneTwisterFast 5 | import org.nlogo.extensions.nw.NetworkExtensionUtil.createTurtle 6 | 7 | import scala.collection.mutable 8 | import scala.collection.mutable.ArrayBuffer 9 | 10 | object BarabasiAlbertGenerator { 11 | def generate( world: World, 12 | turtleBreed: AgentSet, 13 | linkBreed: AgentSet, 14 | numTurtles: Int, 15 | minDegree: Int, 16 | rng: MersenneTwisterFast): Seq[Turtle] = { 17 | require(numTurtles > minDegree, "The number of turtles must be larger than the minimum degree.") 18 | 19 | val turtles = ArrayBuffer.fill(minDegree + 1)(createTurtle(world, turtleBreed, rng)) 20 | val links = turtles.combinations(2).map { 21 | case s +: t +: tail => world.getOrCreateLink(s, t, linkBreed) 22 | case _ => throw new IllegalStateException 23 | }.to(ArrayBuffer) 24 | 25 | for (_ <- turtles.size until numTurtles) { 26 | val s = createTurtle(world, turtleBreed, rng) 27 | val ls = mutable.LinkedHashSet[Link]() 28 | while (ls.size < minDegree) { 29 | // Grabbing a random end of a random link is a very fast and simple way of sampling on degree. However, it is 30 | // not generalizable to different weighting schemes. 31 | val l = links(rng.nextInt(links.length)) 32 | val t = if (rng.nextBoolean) l.end1 else l.end2 33 | ls.add(world.getOrCreateLink(s, t, linkBreed)) 34 | } 35 | links ++= ls 36 | turtles += s 37 | } 38 | 39 | turtles.toSeq 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/missing-attr-type.graphml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | up 31 | 0 32 | 1 33 | 34 | default 35 | turtles 36 | 0 37 | false 38 | 9.9 39 | 0 40 | 5 41 | 0 42 | 1 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /test/missing-attr-name.graphml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | up 31 | 0 32 | 1 33 | 34 | default 35 | turtles 36 | 0 37 | false 38 | 9.9 39 | 0 40 | 5 41 | 0 42 | 1 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/DummyGraph.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung 4 | 5 | import org.apache.commons.collections15.Factory 6 | import org.nlogo.agent.AgentSet 7 | import org.nlogo.agent.Turtle 8 | import org.nlogo.agent.World 9 | import org.nlogo.extensions.nw.NetworkExtensionUtil.createTurtle 10 | import org.nlogo.api.MersenneTwisterFast 11 | 12 | import edu.uci.ics.jung 13 | 14 | import scala.jdk.CollectionConverters.CollectionHasAsScala 15 | 16 | object DummyGraph { 17 | // TODO: the vertex id thing is a ugly hack to get around the fact that 18 | // Jung has no ordered SparseGraph (only ordered MultiGraphs). Ideally, 19 | // we would add a custom sorted graph. NP 2012-06-13 20 | private var vertexIdCounter = 0L 21 | case class Vertex(val id: Long) 22 | class Edge 23 | def edgeFactory: Factory[Edge] = new Factory[Edge]() { 24 | def create = new Edge 25 | } 26 | def vertexFactory: Factory[Vertex] = new Factory[Vertex]() { 27 | def create = { 28 | vertexIdCounter += 1 29 | new Vertex(vertexIdCounter) 30 | } 31 | } 32 | 33 | def importToNetLogo( 34 | graph: jung.graph.Graph[Vertex, Edge], 35 | world: World, 36 | turtleBreed: AgentSet, 37 | linkBreed: AgentSet, 38 | rng: MersenneTwisterFast, 39 | sorted: Boolean = false) = { 40 | 41 | val vs = graph.getVertices.asScala 42 | val vertices = if (sorted) vs.toSeq.sortBy(_.id) else vs 43 | 44 | val turtles: Map[Vertex, Turtle] = 45 | vertices.map { v => 46 | v -> createTurtle(world, turtleBreed, rng) 47 | }.toMap 48 | 49 | graph.getEdges.asScala.foreach { e => 50 | createLink(turtles, graph.getEndpoints(e), defaultDirected = false, linkBreed, world) 51 | } 52 | 53 | turtles.valuesIterator 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/util/Cache.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.util 2 | 3 | import scala.collection.mutable 4 | import scala.ref.WeakReference 5 | import org.nlogo.agent.World.VariableWatcher 6 | import org.nlogo.agent.{World, Agent} 7 | 8 | class Cache[A,B](default: A=>B) extends (A=>B) { 9 | val cachedValues = mutable.Map.empty[A,B] 10 | def apply(key: A): B = cachedValues.getOrElseUpdate(key, default(key)) 11 | def get(key: A): Option[B] = cachedValues.get(key) 12 | def update(key: A, value: B) = cachedValues(key) = value 13 | } 14 | 15 | class CacheManager[A,B](world: World, default: (Option[String])=>A=>B) { 16 | val caches = mutable.Map.empty[Option[String], Cache[A,B]] 17 | def apply(variable: Option[String] = None): Cache[A,B] = caches.getOrElseUpdate(variable, { 18 | variable map { varName => 19 | world.addWatcher(varName, new CacheClearingWatcher(new WeakReference[mutable.Map[Option[String], ?]](caches))) 20 | } 21 | new Cache[A,B](default(variable)) 22 | }) 23 | } 24 | 25 | object CacheManager { 26 | def apply[A,B](world: World): CacheManager[A,B] = 27 | CacheManager(world, (v: Option[String]) => (x: A) => throw new java.util.NoSuchElementException("key not found: " + x.toString)) 28 | def apply[A,B](world: World, default: (Option[String])=>(A)=>B): CacheManager[A,B] = 29 | new CacheManager[A,B](world, default) 30 | } 31 | 32 | /* 33 | The reference to the caches must be weak since the watcher may outlive the graph context (e.g. a clear-all will cause 34 | this). - BCH 5/2/2014 35 | */ 36 | private class CacheClearingWatcher(caches: WeakReference[mutable.Map[Option[String], ?]]) extends VariableWatcher { 37 | def update(agent: Agent, variableName: String, value: AnyRef) = { 38 | caches.get.foreach { _.remove(Some(variableName)) } 39 | agent.world.deleteWatcher(variableName, this) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/org/nlogo/extensions/nw/ClusteringTests.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw 2 | 3 | import org.nlogo.extensions.nw.algorithms.Louvain.CommunityStructure.Community 4 | import org.nlogo.extensions.nw.algorithms.Louvain.CommunityStructure 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import org.nlogo.extensions.nw.algorithms.Louvain 7 | 8 | class ClusteringTestSuite extends AnyFunSuite { 9 | test("merged graphs count weights correctly for mixed multi-graph with self-links") { 10 | val graph = MixedMultiGraph(Seq( 11 | (0, 1, false), 12 | 13 | (0, 2, true), // community link 0 -> 1 14 | 15 | (2, 3, false), 16 | (2, 3, true), 17 | 18 | (2, 4, false), // community link 1 <-> 2 19 | 20 | (4, 5, false), 21 | (4, 5, false), 22 | 23 | (5, 6, true), // community link 2 -> 3 24 | (5, 6, true), 25 | 26 | (6, 6, false), 27 | 28 | (6, 7, true), // community link 3 -> 4 29 | (6, 7, false), 30 | 31 | (7, 7, true) 32 | )) 33 | 34 | val cs = Seq(Seq(0,1), Seq(2,3), Seq(4,5), Seq(6), Seq(7)) 35 | val comStruct = CommunityStructure(graph, cs) 36 | 37 | val mGraph = Louvain.MergedGraph(graph, comStruct) 38 | 39 | 40 | def weight(v1: Community[Int], v2: Community[Int]) = mGraph.links.filter { 41 | l => l.end1 == v1 && l.end2 == v2 42 | }.head.weight 43 | 44 | assert(mGraph.links.size === 11) 45 | 46 | // self-links 47 | assert(weight(0, 0) === 2) 48 | assert(weight(1, 1) === 3) 49 | assert(weight(2, 2) === 4) 50 | assert(weight(3, 3) === 2) 51 | assert(weight(4, 4) === 1) 52 | 53 | // between-community links 54 | assert(weight(0, 1) === 1) 55 | assert(weight(1, 2) === 1) 56 | assert(weight(2, 1) === 1) 57 | assert(weight(2, 3) === 2) 58 | assert(weight(3, 4) === 2) 59 | assert(weight(4, 3) === 1) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/MonitoredAgentSets.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.agent.{ Agent, AgentSet, ArrayAgentSet, Link, TreeAgentSet, Turtle, World } 6 | import org.nlogo.api.SimpleChangeEvent 7 | import org.nlogo.core.Listener 8 | 9 | class AgentSetChangeSubscriber(agentSet: TreeAgentSet, onNotify: () => Unit) 10 | extends Listener[SimpleChangeEvent.type] { 11 | agentSet.simpleChangeEventPublisher.subscribe(this) 12 | def unsubscribe(): Unit = agentSet.simpleChangeEventPublisher.unsubscribe(this) 13 | override def handle(e: SimpleChangeEvent.type): Unit = { 14 | onNotify.apply() 15 | } 16 | } 17 | 18 | trait MonitoredAgentSet[A <: Agent] { 19 | def hasChanged: Boolean 20 | val agentSet: AgentSet 21 | } 22 | 23 | trait MonitoredTreeAgentSet[A <: Agent] extends MonitoredAgentSet[A] { 24 | override val agentSet: TreeAgentSet 25 | def world : World 26 | val breedName = agentSet.printName 27 | var hasChanged = false 28 | protected val changeSubscriber = new AgentSetChangeSubscriber(agentSet, () => hasChanged = true) 29 | def unsubscribe(): Unit = changeSubscriber.unsubscribe() 30 | } 31 | 32 | class MonitoredTurtleTreeAgentSet(override val agentSet: TreeAgentSet, val world: World) 33 | extends MonitoredTreeAgentSet[Turtle] 34 | 35 | class MonitoredLinkTreeAgentSet(override val agentSet: TreeAgentSet, val world: World) 36 | extends MonitoredTreeAgentSet[Link] 37 | 38 | trait MonitoredArrayAgentSet[A <: Agent] extends MonitoredAgentSet[A] { 39 | val agentSet: ArrayAgentSet 40 | val count = agentSet.count 41 | 42 | def hasChanged: Boolean = count != agentSet.count 43 | } 44 | 45 | class MonitoredTurtleArrayAgentSet(override val agentSet: ArrayAgentSet) 46 | extends MonitoredArrayAgentSet[Turtle] 47 | 48 | class MonitoredLinkArrayAgentSet(override val agentSet: ArrayAgentSet) 49 | extends MonitoredArrayAgentSet[Link] 50 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/WattsStrogatzGenerator.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.agent 4 | import org.nlogo.agent.{ AgentSet, World, Turtle } 5 | import org.nlogo.api.MersenneTwisterFast 6 | import scala.collection.mutable 7 | 8 | object WattsStrogatzGenerator { 9 | def generate( 10 | world: World, 11 | turtleBreed: AgentSet, 12 | linkBreed: AgentSet, 13 | nbTurtles: Int, 14 | neighborhoodSize: Int, 15 | rewireProbability: Double, 16 | rng: MersenneTwisterFast): Seq[agent.Turtle] = { 17 | 18 | val turtles = (0 until nbTurtles).map { i => 19 | val t = world.createTurtle(turtleBreed) 20 | t.colorDouble((i % 14) * 10.0 + 5.0) 21 | t.heading((360.0 * i) / nbTurtles) 22 | t 23 | } 24 | val availBuffer = turtles.toArray 25 | 26 | val adjMap: Map[Turtle, mutable.Set[Turtle]] = turtles.zipWithIndex.map { case (t: Turtle, i: Int) => 27 | val targets: mutable.Set[Turtle] = (1 to neighborhoodSize).map(j => turtles((i + j) % nbTurtles)).to(mutable.Set) 28 | t -> targets 29 | }.toMap 30 | 31 | for { 32 | (source, i) <- turtles.zipWithIndex 33 | neighbor <- 1 to neighborhoodSize 34 | } { 35 | val target = turtles((i + neighbor) % nbTurtles) 36 | val realTarget = if (rng.nextDouble < rewireProbability) { 37 | adjMap(source).remove(target) 38 | 39 | val newTarget = Iterator.from(0).map { i => 40 | val j = rng.nextInt(availBuffer.size - i) + i 41 | val t = availBuffer(j) 42 | availBuffer(j) = availBuffer(i) 43 | availBuffer(i) = t 44 | t 45 | }.dropWhile { 46 | t => t == source || adjMap(source).contains(t) || adjMap(t).contains(source) 47 | }.next() 48 | 49 | adjMap(source).add(newTarget) 50 | newTarget 51 | } else target 52 | world.linkManager.createLink(source, realTarget, linkBreed) 53 | } 54 | turtles 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jung/Generators.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jung 4 | 5 | import org.nlogo.api 6 | import org.nlogo.core.Syntax._ 7 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 8 | import org.nlogo.extensions.nw.NetworkExtensionUtil.TurtleCreatingCommand 9 | import org.nlogo.extensions.nw.jung.Generator 10 | import org.nlogo.agent 11 | 12 | class KleinbergSmallWorldGenerator extends TurtleCreatingCommand { 13 | override def getSyntax = commandSyntax( 14 | List(TurtlesetType, LinksetType, NumberType, NumberType, NumberType, BooleanType, CommandBlockType | OptionalType), 15 | blockAgentClassString = Some("-T--")) 16 | def createTurtles(args: Array[api.Argument], context: api.Context) = { 17 | implicit val world = context.world.asInstanceOf[agent.World] 18 | new Generator( 19 | turtleBreed = args(0).getAgentSet.requireTurtleBreed, 20 | linkBreed = args(1).getAgentSet.requireLinkBreed, 21 | world = world) 22 | .kleinbergSmallWorld( 23 | rowCount = getIntValueWithMinimum(args(2), 2, "rows"), 24 | colCount = getIntValueWithMinimum(args(3), 2, "columns"), 25 | clusteringExponent = args(4).getDoubleValue, 26 | isToroidal = args(5).getBooleanValue, 27 | rng = context.getRNG) 28 | } 29 | } 30 | 31 | class Lattice2DGenerator extends TurtleCreatingCommand { 32 | override def getSyntax = commandSyntax( 33 | List(TurtlesetType, LinksetType, NumberType, NumberType, BooleanType, CommandBlockType | OptionalType), 34 | blockAgentClassString = Some("-T--")) 35 | def createTurtles(args: Array[api.Argument], context: api.Context) = { 36 | implicit val world = context.world.asInstanceOf[agent.World] 37 | new Generator( 38 | turtleBreed = args(0).getAgentSet.requireTurtleBreed, 39 | linkBreed = args(1).getAgentSet.requireLinkBreed, 40 | world = world) 41 | .lattice2D( 42 | rowCount = getIntValueWithMinimum(args(2), 2, "rows"), 43 | colCount = getIntValueWithMinimum(args(3), 2, "columns"), 44 | isToroidal = args(4).getBooleanValue, 45 | rng = context.getRNG) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/Clustering.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.prim 2 | 3 | import org.nlogo.agent.Turtle 4 | import org.nlogo.{agent, api} 5 | import org.nlogo.api.{AgentSet, ExtensionException, ScalaConversions, TypeNames} 6 | import org.nlogo.core.Syntax._ 7 | import org.nlogo.core.{AgentKind, LogoList} 8 | import org.nlogo.extensions.nw.GraphContextProvider 9 | import org.nlogo.extensions.nw.algorithms.{ClusteringMetrics, Louvain} 10 | import org.nlogo.extensions.nw.util.TurtleSetsConverters.toTurtleSet 11 | 12 | import scala.jdk.CollectionConverters.IterableHasAsScala 13 | 14 | class ClusteringCoefficient(gcp: GraphContextProvider) extends api.Reporter { 15 | override def getSyntax = reporterSyntax(ret = NumberType, agentClassString = "-T--") 16 | override def report(args: Array[api.Argument], context: api.Context) = { 17 | val graph = gcp.getGraphContext(context.getAgent.world) 18 | ClusteringMetrics.clusteringCoefficient(graph, context.getAgent.asInstanceOf[agent.Turtle]): java.lang.Double 19 | } 20 | } 21 | 22 | class Modularity(gcp: GraphContextProvider) extends api.Reporter { 23 | override def getSyntax = reporterSyntax(right = List(ListType), ret = NumberType) 24 | override def report(args: Array[api.Argument], context: api.Context) = { 25 | val graph = gcp.getGraphContext(context.getAgent.world) 26 | val communities: Iterable[Set[Turtle]] = args(0).getList.toVector.map { 27 | case set: AgentSet if set.kind == AgentKind.Turtle => 28 | set.agents.asScala.map(_.asInstanceOf[Turtle]).toSet 29 | case x: AnyRef => throw new ExtensionException( 30 | s"Expected the items of this list to be turtlesets, but got a ${TypeNames.name(x)}." 31 | ) 32 | } 33 | ScalaConversions.toLogoObject(ClusteringMetrics.modularity(graph, communities)) 34 | } 35 | } 36 | 37 | class LouvainCommunities(gcp: GraphContextProvider) extends api.Reporter { 38 | override def getSyntax = reporterSyntax(ret = ListType) 39 | override def report(args: Array[api.Argument], context: api.Context): LogoList = { 40 | val graph = gcp.getGraphContext(context.getAgent.world) 41 | ScalaConversions.toLogoList(Louvain.cluster(graph, context.getRNG).map(toTurtleSet)) 42 | } 43 | } 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jgrapht/Generators.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jgrapht 4 | 5 | import java.util.Random 6 | 7 | import org.jgrapht.VertexFactory 8 | import org.jgrapht.generate.GraphGenerator 9 | import org.jgrapht.generate.RingGraphGenerator 10 | import org.jgrapht.generate.StarGraphGenerator 11 | import org.jgrapht.generate.WheelGraphGenerator 12 | import org.nlogo.agent.AgentSet 13 | import org.nlogo.agent.World 14 | 15 | import org.jgrapht 16 | 17 | import scala.jdk.CollectionConverters.SetHasAsScala 18 | 19 | class Vertex 20 | class Edge 21 | class Generator(turtleBreed: AgentSet, linkBreed: AgentSet, world: World) { 22 | 23 | private object vertexFactory extends VertexFactory[Vertex] { 24 | override def createVertex = new Vertex 25 | } 26 | 27 | private def newGraph = 28 | if (linkBreed.isDirected) 29 | new jgrapht.graph.SimpleDirectedGraph[Vertex, Edge](classOf[Edge]) 30 | else 31 | new jgrapht.graph.SimpleGraph[Vertex, Edge](classOf[Edge]) 32 | 33 | private def importToNetLogo(graph: org.jgrapht.Graph[Vertex, Edge], rng: Random) = { 34 | val m = graph.vertexSet.asScala.map { v => 35 | v -> world.createTurtle( 36 | turtleBreed, 37 | rng.nextInt(14), // color 38 | rng.nextInt(360)) // heading 39 | }.toMap 40 | graph.edgeSet.asScala 41 | .foreach { edge => 42 | world.linkManager.createLink( 43 | m(graph.getEdgeSource(edge)), 44 | m(graph.getEdgeTarget(edge)), 45 | linkBreed) 46 | } 47 | m.valuesIterator // return turtles 48 | } 49 | 50 | private def resultMap[T] = new java.util.HashMap[String, T]() 51 | 52 | private def generate[T](generator: GraphGenerator[Vertex, Edge, T], rng: Random) = { 53 | val g = newGraph 54 | generator.generateGraph(g, vertexFactory, resultMap[T]) 55 | importToNetLogo(g, rng) 56 | } 57 | 58 | def ringGraphGenerator(size: Int, rng: Random) = 59 | generate[Vertex](new RingGraphGenerator(size), rng) 60 | 61 | def wheelGraphGenerator(size: Int, inwardSpokes: Boolean, rng: Random) = 62 | generate[Vertex](new WheelGraphGenerator(size, inwardSpokes), rng) 63 | 64 | def starGraphGenerator(size: Int, rng: Random) = 65 | generate[Vertex](new StarGraphGenerator(size), rng) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/io/Matrix.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung.io 4 | 5 | import java.io.FileNotFoundException 6 | 7 | import org.nlogo.agent.AgentSet 8 | import org.nlogo.agent.Link 9 | import org.nlogo.agent.Turtle 10 | import org.nlogo.agent.World 11 | import org.nlogo.api.ExtensionException 12 | import org.nlogo.extensions.nw.jung.DummyGraph 13 | import org.nlogo.extensions.nw.jung.factoryFor 14 | import org.nlogo.extensions.nw.NetworkExtensionUtil.using 15 | import org.nlogo.api.MersenneTwisterFast 16 | 17 | import edu.uci.ics.jung 18 | import edu.uci.ics.jung.algorithms.matrix.GraphMatrixOperations 19 | 20 | object Matrix { 21 | 22 | def save(graph: jung.graph.Graph[Turtle, Link], filename: String): Unit = { 23 | /* This is almost a line for line copy of jung.io.MatrixFile.save, the major 24 | * difference being that it explicitly uses the US locale to make sure entries 25 | * use the dot decimal separator (see issue #69) */ 26 | try { 27 | using(new java.io.FileWriter(filename)) { writer => 28 | val matrix = GraphMatrixOperations.graphToSparseMatrix(graph, null) // TODO: provide weights 29 | for (i <- 0 until matrix.rows) { 30 | for (j <- 0 until matrix.columns) { 31 | val w = matrix.getQuick(i, j) 32 | writer.write("%4.2f".formatLocal(java.util.Locale.US, w)) 33 | if (j < matrix.columns - 1) writer.write(" ") 34 | } 35 | writer.write("\n") 36 | } 37 | } 38 | } catch { 39 | case e: Exception => throw new ExtensionException(e) 40 | } 41 | } 42 | 43 | def load(filename: String, turtleBreed: AgentSet, linkBreed: AgentSet, world: World, rng: MersenneTwisterFast) = { 44 | val matrixFile = new jung.io.MatrixFile( 45 | null, // TODO: provide weight key (null means 1) (issue #19) 46 | factoryFor(linkBreed), DummyGraph.vertexFactory, DummyGraph.edgeFactory) 47 | val graph = try { 48 | matrixFile.load(filename) 49 | } catch { 50 | case e: Exception => e.getCause match { 51 | case fileNotFound: FileNotFoundException => 52 | throw new ExtensionException(fileNotFound) 53 | case _ => 54 | throw new ExtensionException(e) 55 | } 56 | } 57 | DummyGraph.importToNetLogo(graph, world, turtleBreed, linkBreed, rng) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/io/GraphMLWriterWithAttribType.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung.io 4 | 5 | import java.io.BufferedWriter 6 | import java.io.IOException 7 | 8 | import org.apache.commons.collections15.Transformer 9 | 10 | import edu.uci.ics.jung.io.GraphMLMetadata 11 | import edu.uci.ics.jung.io.GraphMLWriter 12 | 13 | /** 14 | * Subclass of jung.io.GraphMLWriter that, 15 | * unlike Jung's, writes out attrib.type. 16 | * NP 2013-06-21 17 | */ 18 | class GraphMLWriterWithAttribType[V, E] extends GraphMLWriter[V, E] { 19 | 20 | // map indexed by concatenation of "node" or "edge" with attribute id 21 | val attribTypes = collection.mutable.Map[String, String]() 22 | 23 | def addVertexData(id: String, description: String, default_value: String, 24 | attrType: String, vertex_transformer: Transformer[V, String]): Unit = { 25 | super.addVertexData(id, description, default_value, vertex_transformer) 26 | attribTypes += ("node" + id) -> attrType 27 | } 28 | 29 | def addEdgeData(id: String, description: String, default_value: String, 30 | attrType: String, edge_transformer: Transformer[E, String]): Unit = { 31 | super.addEdgeData(id, description, default_value, edge_transformer) 32 | attribTypes += ("edge" + id) -> attrType 33 | } 34 | 35 | /** 36 | * This is a direct translation of super.writeKeySpecification 37 | * except for the writing of attrib.type if appropriate 38 | */ 39 | @throws(classOf[IOException]) 40 | override def writeKeySpecification( 41 | key: String, `type`: String, 42 | ds: GraphMLMetadata[?], bw: BufferedWriter): Unit = { 43 | 44 | bw.write("\n") 56 | closed = true 57 | } 58 | bw.write("" + desc + "\n") 59 | } 60 | // write out default if any 61 | val `def`: Any = ds.default_value 62 | if (`def` != null) { 63 | if (!closed) { 64 | bw.write(">\n") 65 | closed = true 66 | } 67 | bw.write("" + `def`.toString() + "\n") 68 | } 69 | if (!closed) 70 | bw.write("/>\n") 71 | else 72 | bw.write("\n") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/SetContext.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim 4 | 5 | import org.nlogo.agent.AgentSet 6 | import org.nlogo.{ api, agent } 7 | import org.nlogo.api.Argument 8 | import org.nlogo.api.Context 9 | import org.nlogo.core.Syntax._ 10 | import org.nlogo.core.LogoList 11 | import org.nlogo.extensions.nw.GraphContext 12 | import org.nlogo.extensions.nw.GraphContextManager 13 | import org.nlogo.extensions.nw.GraphContextProvider 14 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 15 | import org.nlogo.nvm.AssemblerAssistant 16 | import org.nlogo.nvm.CustomAssembled 17 | import org.nlogo.nvm.ExtensionContext 18 | 19 | class SetContext(gcm: GraphContextManager) 20 | extends api.Command { 21 | override def getSyntax = commandSyntax( 22 | right = List(AgentsetType, AgentsetType)) 23 | override def perform(args: Array[api.Argument], context: api.Context): Unit = { 24 | implicit val world = context.world.asInstanceOf[agent.World] 25 | val turtleSet = args(0).getAgentSet.requireTurtleSet 26 | val linkSet = args(1).getAgentSet.requireLinkSet 27 | val gc = new GraphContext(world, turtleSet, linkSet) 28 | gcm.setGraphContext(gc) 29 | } 30 | } 31 | 32 | class GetContext(gcp: GraphContextProvider) 33 | extends api.Reporter { 34 | override def getSyntax = reporterSyntax(ret = ListType) 35 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 36 | val gc = gcp.getGraphContext(context.world.asInstanceOf[org.nlogo.agent.World]) 37 | LogoList(gc.turtleSet, gc.linkSet) 38 | } 39 | } 40 | 41 | class WithContext(gcp: GraphContextProvider) 42 | extends api.Command 43 | with CustomAssembled { 44 | override def getSyntax = commandSyntax( 45 | right = List(AgentsetType, AgentsetType, CommandBlockType)) 46 | 47 | def perform(args: Array[Argument], context: Context): Unit = { 48 | implicit val world = context.world.asInstanceOf[agent.World] 49 | val turtleSet = args(0).getAgentSet.requireTurtleSet 50 | val linkSet = args(1).getAgentSet.requireLinkSet 51 | val gc = new GraphContext(world, turtleSet, linkSet) 52 | val extContext = context.asInstanceOf[ExtensionContext] 53 | val nvmContext = extContext.nvmContext 54 | // Note that this can optimized by hanging onto the array and just mutating it. Shouldn't be necessary though. 55 | val agentSet = AgentSet.fromAgent(nvmContext.agent) 56 | gcp.withTempGraphContext(gc) { () => 57 | nvmContext.runExclusiveJob(agentSet, nvmContext.ip + 1) 58 | } 59 | } 60 | 61 | def assemble(a: AssemblerAssistant): Unit = { 62 | a.block() 63 | a.done() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jung/Centrality.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jung 4 | 5 | import org.nlogo.api 6 | import org.nlogo.core.Syntax._ 7 | import org.nlogo.agent 8 | import org.nlogo.agent.Agent 9 | import org.nlogo.extensions.nw.GraphContextProvider 10 | import org.nlogo.extensions.nw.NetworkExtensionUtil.canonocilizeVar 11 | 12 | class BetweennessCentrality(gcp:GraphContextProvider) extends api.Reporter { 13 | override def getSyntax = reporterSyntax(ret = NumberType, agentClassString = "-T-L") 14 | override def report(args: Array[api.Argument], context: api.Context) = { 15 | val graph = gcp.getGraphContext(context.getAgent.world).asJungGraph 16 | graph.betweennessCentrality(context.getAgent.asInstanceOf[Agent]): java.lang.Double 17 | } 18 | } 19 | 20 | class WeightedBetweennessCentrality(gcp: GraphContextProvider) extends api.Reporter { 21 | override def getSyntax = reporterSyntax(right = List(StringType | SymbolType), ret = NumberType, agentClassString = "-T-L") 22 | override def report(args: Array[api.Argument], context: api.Context) = { 23 | val graph = gcp.getGraphContext(context.getAgent.world).asJungGraph 24 | val weightVar = canonocilizeVar(args(0).get) 25 | graph.betweennessCentrality(context.getAgent.asInstanceOf[Agent], weightVar): java.lang.Double 26 | } 27 | } 28 | 29 | class PageRank(gcp: GraphContextProvider) extends api.Reporter { 30 | override def getSyntax = reporterSyntax(ret = NumberType, agentClassString = "-T--") 31 | override def report(args: Array[api.Argument], context: api.Context) = { 32 | val graph = gcp.getGraphContext(context.getAgent.world).asUndirectedJungGraph 33 | graph.PageRank.getScore(context.getAgent.asInstanceOf[agent.Turtle]).asInstanceOf[java.lang.Double] 34 | } 35 | } 36 | 37 | class ClosenessCentrality(gcp: GraphContextProvider) extends api.Reporter { 38 | override def getSyntax = reporterSyntax(ret = NumberType, agentClassString = "-T--") 39 | override def report(args: Array[api.Argument], context: api.Context) = { 40 | val graph = gcp.getGraphContext(context.getAgent.world).asJungGraph 41 | graph.closenessCentrality(context.getAgent.asInstanceOf[agent.Turtle]): java.lang.Double 42 | } 43 | } 44 | 45 | class WeightedClosenessCentrality(gcp: GraphContextProvider) extends api.Reporter { 46 | override def getSyntax = reporterSyntax(right = List(StringType | SymbolType), ret = NumberType, agentClassString = "-T--") 47 | override def report(args: Array[api.Argument], context: api.Context) = { 48 | val graph = gcp.getGraphContext(context.getAgent.world).asJungGraph 49 | val varName = canonocilizeVar(args(0).get) 50 | graph.closenessCentrality(context.getAgent.asInstanceOf[agent.Turtle], varName): java.lang.Double 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/Graph.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw 2 | 3 | trait Graph[V, E] { 4 | 5 | def nodes: Iterable[V] 6 | def links: Iterable[E] = nodes.flatMap(outEdges).toSet 7 | 8 | /** 9 | * Returns the number of directed arcs, where an undirected link count as 10 | * two arcs. Thus, this will be equal to `links.size` for directed networks 11 | * and `links.size * 2` for undirected networks. For mixed networks, this 12 | * will be somewhere in between. 13 | * In other words, this is the sum of the elements of the adjacency matrix 14 | * of the network. 15 | */ 16 | lazy val arcCount: Int = nodes.view.map(outEdges(_).size).sum 17 | lazy val totalArcWeight: Double = nodes.view.flatMap(outEdges).map(weight).sum 18 | 19 | def otherEnd(node: V)(link: E): V = { 20 | val (end1, end2) = ends(link) 21 | if (end2 == node) end1 else end2 22 | } 23 | 24 | def ends(link: E): (V, V) 25 | 26 | /** 27 | * Should return both incoming directed and undirected edges. 28 | */ 29 | def inEdges(node: V): Seq[E] = nodes.view.flatMap(v => outEdges(v) filter (otherEnd(v)(_) == node)).toSeq 30 | /** 31 | * Should return both incoming directed and undirected neighbors. 32 | */ 33 | def inNeighbors(node: V): Seq[V] = inEdges(node) map otherEnd(node) 34 | 35 | /** 36 | * Should return both outgoing directed and undirected edges. 37 | */ 38 | def outEdges(node: V): Seq[E] 39 | /** 40 | * Should return both outgoing directed and undirected neighbors. 41 | */ 42 | def outNeighbors(node: V): Seq[V] = outEdges(node) map otherEnd(node) 43 | 44 | def allEdges(node: V): Seq[E] = (inEdges(node) ++ outEdges(node)).distinct 45 | def allNeighbors(node: V): Seq[V] = allEdges(node) map otherEnd(node) 46 | 47 | def weight(link: E): Double = 1.0 48 | } 49 | 50 | case class MixedMultiGraph[V](override val links: Seq[(V, V, Boolean)]) extends Graph[V, (V,V,Boolean)] { 51 | 52 | override val nodes: Seq[V] = (links.map(_._1) ++ links.map(_._2)).distinct 53 | val undirLinks: Map[V, Seq[(V,V,Boolean)]] = { 54 | val undirLinks = links.filterNot(_._3) 55 | val outUndirLinks = undirLinks.groupBy(_._1) 56 | val inUndirLinks = undirLinks.groupBy(_._2) 57 | nodes.map { node => node -> 58 | (outUndirLinks.getOrElse(node, Seq.empty[(V,V,Boolean)]) 59 | ++ inUndirLinks.getOrElse(node, Seq.empty[(V,V,Boolean)])) 60 | }.toMap 61 | } 62 | val outLinks: Map[V, Seq[(V,V,Boolean)]] = 63 | links.filter(_._3).groupBy(_._1) withDefaultValue Seq.empty[(V,V,Boolean)] 64 | lazy val inLinks: Map[V, Seq[(V,V,Boolean)]] = 65 | links.filter(_._3).groupBy(_._2) withDefaultValue Seq.empty[(V,V,Boolean)] 66 | 67 | override def outEdges(node: V): Seq[(V,V,Boolean)] = undirLinks(node) ++ outLinks(node) 68 | override def inEdges(node: V): Seq[(V,V,Boolean)] = undirLinks(node) ++ outLinks(node) 69 | 70 | override def ends(link: (V,V,Boolean)) = (link._1, link._2) 71 | } 72 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/jgrapht/Generators.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim.jgrapht 4 | 5 | import org.nlogo.{ api, agent } 6 | import org.nlogo.core.Syntax._ 7 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 8 | import org.nlogo.extensions.nw.NetworkExtensionUtil.TurtleCreatingCommand 9 | import org.nlogo.extensions.nw.jgrapht.Generator 10 | 11 | trait SimpleGeneratorPrim 12 | extends TurtleCreatingCommand { 13 | override def getSyntax = commandSyntax( 14 | List(TurtlesetType, LinksetType, NumberType, CommandBlockType | OptionalType), 15 | blockAgentClassString = Some("-T--")) 16 | def turtleBreed(args: Array[api.Argument])(implicit world: agent.World) = { 17 | args(0).getAgentSet.requireTurtleBreed 18 | } 19 | def linkBreed(args: Array[api.Argument])(implicit world: agent.World) = { 20 | args(1).getAgentSet.requireLinkBreed 21 | } 22 | def generator(args: Array[api.Argument], context: api.Context) = { 23 | implicit val world = context.world.asInstanceOf[agent.World] 24 | new Generator(turtleBreed(args), linkBreed(args), world) 25 | } 26 | } 27 | 28 | trait SimpleUndirectedGeneratorPrim extends SimpleGeneratorPrim { 29 | override def linkBreed(args: Array[api.Argument])(implicit world: agent.World) = 30 | args(1).getAgentSet.requireUndirectedLinkBreed 31 | } 32 | 33 | trait SimpleDirectedGeneratorPrim extends SimpleGeneratorPrim { 34 | override def linkBreed(args: Array[api.Argument])(implicit world: agent.World) = 35 | args(1).getAgentSet.requireDirectedLinkBreed 36 | } 37 | 38 | class RingGenerator extends SimpleGeneratorPrim { 39 | def createTurtles(args: Array[api.Argument], context: api.Context) = 40 | generator(args, context) 41 | .ringGraphGenerator(getIntValueWithMinimum(args(2), 3), context.getRNG) 42 | } 43 | 44 | class StarGenerator extends SimpleGeneratorPrim { 45 | def createTurtles(args: Array[api.Argument], context: api.Context) = 46 | generator(args, context) 47 | .starGraphGenerator(getIntValueWithMinimum(args(2), 1), context.getRNG) 48 | } 49 | 50 | class WheelGenerator extends SimpleUndirectedGeneratorPrim { 51 | def createTurtles(args: Array[api.Argument], context: api.Context) = 52 | generator(args, context) 53 | .wheelGraphGenerator(getIntValueWithMinimum(args(2), 4), true, context.getRNG) 54 | } 55 | 56 | class WheelGeneratorInward extends SimpleDirectedGeneratorPrim { 57 | def createTurtles(args: Array[api.Argument], context: api.Context) = 58 | generator(args, context) 59 | .wheelGraphGenerator(getIntValueWithMinimum(args(2), 4), true, context.getRNG) 60 | } 61 | 62 | class WheelGeneratorOutward extends SimpleDirectedGeneratorPrim { 63 | def createTurtles(args: Array[api.Argument], context: api.Context) = 64 | generator(args, context) 65 | .wheelGraphGenerator(getIntValueWithMinimum(args(2), 4), false, context.getRNG) 66 | } 67 | -------------------------------------------------------------------------------- /test/exclude-dangling-link.graphml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 0 31 | false 32 | 0 33 | 1 34 | 0 35 | default 36 | 9.9 37 | 1 38 | up 39 | 5 40 | 41 | mice 42 | 0 43 | 44 | 45 | 0 46 | false 47 | 0 48 | 1 49 | 180 50 | default 51 | 9.9 52 | 1 53 | up 54 | 15 55 | 56 | mice 57 | 1 58 | 59 | 60 | default 61 | 9.9 62 | false 63 | (mouse 0) 64 | 5 65 | none 66 | (mouse 1) 67 | 68 | links 69 | 0 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /test/gexf/full.gexf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gephi 0.8 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/Generators.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim 4 | 5 | import org.nlogo.api 6 | import org.nlogo.api.{Argument, Context, ExtensionException} 7 | import org.nlogo.agent.{Turtle, World} 8 | import org.nlogo.core.Syntax 9 | import org.nlogo.core.Syntax._ 10 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 11 | import org.nlogo.extensions.nw.NetworkExtensionUtil.TurtleCreatingCommand 12 | import org.nlogo.extensions.nw.algorithms 13 | 14 | class ErdosRenyiGenerator extends TurtleCreatingCommand { 15 | override def getSyntax: Syntax = commandSyntax( 16 | List(TurtlesetType, LinksetType, NumberType, NumberType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 17 | def createTurtles(args: Array[api.Argument], context: api.Context): Seq[Turtle] = { 18 | implicit val world: World = context.world.asInstanceOf[World] 19 | val turtleBreed = args(0).getAgentSet.requireTurtleBreed 20 | val linkBreed = args(1).getAgentSet.requireLinkBreed 21 | val nbTurtles = getIntValueWithMinimum(args(2), 1) 22 | val connexionProbability = args(3).getDoubleValue 23 | if (!(connexionProbability >= 0 && connexionProbability <= 1.0)) 24 | throw new ExtensionException("The connexion probability must be between 0 and 1.") 25 | algorithms.ErdosRenyiGenerator.generate(world, turtleBreed, linkBreed, nbTurtles, connexionProbability, context.getRNG) 26 | } 27 | } 28 | 29 | class WattsStrogatzGenerator extends TurtleCreatingCommand { 30 | override def getSyntax: Syntax = commandSyntax( 31 | List(TurtlesetType, LinksetType, NumberType, NumberType, NumberType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 32 | def createTurtles(args: Array[api.Argument], context: api.Context): Seq[Turtle] = { 33 | implicit val world: World = context.world.asInstanceOf[World] 34 | val turtleBreed = args(0).getAgentSet.requireTurtleBreed 35 | val linkBreed = args(1).getAgentSet.requireLinkBreed 36 | val nbTurtles = getIntValueWithMinimum(args(2), 1) 37 | val neighborhoodSize = args(3).getIntValue 38 | if (neighborhoodSize < 0 || neighborhoodSize > (nbTurtles / 2.0 - 1.0).ceil) 39 | throw new ExtensionException("Neighborhood size must be less than half the number of turtles.") 40 | val rewireProbability = args(4).getDoubleValue 41 | if (!(rewireProbability >= 0 && rewireProbability <= 1.0)) 42 | throw new ExtensionException("The rewire probability must be between 0 and 1.") 43 | algorithms.WattsStrogatzGenerator.generate(world, turtleBreed, linkBreed, nbTurtles, neighborhoodSize, rewireProbability, context.getRNG) 44 | } 45 | } 46 | 47 | class BarabasiAlbertGenerator extends TurtleCreatingCommand { 48 | override def getSyntax: Syntax = commandSyntax( 49 | List(TurtlesetType, LinksetType, NumberType, NumberType, CommandBlockType | OptionalType), 50 | blockAgentClassString = Some("-T--") 51 | ) 52 | override def createTurtles(args: Array[Argument], context: Context): Seq[Turtle] = { 53 | implicit val world: World = context.world.asInstanceOf[World] 54 | val turtleBreed = args(0).getAgentSet.requireTurtleBreed 55 | val linkBreed = args(1).getAgentSet.requireLinkBreed 56 | val numTurtles = getIntValueWithMinimum(args(2), 1) 57 | val minDegree = getIntValueWithMinimum(args(3), 1) 58 | algorithms.BarabasiAlbertGenerator.generate(world, turtleBreed, linkBreed, numTurtles, minDegree, context.getRNG) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/IO.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.prim 2 | 3 | import java.io.File 4 | 5 | import org.nlogo.agent.World 6 | import org.nlogo.api 7 | import org.nlogo.core.Syntax._ 8 | import org.nlogo.nvm.ExtensionContext 9 | 10 | import org.nlogo.extensions.nw.GraphContextProvider 11 | import org.nlogo.extensions.nw.NetworkExtensionUtil._ 12 | import org.nlogo.extensions.nw.gephi.{ GephiExport, GephiImport, GephiUtils } 13 | 14 | class Load extends TurtleAskingCommand { 15 | override def getSyntax = commandSyntax( 16 | right = List(StringType, TurtlesetType, LinksetType, CommandBlockType | OptionalType), 17 | blockAgentClassString = Some("-T--")) 18 | override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext { 19 | implicit val world = context.world.asInstanceOf[World] 20 | val ws = context.asInstanceOf[ExtensionContext].workspace 21 | val turtleBreed = args(1).getAgentSet.requireTurtleBreed 22 | val linkBreed = args(2).getAgentSet.requireLinkBreed 23 | val file = new File(ws.fileManager.attachPrefix(args(0).getString)) 24 | GephiImport.load(file, world, turtleBreed, linkBreed, askTurtles(context)) 25 | } 26 | } 27 | 28 | class LoadFileType(extension: String) extends TurtleAskingCommand { 29 | override def getSyntax = commandSyntax(right = List(StringType, TurtlesetType, LinksetType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 30 | override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext { 31 | implicit val world = context.world.asInstanceOf[World] 32 | val ws = context.asInstanceOf[ExtensionContext].workspace 33 | val turtleBreed = args(1).getAgentSet.requireTurtleBreed 34 | val linkBreed = args(2).getAgentSet.requireLinkBreed 35 | val file = new File(ws.fileManager.attachPrefix(args(0).getString)) 36 | GephiImport.load(file, world, turtleBreed, linkBreed, askTurtles(context), extension) 37 | } 38 | } 39 | 40 | class LoadFileTypeDefaultBreeds(extension: String) extends TurtleAskingCommand { 41 | override def getSyntax = commandSyntax(right = List(StringType, CommandBlockType | OptionalType), blockAgentClassString = Some("-T--")) 42 | override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext { 43 | val world = context.world.asInstanceOf[World] 44 | val ws = context.asInstanceOf[ExtensionContext].workspace 45 | val file = new File(ws.fileManager.attachPrefix(args(0).getString)) 46 | GephiImport.load(file, world, world.turtles, world.links, askTurtles(context), extension) 47 | } 48 | } 49 | 50 | class Save(gcp: GraphContextProvider) extends api.Command { 51 | override def getSyntax = commandSyntax(right = List(StringType)) 52 | override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext { 53 | val world = context.getAgent.world.asInstanceOf[World] 54 | val workspace = context.asInstanceOf[ExtensionContext].workspace 55 | val fm = workspace.fileManager 56 | val file = new File(fm.attachPrefix(args(0).getString)) 57 | GephiExport.save(gcp.getGraphContext(world), world, file) 58 | } 59 | } 60 | 61 | class SaveFileType(gcp: GraphContextProvider, extension: String) extends api.Command { 62 | override def getSyntax = commandSyntax(right = List(StringType)) 63 | override def perform(args: Array[api.Argument], context: api.Context) = GephiUtils.withNWLoaderContext { 64 | val world = context.getAgent.world.asInstanceOf[World] 65 | val workspace = context.asInstanceOf[ExtensionContext].workspace 66 | val fm = workspace.fileManager 67 | val file = new File(fm.attachPrefix(args(0).getString)) 68 | GephiExport.save(gcp.getGraphContext(world), world, file, extension) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jgrapht/Graphs.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jgrapht 4 | 5 | import org.nlogo.agent.Link 6 | import org.nlogo.agent.Turtle 7 | import org.nlogo.api 8 | import org.nlogo.api.ExtensionException 9 | import org.nlogo.extensions.nw.GraphContext 10 | import org.nlogo.extensions.nw.util.TurtleSetsConverters.toTurtleSets 11 | 12 | import org.jgrapht 13 | 14 | import scala.jdk.CollectionConverters.{ SetHasAsJava, CollectionHasAsScala } 15 | 16 | trait Graph 17 | extends jgrapht.graph.AbstractGraph[Turtle, Link] { 18 | val gc: GraphContext 19 | 20 | override def getAllEdges(sourceVertex: Turtle, targetVertex: Turtle) = 21 | gc.allEdges(sourceVertex).toSet.filter(_.end2 == targetVertex).asJava 22 | 23 | override def getEdge(sourceVertex: Turtle, targetVertex: Turtle) = 24 | gc.outEdges(sourceVertex).find(e => gc.otherEnd(sourceVertex)(e) == targetVertex).orNull 25 | 26 | override def containsEdge(sourceVertex: Turtle, targetVertex: Turtle) = 27 | getEdge(sourceVertex, targetVertex) != null 28 | 29 | override def containsEdge(edge: Link) = gc.outEdges(edge.end1).contains(edge) 30 | override def containsVertex(vertex: Turtle) = gc.nodes.contains(vertex) 31 | 32 | override def edgeSet() = gc.links.toSet.asJava 33 | 34 | override def edgesOf(vertex: Turtle) = gc.allEdges(vertex).toSet.asJava 35 | 36 | override def vertexSet() = gc.nodes.toSet.asJava 37 | 38 | override def getEdgeSource(edge: Link) = edge.end1 39 | override def getEdgeTarget(edge: Link) = edge.end2 40 | override def getEdgeWeight(edge: Link): Double = 1.0 // TODO: figure out how to deal with weigths 41 | 42 | override def getEdgeFactory() = throw sys.error("not implemented") 43 | override def addEdge(sourceVertex: Turtle, targetVertex: Turtle) = throw sys.error("not implemented") 44 | override def addEdge(sourceVertex: Turtle, targetVertex: Turtle, edge: Link) = throw sys.error("not implemented") 45 | override def addVertex(v: Turtle) = throw sys.error("not implemented") 46 | override def removeAllEdges(edges: java.util.Collection[? <: Link]) = throw sys.error("not implemented") 47 | override def removeAllEdges(sourceVertex: Turtle, targetVertex: Turtle) = throw sys.error("not implemented") 48 | override def removeAllVertices(vertices: java.util.Collection[? <: Turtle]) = throw sys.error("not implemented") 49 | override def removeEdge(sourceVertex: Turtle, targetVertex: Turtle) = throw sys.error("not implemented") 50 | override def removeEdge(edge: Link) = throw sys.error("not implemented") 51 | override def removeVertex(vertex: Turtle) = throw sys.error("not implemented") 52 | 53 | object BronKerboschCliqueFinder extends jgrapht.alg.BronKerboschCliqueFinder(this) { 54 | def allCliques(rng: java.util.Random): Seq[api.AgentSet] = 55 | toTurtleSets(getAllMaximalCliques.asScala, rng) 56 | def biggestCliques(rng: java.util.Random): Seq[api.AgentSet] = 57 | toTurtleSets(getBiggestMaximalCliques.asScala, rng) 58 | } 59 | } 60 | 61 | class UndirectedGraph( 62 | override val gc: GraphContext) 63 | extends Graph 64 | with jgrapht.UndirectedGraph[Turtle, Link] { 65 | override def degreeOf(vertex: Turtle) = gc.outEdges(vertex).size 66 | } 67 | 68 | class DirectedGraph( 69 | override val gc: GraphContext) 70 | extends Graph 71 | with jgrapht.DirectedGraph[Turtle, Link] { 72 | if (!gc.isDirected) 73 | throw new ExtensionException("link set must be directed") 74 | 75 | override def incomingEdgesOf(vertex: Turtle) = gc.inEdges(vertex).toSet.asJava 76 | override def inDegreeOf(vertex: Turtle) = gc.inEdges(vertex).size 77 | override def outgoingEdgesOf(vertex: Turtle) = gc.outEdges(vertex).toSet.asJava 78 | override def outDegreeOf(vertex: Turtle) = gc.outEdges(vertex).size 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/Graphs.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung 4 | 5 | import java.util.Collection 6 | import org.nlogo.agent.Link 7 | import org.nlogo.agent.Turtle 8 | import org.nlogo.api.ExtensionException 9 | import org.nlogo.extensions.nw.GraphContext 10 | import edu.uci.ics 11 | import edu.uci.ics.jung.graph.util.EdgeType 12 | import edu.uci.ics.jung.graph.util.Pair 13 | 14 | import scala.jdk.CollectionConverters.IterableHasAsJava 15 | 16 | trait Graph 17 | extends ics.jung.graph.AbstractGraph[Turtle, Link] 18 | with Algorithms { 19 | 20 | val gc: GraphContext 21 | 22 | def edgeType = 23 | if (gc.isDirected) 24 | ics.jung.graph.util.EdgeType.DIRECTED 25 | else 26 | ics.jung.graph.util.EdgeType.UNDIRECTED 27 | 28 | override def getIncidentEdges(turtle: Turtle): Collection[Link] = gc.allEdges(turtle).asJavaCollection 29 | 30 | override def getEdgeCount: Int = gc.linkCount 31 | 32 | override def getNeighbors(turtle: Turtle): Collection[Turtle] = gc.allNeighbors(turtle).asJavaCollection 33 | 34 | override def getVertexCount: Int = gc.turtleCount 35 | override def getVertices: Collection[Turtle] = gc.nodes.asJavaCollection 36 | override def getEdges: Collection[Link] = gc.links.asJavaCollection 37 | override def containsEdge(link: Link): Boolean = gc.outEdges(link.end1).contains(link) 38 | override def containsVertex(turtle: Turtle): Boolean = gc.nodes.contains(turtle) 39 | 40 | def getEndpoints(link: Link): Pair[Turtle] = 41 | new Pair(link.end1, link.end2) // Note: contract says nothing about edge being in graph 42 | 43 | def removeEdge(link: Link): Boolean = 44 | throw sys.error("not implemented") 45 | def removeVertex(turtle: Turtle): Boolean = 46 | throw sys.error("not implemented") 47 | def addVertex(turtle: Turtle): Boolean = 48 | throw sys.error("not implemented") 49 | override def addEdge(link: Link, turtles: Collection[? <: Turtle]): Boolean = 50 | throw sys.error("not implemented") 51 | def addEdge(link: Link, turtles: Pair[? <: Turtle], edgeType: EdgeType): Boolean = 52 | throw sys.error("not implemented") 53 | } 54 | 55 | class DirectedGraph( 56 | override val gc: GraphContext) 57 | extends ics.jung.graph.AbstractTypedGraph[Turtle, Link](EdgeType.DIRECTED) 58 | with Graph 59 | with DirectedAlgorithms 60 | with ics.jung.graph.DirectedGraph[Turtle, Link] { 61 | 62 | if (!gc.isDirected) 63 | throw new ExtensionException("link set must be directed") 64 | 65 | override def getInEdges(turtle: Turtle): Collection[Link] = gc.inEdges(turtle).asJavaCollection 66 | override def getPredecessors(turtle: Turtle): Collection[Turtle] = gc.inNeighbors(turtle).asJavaCollection 67 | 68 | override def getOutEdges(turtle: Turtle): Collection[Link] = gc.outEdges(turtle).asJavaCollection 69 | override def getSuccessors(turtle: Turtle): Collection[Turtle] = gc.outNeighbors(turtle).asJavaCollection 70 | 71 | def isDest(turtle: Turtle, link: Link): Boolean = link.end2 == turtle 72 | def isSource(turtle: Turtle, link: Link): Boolean = link.end1 == turtle 73 | 74 | override def getSource(link: Link): Turtle = link.end1 75 | 76 | override def getDest(link: Link): Turtle = link.end2 77 | 78 | } 79 | 80 | class UndirectedGraph( 81 | override val gc: GraphContext) 82 | extends ics.jung.graph.AbstractTypedGraph[Turtle, Link](EdgeType.UNDIRECTED) 83 | with Graph 84 | with UndirectedAlgorithms 85 | with ics.jung.graph.UndirectedGraph[Turtle, Link] { 86 | 87 | override def getInEdges(turtle: Turtle) = getIncidentEdges(turtle) 88 | override def getPredecessors(turtle: Turtle) = getNeighbors(turtle: Turtle) 89 | 90 | override def getOutEdges(turtle: Turtle) = getIncidentEdges(turtle) 91 | override def getSuccessors(turtle: Turtle) = getNeighbors(turtle: Turtle) 92 | 93 | def isDest(turtle: Turtle, link: Link) = false 94 | def isSource(turtle: Turtle, link: Link) = false 95 | 96 | override def getSource(link: Link): Turtle = null 97 | override def getDest(link: Link): Turtle = null 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/io/GraphMLExport.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung.io 4 | 5 | import java.io.BufferedWriter 6 | import java.io.FileWriter 7 | import java.io.PrintWriter 8 | 9 | import org.apache.commons.collections15.Transformer 10 | import org.nlogo.agent 11 | import org.nlogo.api 12 | import org.nlogo.api.ExtensionException 13 | import org.nlogo.extensions.nw.GraphContext 14 | import org.nlogo.extensions.nw.NetworkExtensionUtil.{ functionToTransformer, using } 15 | 16 | object GraphMLExport { 17 | 18 | def save(graphContext: GraphContext, filename: String) = { 19 | val world = graphContext.world 20 | 21 | val graphMLWriter = new GraphMLWriterWithAttribType[agent.Turtle, agent.Link] 22 | 23 | addImplicitVariables(api.AgentVariables.getImplicitTurtleVariables(false), graphMLWriter.addVertexData) 24 | addImplicitVariables(api.AgentVariables.getImplicitLinkVariables, graphMLWriter.addEdgeData) 25 | 26 | def addImplicitVariables[T <: agent.Agent]( 27 | vars: Iterable[String], 28 | adder: (String, String, String, String, Transformer[T, String]) => Unit): Unit = { 29 | for ((variableName, i) <- vars.zipWithIndex) { 30 | val transformer = (a: T) => api.Dump.logoObject(a.getVariable(i)) 31 | adder(variableName, null, null, "string", transformer) 32 | } 33 | } 34 | 35 | val program = world.program 36 | 37 | val turtles = graphContext.nodes 38 | val links = graphContext.links 39 | 40 | val turtlesOwn = program.turtlesOwn 41 | val linksOwn = program.linksOwn 42 | 43 | addVariables( 44 | turtles, turtlesOwn, 45 | (t: agent.Turtle, v: String) => turtlesOwn.contains(v), 46 | (t: agent.Turtle, v: String) => t.getVariable(turtlesOwn.indexOf(v)), 47 | graphMLWriter.addVertexData) 48 | addVariables( 49 | links, linksOwn, 50 | (t: agent.Link, v: String) => linksOwn.contains(v), 51 | (t: agent.Link, v: String) => t.getVariable(linksOwn.indexOf(v)), 52 | graphMLWriter.addEdgeData) 53 | 54 | addVariables( 55 | turtles, program.breeds.values.flatMap(_.owns), 56 | (t: agent.Turtle, v: String) => world.breedOwns(t.getBreed, v), 57 | (t: agent.Turtle, v: String) => t.getBreedVariable(v), 58 | graphMLWriter.addVertexData) 59 | addVariables( 60 | links, program.linkBreeds.values.flatMap(_.owns), 61 | (l: agent.Link, v: String) => world.linkBreedOwns(l.getBreed, v), 62 | (l: agent.Link, v: String) => l.getLinkBreedVariable(v), 63 | graphMLWriter.addEdgeData) 64 | 65 | def addVariables[T <: agent.Agent]( 66 | agents: Iterable[T], 67 | vars: Iterable[String], 68 | varChecker: (T, String) => Boolean, 69 | varGetter: (T, String) => Object, 70 | adder: (String, String, String, String, Transformer[T, String]) => Unit): Unit = { 71 | for (variableName <- vars) { 72 | val attrType = findAttrType(agents, 73 | varChecker(_: T, variableName), 74 | varGetter(_: T, variableName)) 75 | val transformer = (a: T) => 76 | if (varChecker(a, variableName)) api.Dump.logoObject(varGetter(a, variableName)) 77 | else null 78 | adder(variableName, null, null, attrType, transformer) 79 | } 80 | } 81 | 82 | try { 83 | using(new PrintWriter(new BufferedWriter(new FileWriter(filename)))) { printWriter => 84 | graphMLWriter.save(graphContext.asJungGraph, printWriter) 85 | } 86 | } catch { 87 | case e: Exception => throw new ExtensionException(e) 88 | } 89 | 90 | } 91 | 92 | /** 93 | * Tries to find out the appropriate xml attribute type 94 | * for a variable by looking at its value for the first agent. 95 | * NP 2013-08-08 96 | */ 97 | def findAttrType[T <: agent.Agent]( 98 | agents: Iterable[T], 99 | varChecker: (T) => Boolean, 100 | varGetter: (T) => Object) = { 101 | (for { 102 | agent <- agents.headOption 103 | if varChecker(agent) 104 | } yield varGetter(agent) match { 105 | case _: java.lang.Double => "double" 106 | case _: java.lang.Boolean => "boolean" 107 | case _ => "string" 108 | }).getOrElse("") // there was no agent; "" will result in no attrType being written 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /proguard/lite.txt: -------------------------------------------------------------------------------- 1 | # THIS IS A SPECIAL VERSION OF LITE.TXT 2 | # It keeps more stuff in NetLogoLite.jar, in order for the network extension to work. 3 | # It can be used to produce a special version of the jar by overwriting the equivalent 4 | # file in the NetLogo 5.0.x distribution (in the project/proguard) directory. 5 | 6 | -include common.txt 7 | 8 | # since this file is in the project/proguard directory, we need to get back up to the root 9 | -basedirectory ../.. 10 | 11 | # forcibly exclude the generator. at runtime, the code will notice it is missing 12 | # and do without it. also exclude unneeded resources 13 | -injars target/classes(!org/nlogo/generator/**,!images/shapes-editor/**,!system/libraryShapes.txt,!system/about.txt,!system/library.html,!system/empty.nlogo3d,!system/behaviorspace.dtd,!system/dict3d.txt,!system/dict.txt,!system/info.css,!i18n/**,!images/title.jpg,**) 14 | 15 | # this is an injar, not a library jar, because we're going to stuff the Scala classes 16 | # we need directly into NetLogoLite.jar, so NetLogoLite.jar has no external dependencies 17 | -injars 18 | 19 | -outjar NetLogoLite.jar 20 | 21 | # ProGuard doc recommends keeping Exceptions,InnerClasses,Signature 22 | # for libraries. we keep SourceFile and LineNumberTable so we get 23 | # nice stack traces. but we get significant space savings (10% or so) 24 | # by not keeping ScalaSignature - ST 5/13/11 25 | -keepnames class * { *; } 26 | -keepattributes Exceptions,InnerClasses,Signature,SourceFile,LineNumberTable 27 | 28 | -keep public class org.nlogo.lite.* 29 | 30 | # the Event stuff works by reflection so we need to explicitly include these 31 | -keep public class * extends org.nlogo.window.Event 32 | 33 | # pull in all the prims (but not threed, hubnet, dead) 34 | -keep public class org.nlogo.prim._* { 35 | *; 36 | } 37 | -keep public class org.nlogo.prim.etc.* { 38 | *; 39 | } 40 | -keep public class org.nlogo.prim.gui.* { 41 | *; 42 | } 43 | -keep public class org.nlogo.prim.plot.* { 44 | *; 45 | } 46 | -keep public class org.nlogo.prim.file.* { 47 | *; 48 | } 49 | 50 | # keep Lite (formerly applet) stuff 51 | -keep public class org.nlogo.lite.LitePanel { 52 | *; 53 | } 54 | -keep public class org.nlogo.window.NetLogoListenerManager { 55 | *; 56 | } 57 | 58 | # pull in the extensions API 59 | -keep public class org.nlogo.api.* { 60 | *; 61 | } 62 | -keep public class org.nlogo.nvm.ExtensionContext { 63 | *; 64 | } 65 | -keep public class org.nlogo.nvm.Workspace { 66 | *; 67 | } 68 | -keep public class org.nlogo.nvm.Argument { 69 | *; 70 | } 71 | 72 | # some methods used by extensions 73 | -keep public class org.nlogo.window.GUIWorkspace { 74 | public org.nlogo.window.WidgetContainer getWidgetContainer(); 75 | public void waitFor(java.lang.Runnable); 76 | } 77 | -keep public class org.nlogo.util.Utils { 78 | public static java.lang.String reader2String(java.io.Reader); 79 | } 80 | 81 | # pull in stuff we access only by reflection (for dependency injection) 82 | -keep class org.nlogo.render.Renderer { 83 | Renderer(...); 84 | } 85 | -keep class org.nlogo.lex.Tokenizer?D { 86 | *; 87 | } 88 | -keep class org.nlogo.lex.TokenReader { 89 | *; 90 | } 91 | -keep class org.nlogo.sdm.AggregateManagerLite { 92 | *; 93 | } 94 | -keep class org.nlogo.compiler.Compiler$ { 95 | *; 96 | } 97 | -keep class org.nlogo.job.JobManager { 98 | *; 99 | } 100 | 101 | # Package objects need it 102 | -keep class scala.runtime.EmptyMethodCache { 103 | *; 104 | } 105 | 106 | # Structural types need it 107 | -keep class scala.runtime.ScalaRunTime$ { 108 | public java.lang.reflect.Method ensureAccessible(java.lang.reflect.Method); 109 | } 110 | 111 | # needed or extensions written in Scala won't build 112 | -keep class scala.reflect.ScalaSignature { 113 | *; 114 | } 115 | 116 | # stuff added for the NW extension: 117 | -keep class scala.collection.JavaConverters { *; } 118 | -keep class scala.collection.JavaConverters$AsJavaCollection { *; } 119 | -keep class scala.collection.generic.GenericTraversableTemplate { *; } 120 | -keep class scala.collection.mutable.Map$ { *; } 121 | -keep class scala.Option$WithFilter { *; } 122 | -keep class scala.collection.mutable.MapLike { *; } 123 | -keep class scala.collection.mutable.Set$ { *; } 124 | -keep class scala.Option { *; } 125 | -keep class scala.math.Ordering$Long$ { *; } 126 | -keep class scala.collection.IterableLike { *; } 127 | -keep class scala.collection.TraversableLike { *; } 128 | -keep class scala.package$ { *; } 129 | -keep class scala.collection.mutable.Publisher { *; } 130 | -keep class scala.collection.mutable.Subscriber { *; } 131 | -keep class scala.collection.immutable.Stream$ { *; } 132 | -keep class scala.runtime.AbstractFunction1** { *; } 133 | 134 | # remove those after testing: 135 | #-dontobfuscate 136 | #-verbose 137 | 138 | # this is a bit of magic needed to make Java enums work, taken from the ProGuard manual 139 | -keepclassmembers enum * { 140 | public static **[] values(); 141 | public static ** valueOf(java.lang.String); 142 | } 143 | 144 | -keep public class org.nlogo.widget.* { 145 | *; 146 | } 147 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/NetworkExtension.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.extensions.nw.prim.{SaveFileType, LoadFileType} 6 | import org.nlogo.extensions.nw.prim.jung.{SaveGraphML, LoadGraphML} 7 | 8 | import org.nlogo.api 9 | 10 | class NetworkExtension extends api.DefaultClassManager with GraphContextManager { 11 | 12 | val version = "1.0.0" 13 | 14 | def checkNetLogoVersion(): Unit = { 15 | try { 16 | Class.forName("org.nlogo.api.SimpleChangeEventPublisher") 17 | } catch { 18 | case e: ClassNotFoundException => throw new api.ExtensionException( 19 | "Version " + version + " of the NW extension requires NetLogo version 5.0.5 or greater.", e) 20 | } 21 | } 22 | 23 | override def clearAll(): Unit = { clearContext() } 24 | override def unload(em: api.ExtensionManager): Unit = { 25 | clearAll() 26 | } 27 | 28 | override def load(primManager: api.PrimitiveManager): Unit = { 29 | 30 | checkNetLogoVersion() 31 | 32 | val add = primManager.addPrimitive 33 | 34 | add("set-context", new prim.SetContext(this)) 35 | add("get-context", new prim.GetContext(this)) 36 | add("with-context", new prim.WithContext(this)) 37 | 38 | add("turtles-in-radius", new org.nlogo.extensions.nw.prim.TurtlesInRadius(this)) 39 | add("turtles-in-reverse-radius", new org.nlogo.extensions.nw.prim.TurtlesInReverseRadius(this)) 40 | 41 | add("mean-path-length", new prim.MeanPathLength(this)) 42 | add("mean-weighted-path-length", new prim.MeanWeightedPathLength(this)) 43 | 44 | add("distance-to", new prim.DistanceTo(this)) 45 | add("weighted-distance-to", new prim.WeightedDistanceTo(this)) 46 | add("path-to", new prim.PathTo(this)) 47 | add("weighted-path-to", new prim.WeightedPathTo(this)) 48 | add("turtles-on-path-to", new prim.TurtlesOnPathTo(this)) 49 | add("turtles-on-weighted-path-to", new prim.TurtlesOnWeightedPathTo(this)) 50 | 51 | add("betweenness-centrality", new prim.jung.BetweennessCentrality(this)) 52 | add("eigenvector-centrality", new prim.EigenvectorCentrality(this)) 53 | add("page-rank", new prim.jung.PageRank(this)) 54 | add("closeness-centrality", new prim.jung.ClosenessCentrality(this)) 55 | 56 | add("weighted-closeness-centrality", new prim.jung.WeightedClosenessCentrality(this)) 57 | /* 58 | There are some major oddities with Jung's weighted betweenness centrality. For example, in the network 0--1--2--3--0, 59 | with 3--0 having weight 10, it gives [0 1.5 1.25 0]. I don't understand what betweenness centrality > 1 is or 60 | how it could be asymmetric. So for now, I'm going to leave the plumbing in place, but not expose the functionality 61 | till we understand it. -- BCH 5/14/2014 62 | */ 63 | //add("weighted-betweenness-centrality", new prim.jung.WeightedBetweennessCentrality(this)) 64 | 65 | add("clustering-coefficient", new prim.ClusteringCoefficient(this)) 66 | add("modularity", new prim.Modularity(this)) 67 | 68 | add("louvain-communities", new prim.LouvainCommunities(this)) 69 | 70 | add("bicomponent-clusters", new prim.jung.BicomponentClusters(this)) 71 | add("weak-component-clusters", new prim.jung.WeakComponentClusters(this)) 72 | 73 | add("maximal-cliques", new prim.jgrapht.MaximalCliques(this)) 74 | add("biggest-maximal-cliques", new prim.jgrapht.BiggestMaximalCliques(this)) 75 | 76 | add("generate-preferential-attachment", new prim.BarabasiAlbertGenerator) 77 | add("generate-random", new prim.ErdosRenyiGenerator) 78 | add("generate-small-world", new prim.jung.KleinbergSmallWorldGenerator) 79 | add("generate-watts-strogatz", new prim.WattsStrogatzGenerator) 80 | add("generate-lattice-2d", new prim.jung.Lattice2DGenerator) 81 | 82 | add("generate-ring", new prim.jgrapht.RingGenerator) 83 | add("generate-star", new prim.jgrapht.StarGenerator) 84 | add("generate-wheel", new prim.jgrapht.WheelGenerator) 85 | add("generate-wheel-inward", new prim.jgrapht.WheelGeneratorInward) 86 | add("generate-wheel-outward", new prim.jgrapht.WheelGeneratorOutward) 87 | 88 | add("save-matrix", new prim.jung.SaveMatrix(this)) 89 | add("load-matrix", new prim.jung.LoadMatrix) 90 | 91 | //add("save-graphml", new SaveFileType(this, ".graphml")) 92 | add("save-graphml", new SaveGraphML(this)) 93 | //add("load-graphml", new LoadFileTypeDefaultBreeds(".graphml")) 94 | add("load-graphml", new LoadGraphML()) 95 | 96 | add("load", new prim.Load()) 97 | add("load-dl", new LoadFileType(".dl")) 98 | add("load-gdf", new LoadFileType(".gdf")) 99 | add("load-gexf", new LoadFileType(".gexf")) 100 | add("load-gml", new LoadFileType(".gml")) 101 | add("load-vna", new LoadFileType(".vna")) 102 | 103 | add("save", new prim.Save(this)) 104 | add("save-dl", new SaveFileType(this, ".dl")) 105 | add("save-gdf", new SaveFileType(this, ".gdf")) 106 | add("save-gexf", new SaveFileType(this, ".gexf")) 107 | add("save-gml", new SaveFileType(this, ".gml")) 108 | add("save-vna", new SaveFileType(this, ".vna")) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/Algorithms.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.jung 4 | 5 | import org.nlogo.agent.{World, Agent, Link, Turtle} 6 | import org.nlogo.api.ExtensionException 7 | import org.nlogo.extensions.nw.NetworkExtensionUtil.LinkToRichLink 8 | import org.nlogo.extensions.nw.NetworkExtensionUtil.functionToTransformer 9 | import org.nlogo.extensions.nw.util.TurtleSetsConverters.toTurtleSets 10 | 11 | import edu.uci.ics.jung.{ algorithms => jungalg } 12 | import org.nlogo.agent.World.VariableWatcher 13 | import org.nlogo.extensions.nw.util.CacheManager 14 | 15 | import scala.collection.mutable 16 | import scala.jdk.CollectionConverters.SetHasAsScala 17 | 18 | trait Algorithms { 19 | self: Graph => 20 | 21 | val weightedGraphCaches: mutable.Map[String, jungalg.shortestpath.DijkstraShortestPath[Turtle, Link]] = new mutable.HashMap[String, jungalg.shortestpath.DijkstraShortestPath[Turtle, Link]]() 22 | 23 | val cacheInvalidator: World.VariableWatcher = new VariableWatcher { 24 | def update(agent: Agent, variable: String, value: AnyRef) = agent match { 25 | case link: Link => if (gc.outEdges(link.end1) contains link) { 26 | weightedGraphCaches.remove(variable) 27 | gc.world.deleteWatcher(variable, cacheInvalidator) 28 | } 29 | case _ => 30 | } 31 | } 32 | 33 | def weightedDijkstraShortestPath(variable: String) = { 34 | getOrCreateCache(variable) 35 | } 36 | 37 | def getOrCreateCache(variable: String) = { 38 | weightedGraphCaches.getOrElseUpdate(variable, { 39 | val weightFunction = (link: Link) => { 40 | implicit val world = gc.world 41 | val value = link.getBreedOrLinkVariable(variable) 42 | try value.asInstanceOf[java.lang.Number] 43 | catch { 44 | case e: Exception => throw new ExtensionException("Weight variable must be numeric.") 45 | } 46 | } 47 | gc.world.addWatcher(variable, cacheInvalidator) 48 | new jungalg.shortestpath.DijkstraShortestPath(self, weightFunction, true) 49 | }) 50 | } 51 | 52 | def betweennessCentrality(agent: Agent) = betweennessCentralityCache()(agent) 53 | def betweennessCentrality(agent: Agent, weightVar: String) = betweennessCentralityCache(Some(weightVar))(agent) 54 | 55 | val betweennessCentralityCache = CacheManager[Agent, Double](gc.world, { 56 | case None => new UnweightedBetweennessCentrality().get 57 | case Some(varName: String) => new WeightedBetweennessCentrality(varName).get 58 | }: Option[String] => Agent => Double) 59 | 60 | class WeightedBetweennessCentrality(variable: String) 61 | extends jungalg.scoring.BetweennessCentrality(self, gc.weightFunction(variable).andThen(_.asInstanceOf[java.lang.Double])) 62 | with BetweennessCentrality 63 | 64 | class UnweightedBetweennessCentrality 65 | extends jungalg.scoring.BetweennessCentrality(self) 66 | with BetweennessCentrality 67 | 68 | trait BetweennessCentrality extends jungalg.scoring.BetweennessCentrality[Turtle, Link] { 69 | def get(agent: Agent) = agent match { 70 | case (t: Turtle) => getVertexScore(t) 71 | case (l: Link) => getEdgeScore(l) 72 | case _ => throw new IllegalStateException 73 | } 74 | } 75 | 76 | object PageRank extends jungalg.scoring.PageRank(this, 0.15) { 77 | evaluate() 78 | def getScore(turtle: Turtle) = { 79 | if (!graph.containsVertex(turtle)) 80 | throw new ExtensionException(s"$turtle is not a member of the current graph context.") 81 | getVertexScore(turtle) 82 | } 83 | } 84 | 85 | def closenessCentrality(turtle: Turtle) = closenessCentralityCache()(turtle) 86 | def closenessCentrality(turtle: Turtle, weightVar: String) = closenessCentralityCache(Some(weightVar))(turtle) 87 | 88 | val closenessCentralityCache = CacheManager[Turtle, Double](gc.world,{ 89 | case None => new UnweightedClosenessCentrality().getScore 90 | case Some(varName: String) => new WeightedClosenessCentrality(varName).getScore 91 | }: Option[String] => Turtle => Double) 92 | 93 | class UnweightedClosenessCentrality 94 | extends jungalg.scoring.ClosenessCentrality(this) 95 | with ClosenessCentrality 96 | 97 | class WeightedClosenessCentrality(variable: String) 98 | extends jungalg.scoring.ClosenessCentrality(this, gc.weightFunction(variable).andThen(_.asInstanceOf[java.lang.Double])) 99 | with ClosenessCentrality 100 | 101 | trait ClosenessCentrality extends jungalg.scoring.ClosenessCentrality[Turtle, Link] { 102 | def getScore(turtle: Turtle) = { 103 | if (!self.containsVertex(turtle)) 104 | throw new ExtensionException(s"$turtle is not a member of the current graph context.") 105 | val res = getVertexScore(turtle) 106 | if (res.isNaN) 107 | Double.box(0.0) // for isolates 108 | else res 109 | } 110 | } 111 | } 112 | 113 | trait UndirectedAlgorithms extends Algorithms { 114 | self: UndirectedGraph => 115 | object BicomponentClusterer 116 | extends jungalg.cluster.BicomponentClusterer[Turtle, Link] { 117 | def clusters(rng: java.util.Random) = toTurtleSets(transform(self).asScala, rng) 118 | } 119 | } 120 | 121 | trait DirectedAlgorithms extends Algorithms { 122 | self: DirectedGraph => 123 | } 124 | -------------------------------------------------------------------------------- /src/test/org/nlogo/extensions/nw/SubscriberTests.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.agent.{ AgentSet, TreeAgentSet } 6 | import org.nlogo.headless.HeadlessWorkspace 7 | import org.scalatest.funsuite.AnyFunSuite 8 | import org.scalatest.matchers.should.Matchers._ 9 | import org.scalatest.GivenWhenThen 10 | 11 | class AgentSetChangeSubscribersTestSuite extends AnyFunSuite with GivenWhenThen { 12 | 13 | def getSubscribers(agentSet: AgentSet): collection.Set[AgentSetChangeSubscriber] = { 14 | val pub = agentSet.asInstanceOf[TreeAgentSet].simpleChangeEventPublisher 15 | // get private `listeners` field using reflection: 16 | val field = pub.getClass.getDeclaredField("org$nlogo$core$Publisher$$listeners") 17 | field.setAccessible(true) 18 | field.get(pub).asInstanceOf[collection.Set[AgentSetChangeSubscriber]] 19 | } 20 | 21 | def checkSizes(expectedSizes: (Iterable[?], Int)*) = 22 | for ((xs, n) <- expectedSizes) xs should have size n 23 | 24 | test("subscribers") { 25 | val ws: HeadlessWorkspace = HeadlessWorkspace.newInstance 26 | ws.setModelPath(new java.io.File("tests.txt").getPath) 27 | try { 28 | 29 | Given("a newly initialized workspace") 30 | ws.initForTesting(1, "extensions [nw]\n" + HeadlessWorkspace.TestDeclarations) 31 | 32 | Then("there should be no subscribers") 33 | val t = getSubscribers(ws.world.turtles) 34 | val l = getSubscribers(ws.world.links) 35 | val f = getSubscribers(ws.world.getBreed("FROGS")) 36 | val m = getSubscribers(ws.world.getBreed("MICE")) 37 | val u = getSubscribers(ws.world.getLinkBreed("UNDIRECTED-EDGES")) 38 | val d = getSubscribers(ws.world.getLinkBreed("DIRECTED-EDGES")) 39 | checkSizes(t -> 0, l -> 0, f -> 0, m -> 0, u -> 0, d -> 0) 40 | 41 | When("we `nw:set-context turtles links`") 42 | ws.command("nw:set-context turtles links") 43 | Then("turtles and links should get one subscriber each") 44 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 45 | 46 | When("we create a single mouse") 47 | ws.command("create-mice 1") 48 | Then("subscriber counts should not change") 49 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 50 | 51 | When("we create a couple of frogs with undir links to each other") 52 | ws.command("create-frogs 2 [ create-undirected-edges-with other frogs ]") 53 | Then("subscriber counts should not change") 54 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 55 | 56 | When("we explicitely get the context") 57 | ws.report("nw:get-context") 58 | Then("subscriber counts should not change either") 59 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 60 | 61 | When("we set the context to the breeds") 62 | ws.command("nw:set-context frogs undirected-edges") 63 | Then("turtles and links should loose their subscribers and frogs/undir-links should get theirs") 64 | checkSizes(t -> 0, l -> 0, f -> 1, m -> 0, u -> 1, d -> 0) 65 | 66 | When("we go back to the `turtles links` context") 67 | ws.command("nw:set-context turtles links") 68 | Then("only turtles and links should have subscribers") 69 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 70 | 71 | locally { 72 | val turtleSub = t.head 73 | val linkSub = l.head 74 | When("we use `nw:with-context` to do something innocuous") 75 | ws.command("nw:with-context frogs undirected-edges [" + 76 | "let d [ nw:distance-to one-of other frogs ] of one-of frogs ]") 77 | Then("the subscribers counts should not change") 78 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 79 | And("the same old subscribers objects should still be subscribed") 80 | t.head should be theSameInstanceAs turtleSub 81 | l.head should be theSameInstanceAs linkSub 82 | When("we do something that verifies the context") 83 | ws.report("nw:get-context") 84 | Then("again, the counts and subscribers objects should not change") 85 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 86 | t.head should be theSameInstanceAs turtleSub 87 | l.head should be theSameInstanceAs linkSub 88 | When("we do something within nw:with-context that should invalidate the cache") 89 | ws.command("nw:with-context frogs undirected-edges [ ask one-of frogs [ die ] ]") 90 | Then("the subscribers counts should not change") 91 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 92 | When("we do something that verifies the context") 93 | ws.command("let c nw:get-context") 94 | Then("the subscribers counts should not change") 95 | checkSizes(t -> 1, l -> 1, f -> 0, m -> 0, u -> 0, d -> 0) 96 | And("we should have new subscriber objects because of the invalidated context") 97 | t.head should not be theSameInstanceAs(turtleSub) 98 | l.head should not be theSameInstanceAs(linkSub) 99 | } 100 | 101 | When("we clear-all") 102 | ws.command("clear-all") 103 | Then("all subscribers should be gone") 104 | checkSizes(t -> 0, l -> 0, f -> 0, m -> 0, u -> 0, d -> 0) 105 | 106 | } finally ws.dispose() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/prim/Paths.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw.prim 4 | 5 | import org.nlogo.agent 6 | import org.nlogo.api 7 | import org.nlogo.core.LogoList 8 | import org.nlogo.api.ScalaConversions.toLogoObject 9 | import org.nlogo.core.Syntax._ 10 | import org.nlogo.extensions.nw.NetworkExtensionUtil.{AgentToRichAgent, canonocilizeVar} 11 | import org.nlogo.extensions.nw.GraphContextProvider 12 | 13 | class DistanceTo(gcp: GraphContextProvider) 14 | extends api.Reporter { 15 | override def getSyntax = reporterSyntax( 16 | right = List(TurtleType), 17 | ret = NumberType | BooleanType, 18 | agentClassString = "-T--") 19 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 20 | val source = context.getAgent.asInstanceOf[agent.Turtle] 21 | val target = args(0).getAgent.requireAlive.asInstanceOf[agent.Turtle] 22 | val graphContext = gcp.getGraphContext(context.getAgent.world) 23 | toLogoObject(graphContext.pathFinder.distance(source, target).getOrElse(false)) 24 | } 25 | } 26 | 27 | class WeightedDistanceTo(gcp: GraphContextProvider) 28 | extends api.Reporter { 29 | override def getSyntax = reporterSyntax( 30 | right = List(TurtleType, StringType | SymbolType), 31 | ret = NumberType | BooleanType, 32 | agentClassString = "-T--") 33 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 34 | val source = context.getAgent.asInstanceOf[agent.Turtle] 35 | val target = args(0).getAgent.asInstanceOf[agent.Turtle] 36 | val weightVariable = canonocilizeVar(args(1).get) 37 | val distance = gcp.getGraphContext(context.getAgent.world).pathFinder.distance(source, target, Some(weightVariable)) 38 | toLogoObject(distance.getOrElse(false)) 39 | } 40 | } 41 | 42 | class PathTo(gcp: GraphContextProvider) 43 | extends api.Reporter { 44 | override def getSyntax = reporterSyntax( 45 | right = List(TurtleType), 46 | ret = ListType | BooleanType, 47 | agentClassString = "-T--") 48 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 49 | val source = context.getAgent.asInstanceOf[agent.Turtle] 50 | val target = args(0).getAgent.requireAlive.asInstanceOf[agent.Turtle] 51 | val gc = gcp.getGraphContext(context.getAgent.world) 52 | def turtlesToLinks(turtles: List[agent.Turtle]): Iterator[agent.Link] = 53 | for { 54 | (source, target) <- turtles.iterator zip turtles.tail.iterator 55 | // RNG is necessary because there may be more than one link between the turtles 56 | links = gc.outEdges(source).filter(l => gc.otherEnd(source)(l) == target) 57 | l = links(context.getRNG.nextInt(links.size)) 58 | } yield l 59 | toLogoObject(gc.pathFinder.path(source, target, context.getRNG) 60 | .map { p => LogoList.fromIterator(turtlesToLinks(p.toList)) } 61 | .getOrElse(false)) 62 | } 63 | } 64 | 65 | class TurtlesOnPathTo(gcp: GraphContextProvider) 66 | extends api.Reporter { 67 | override def getSyntax = reporterSyntax( 68 | right = List(TurtleType), 69 | ret = ListType | BooleanType, 70 | agentClassString = "-T--") 71 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 72 | val source = context.getAgent.asInstanceOf[agent.Turtle] 73 | val target = args(0).getAgent.requireAlive.asInstanceOf[agent.Turtle] 74 | val graphContext = gcp.getGraphContext(context.getAgent.world) 75 | toLogoObject(graphContext.pathFinder.path(source, target, context.getRNG) 76 | .map { p => LogoList.fromIterator(p.iterator) } 77 | .getOrElse(false)) 78 | } 79 | } 80 | 81 | class TurtlesOnWeightedPathTo(gcp:GraphContextProvider) 82 | extends api.Reporter { 83 | override def getSyntax = reporterSyntax( 84 | right = List(TurtleType, StringType | SymbolType), 85 | ret = ListType | BooleanType, 86 | agentClassString = "-T--") 87 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 88 | val source = context.getAgent.asInstanceOf[agent.Turtle] 89 | val target = args(0).getAgent.asInstanceOf[agent.Turtle] 90 | val weightVariable = canonocilizeVar(args(1).get) 91 | val graphContext = gcp.getGraphContext(context.getAgent.world) 92 | toLogoObject(graphContext.pathFinder.path(source, target, context.getRNG, Some(weightVariable)) 93 | .map { p => LogoList.fromIterator(p.iterator) } 94 | .getOrElse(false)) 95 | } 96 | } 97 | 98 | class WeightedPathTo(gcp: GraphContextProvider) 99 | extends api.Reporter { 100 | override def getSyntax = reporterSyntax( 101 | right = List(TurtleType, StringType | SymbolType), 102 | ret = ListType | BooleanType, 103 | agentClassString = "-T--") 104 | override def report(args: Array[api.Argument], context: api.Context): AnyRef = { 105 | val source = context.getAgent.asInstanceOf[agent.Turtle] 106 | val target = args(0).getAgent.asInstanceOf[agent.Turtle] 107 | val weightVariable = canonocilizeVar(args(1).get) 108 | val gc = gcp.getGraphContext(context.getAgent.world) 109 | def turtlesToLinks(turtles: List[agent.Turtle]): Iterator[agent.Link] = 110 | for { 111 | (source, target) <- turtles.iterator zip turtles.tail.iterator 112 | // RNG is necessary because there may be more than one link between the turtles 113 | links = gc.outEdges(source).filter(l => gc.otherEnd(source)(l) == target) 114 | l = links(context.getRNG.nextInt(links.size)) 115 | } yield l 116 | toLogoObject(gc 117 | .pathFinder.path(source, target, context.getRNG, Some(weightVariable)) 118 | .map { p => LogoList.fromIterator(turtlesToLinks(p.toList)) } 119 | .getOrElse(false)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/NetworkExtensionUtil.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.agent.TreeAgentSet 6 | import org.nlogo.api.{Agent, ExtensionException} 7 | import org.nlogo.core.{ AgentKind, I18N, Syntax, Token } 8 | import org.nlogo.{agent, api, nvm} 9 | import org.nlogo.api.MersenneTwisterFast 10 | import scala.language.implicitConversions 11 | import scala.reflect.Selectable.reflectiveSelectable 12 | import java.util.Locale 13 | 14 | object NetworkExtensionUtil { 15 | 16 | implicit def functionToTransformer[I, O](f: Function1[I, O]): org.apache.commons.collections15.Transformer[I,O] = 17 | new org.apache.commons.collections15.Transformer[I, O] { 18 | override def transform(i: I) = f(i) 19 | } 20 | 21 | implicit def AgentToRichAgent(agent: Agent): org.nlogo.extensions.nw.NetworkExtensionUtil.RichAgent = new RichAgent(agent) 22 | class RichAgent(agent: Agent) { 23 | def requireAlive = 24 | if (agent.id != -1) // is alive 25 | agent 26 | else throw new ExtensionException( 27 | I18N.errors.get("org.nlogo.$common.thatAgentIsDead")) 28 | } 29 | 30 | implicit def LinkToRichLink(link: org.nlogo.agent.Link)(implicit world: agent.World): org.nlogo.extensions.nw.NetworkExtensionUtil.RichLink = 31 | new RichLink(link, world) 32 | 33 | class RichLink(link: org.nlogo.agent.Link, world: agent.World) { 34 | def getBreedOrLinkVariable(variable: String) = 35 | try { 36 | world.program.linksOwn.indexOf(variable) match { 37 | case -1 => link.getLinkBreedVariable(variable) 38 | case i => link.getLinkVariable(i) 39 | } 40 | } catch { 41 | case e: Exception => throw new ExtensionException(e) 42 | } 43 | } 44 | 45 | implicit def AgentSetToRichAgentSet(agentSet: api.AgentSet)(implicit world: org.nlogo.agent.World): org.nlogo.extensions.nw.NetworkExtensionUtil.RichAgentSet = 46 | new RichAgentSet(agentSet.asInstanceOf[agent.AgentSet], world) 47 | 48 | class RichAgentSet(agentSet: agent.AgentSet, val world: org.nlogo.agent.World) { 49 | assert(agentSet != null) 50 | def isLinkBreed = (agentSet eq world.links) || world.isLinkBreed(agentSet) 51 | def isTurtleBreed = (agentSet eq world.turtles) || world.isBreed(agentSet) 52 | 53 | def isLinkSet = agentSet.kind == AgentKind.Link 54 | def isTurtleSet = agentSet.kind == AgentKind.Turtle 55 | def requireTurtleSet = 56 | if (isTurtleSet) agentSet 57 | else throw new ExtensionException("Expected input to be a turtleset") 58 | def requireLinkSet = 59 | if (isLinkSet) agentSet 60 | else throw new ExtensionException("Expected input to be a linkset") 61 | 62 | def requireTurtleBreed = 63 | if (isTurtleBreed) agentSet.asInstanceOf[TreeAgentSet] 64 | else throw new ExtensionException("Expected input to be a turtle breed") 65 | def requireLinkBreed = 66 | if (isLinkBreed) agentSet.asInstanceOf[TreeAgentSet] 67 | else throw new ExtensionException( 68 | I18N.errors.get("org.nlogo.prim.etc.$common.expectedLastInputToBeLinkBreed")) 69 | def requireDirectedLinkBreed = 70 | if (isLinkBreed && agentSet.isDirected) agentSet.asInstanceOf[TreeAgentSet] 71 | else throw new ExtensionException( 72 | "Expected input to be a directed link breed") 73 | def requireUndirectedLinkBreed = 74 | if (isLinkBreed && !agentSet.isDirected) agentSet.asInstanceOf[TreeAgentSet] 75 | else throw new ExtensionException( 76 | "Expected input to be an undirected link breed") 77 | 78 | private class AgentSetIterable[T <: Agent] 79 | extends Iterable[T] { 80 | protected def newIt = agentSet.iterator 81 | override def iterator: Iterator[T] = { 82 | val it = newIt 83 | new Iterator[T] { 84 | def hasNext = it.hasNext 85 | def next() = it.next().asInstanceOf[T] 86 | } 87 | } 88 | } 89 | def asIterable[T <: Agent]: Iterable[T] = new AgentSetIterable 90 | 91 | private class AgentSetShufflerable[T <: Agent](rng: MersenneTwisterFast) 92 | extends AgentSetIterable[T] { 93 | override def newIt = agentSet.shufflerator(rng) 94 | } 95 | /** A Shufflerable is to a Shufflerator as an Iterable is to an Iterator */ 96 | def asShufflerable[T <: Agent](rng: MersenneTwisterFast): Iterable[T] = 97 | new AgentSetShufflerable(rng) 98 | 99 | } 100 | 101 | trait TurtleAskingCommand extends api.Command with nvm.CustomAssembled { 102 | // the command itself is turtle or observer. inside the block is turtle code. 103 | // Issue #126 provides a good use case for this to be executed in turtle contexts. 104 | override def getSyntax = 105 | Syntax.commandSyntax(agentClassString = "OT--", blockAgentClassString = Some("-T--")) 106 | 107 | def askTurtles(context: api.Context)(turtles: IterableOnce[agent.Turtle]) = { 108 | val agents = turtles.iterator.toArray 109 | val extContext = context.asInstanceOf[nvm.ExtensionContext] 110 | val nvmContext = extContext.nvmContext 111 | agents.foreach(extContext.workspace.joinForeverButtons) 112 | val agentSet = agent.AgentSet.fromArray(AgentKind.Turtle, agents) 113 | nvmContext.runExclusiveJob(agentSet, nvmContext.ip + 1) 114 | } 115 | def assemble(a: nvm.AssemblerAssistant): Unit = { 116 | a.block() 117 | a.done() 118 | } 119 | 120 | } 121 | 122 | trait TurtleCreatingCommand extends TurtleAskingCommand { 123 | def createTurtles(args: Array[api.Argument], context: api.Context): IterableOnce[agent.Turtle] 124 | override def perform(args: Array[api.Argument], context: api.Context) = 125 | askTurtles(context)(createTurtles(args, context)) 126 | 127 | // helper function to validate a minimum number of "things" (could be nodes, rows, columns, etc.) 128 | // and throw an appropriate exception if the value is below that minimum 129 | protected def getIntValueWithMinimum(arg: api.Argument, minimum: Int, things: String = "nodes") = { 130 | val nb = arg.getIntValue 131 | if (nb < minimum) 132 | throw new ExtensionException( 133 | "The number of " + things + " in the generated network must be at least " + minimum + ".") 134 | nb 135 | } 136 | 137 | } 138 | 139 | def createTurtle(world: agent.World, turtleBreed: agent.AgentSet, rng: MersenneTwisterFast) = 140 | world.createTurtle( 141 | turtleBreed, 142 | rng.nextInt(14), // color 143 | rng.nextInt(360)) // heading 144 | 145 | def using[A <: { def close(): Unit }, B](closeable: A)(body: A => B): B = 146 | try body(closeable) finally closeable.close() 147 | 148 | def canonocilizeVar(variable: AnyRef) = variable match { 149 | case s: String => s.toUpperCase(Locale.ENGLISH) 150 | case t: Token => t.text.toString.toUpperCase(Locale.ENGLISH) 151 | case _ => throw new IllegalStateException 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/GraphContext.scala: -------------------------------------------------------------------------------- 1 | // (C) Uri Wilensky. https://github.com/NetLogo/NW-Extension 2 | 3 | package org.nlogo.extensions.nw 4 | 5 | import org.nlogo.agent._ 6 | import org.nlogo.api.ExtensionException 7 | import org.nlogo.extensions.nw.NetworkExtensionUtil.AgentSetToRichAgentSet 8 | import org.nlogo.extensions.nw.algorithms.{BreadthFirstSearch, PathFinder} 9 | 10 | import scala.collection.immutable.{ SortedSet, SortedMap } 11 | import scala.collection.mutable 12 | import scala.collection.mutable.ArrayBuffer 13 | 14 | class GraphContext( val world: World, val turtleSet: AgentSet, val linkSet: AgentSet) 15 | extends Graph[Turtle, Link] 16 | with algorithms.CentralityMeasurer { 17 | 18 | implicit val implicitWorld: World = world 19 | 20 | val turtleMonitor: MonitoredAgentSet[Turtle] = turtleSet match { 21 | case tas: TreeAgentSet => new MonitoredTurtleTreeAgentSet(tas, world) 22 | case aas: ArrayAgentSet => new MonitoredTurtleArrayAgentSet(aas) 23 | case _ => throw new IllegalStateException 24 | } 25 | 26 | val linkMonitor: MonitoredAgentSet[Link] = linkSet match { 27 | case tas: TreeAgentSet => new MonitoredLinkTreeAgentSet(tas, world) 28 | case aas: ArrayAgentSet => new MonitoredLinkArrayAgentSet(aas) 29 | case _ => throw new IllegalStateException 30 | } 31 | 32 | // We need a guaranteed iteration order for Turtles, but also want fast 33 | // contains checks, so we use a SortedSet. Note that if you want to shuffle 34 | // the turtles, you *must* convert to Seq first. Shuffling a Set converts to 35 | // a HashTrie which does *not* have a deterministic ordering over Turtles. 36 | implicit object TurtleOrdering extends Ordering[Turtle]{ 37 | override def compare(x: Turtle, y: Turtle): Int = x.compareTo(y) 38 | } 39 | override val nodes: SortedSet[Turtle] = turtleSet.asIterable[Turtle].to(SortedSet) 40 | override val links: Iterable[Link] = linkSet.asIterable[Link] 41 | .filter(l => nodes.contains(l.end1) && nodes.contains(l.end2)) 42 | 43 | val (undirLinks, inLinks, outLinks) = { 44 | val in = mutable.Map.empty[Turtle, ArrayBuffer[Link]] 45 | val out = mutable.Map.empty[Turtle, ArrayBuffer[Link]] 46 | val undir = mutable.Map.empty[Turtle, ArrayBuffer[Link]] 47 | links foreach { link => 48 | if (link.isDirectedLink) { 49 | out.getOrElseUpdate(link.end1, ArrayBuffer.empty[Link]) += link 50 | in.getOrElseUpdate(link.end2, ArrayBuffer.empty[Link]) += link 51 | } else { 52 | undir.getOrElseUpdate(link.end1, ArrayBuffer.empty[Link]) += link 53 | undir.getOrElseUpdate(link.end2, ArrayBuffer.empty[Link]) += link 54 | } 55 | } 56 | (undir.toMap[Turtle, mutable.Seq[Link]] withDefaultValue mutable.Seq.empty[Link], 57 | in.toMap[Turtle, mutable.Seq[Link]] withDefaultValue mutable.Seq.empty[Link], 58 | out.toMap[Turtle, mutable.Seq[Link]] withDefaultValue mutable.Seq.empty[Link]) 59 | } 60 | 61 | def verify(w: World): GraphContext = { 62 | if (w != world) { 63 | new GraphContext(w, w.turtles, w.links) 64 | } else if (turtleMonitor.hasChanged || linkMonitor.hasChanged) { 65 | // When a resize occurs, breed sets are all set to new objects, so we 66 | // need to make sure we're working with the latest object. 67 | new GraphContext(w, 68 | if (turtleSet.isInstanceOf[TreeAgentSet]) Option(w.getBreed(turtleSet.printName)).getOrElse(w.turtles) else turtleSet, 69 | if (linkSet.isInstanceOf[TreeAgentSet]) Option(w.getLinkBreed(linkSet.printName)).getOrElse(w.links) else linkSet) 70 | } else { 71 | this 72 | } 73 | } 74 | 75 | def asJungGraph: jung.Graph = if (isDirected) asDirectedJungGraph else asUndirectedJungGraph 76 | private var directedJungGraph: Option[jung.DirectedGraph] = None 77 | def asDirectedJungGraph: jung.DirectedGraph = { 78 | directedJungGraph 79 | .getOrElse { 80 | val g = new jung.DirectedGraph(this) 81 | directedJungGraph = Some(g) 82 | g 83 | } 84 | } 85 | private var undirectedJungGraph: Option[jung.UndirectedGraph] = None 86 | def asUndirectedJungGraph: jung.UndirectedGraph = { 87 | undirectedJungGraph 88 | .getOrElse { 89 | val g = new jung.UndirectedGraph(this) 90 | undirectedJungGraph = Some(g) 91 | g 92 | } 93 | } 94 | 95 | def asJGraphTGraph: jgrapht.Graph = if (isDirected) asDirectedJGraphTGraph else asUndirectedJGraphTGraph 96 | lazy val asDirectedJGraphTGraph = new jgrapht.DirectedGraph(this) 97 | lazy val asUndirectedJGraphTGraph = new jgrapht.UndirectedGraph(this) 98 | 99 | // I tried caching this, but it only made SingleSourceWeighted benchmark ~5% faster; not significant 100 | // enough to be worth the memory. -- BCH 5/14/2014 101 | def weightFunction(variable: String): (Link => Double) = { 102 | (link: Link) => 103 | try { 104 | link.world.program.linksOwn.indexOf(variable) match { 105 | case -1 => link.getLinkBreedVariable(variable).asInstanceOf[Double] 106 | case i => link.getLinkVariable(i).asInstanceOf[Double] 107 | } 108 | } catch { 109 | case e: ClassCastException => throw new ExtensionException("Weights must be numbers.", e) 110 | case e: Exception => throw new ExtensionException(e) 111 | } 112 | } 113 | 114 | // linkSet.isDirected fails for empty and mixed directed networks. The only 115 | // reliable way is to actually see if there are any directed links. 116 | // -- BCH 5/13/2014 117 | val isDirected: Boolean = outLinks.nonEmpty 118 | 119 | lazy val turtleCount: Int = nodes.size 120 | lazy val linkCount: Int = links.size 121 | 122 | override def ends(link: Link): (Turtle, Turtle) = (link.end1, link.end2) 123 | override def otherEnd(node: Turtle)(link: Link): Turtle = if (link.end1 == node) link.end2 else link.end1 124 | 125 | override def inEdges(turtle: Turtle): Seq[Link] = (inLinks(turtle) ++ undirLinks(turtle)).toSeq 126 | 127 | override def outEdges(turtle: Turtle): Seq[Link] = (outLinks(turtle) ++ undirLinks(turtle)).toSeq 128 | 129 | override def allEdges(turtle: Turtle): Seq[Link] = (inLinks(turtle) ++ outLinks(turtle) ++ undirLinks(turtle)).toSeq 130 | 131 | override def toString = turtleSet.toLogoList.toString + "\n" + linkSet.toLogoList 132 | 133 | val pathFinder = new PathFinder[Turtle, Link](this, world, weightFunction) 134 | 135 | lazy val components: Iterable[SortedSet[Turtle]] = { 136 | val foundBy = mutable.Map[Turtle, Turtle]() 137 | nodes.groupBy { t => 138 | foundBy.getOrElseUpdate(t, { 139 | BreadthFirstSearch(this, t, followOut = true, followIn = true) 140 | .map(_.head) 141 | .foreach(found => foundBy(found) = t) 142 | t 143 | }) 144 | }.to(SortedMap).values 145 | } 146 | 147 | def monitoredTreeAgentSets = 148 | Seq(turtleMonitor, linkMonitor).collect { 149 | case mtas: MonitoredTreeAgentSet[_] => mtas 150 | } 151 | 152 | def unsubscribe(): Unit = monitoredTreeAgentSets.foreach(_.unsubscribe()) 153 | } 154 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/gephi/GephiExport.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.gephi 2 | 3 | import java.io.File 4 | 5 | import org.nlogo.agent.{ AgentSet, Link, Turtle, World } 6 | import org.nlogo.api 7 | import org.nlogo.api._ 8 | import org.nlogo.core.Breed 9 | 10 | import org.gephi.graph.api.{ Column, GraphController } 11 | import org.gephi.io.exporter.api.ExportController 12 | import org.gephi.io.exporter.plugin.{ ExporterCSV, ExporterGraphML } 13 | import org.gephi.io.exporter.spi.Exporter 14 | import org.gephi.project.api.ProjectController 15 | import org.gephi.utils.longtask.spi.LongTask 16 | import org.gephi.utils.progress.ProgressTicket 17 | import org.openide.util.Lookup 18 | 19 | import org.nlogo.extensions.nw.GraphContext 20 | import org.nlogo.extensions.nw.NetworkExtensionUtil._ 21 | 22 | object GephiExport { 23 | val exportController = GephiUtils.withNWLoaderContext {Lookup.getDefault.lookup(classOf[ExportController])} 24 | 25 | def save(context: GraphContext, world: World, file: File, extension: String): Unit = GephiUtils.withNWLoaderContext { 26 | save(context, world, file, exportController.getExporter(extension)) 27 | } 28 | 29 | def save(context: GraphContext, world: World, file: File): Unit = GephiUtils.withNWLoaderContext { 30 | save(context, world, file, exportController.getFileExporter(file)) 31 | } 32 | 33 | def save(context: GraphContext, world: World, file: File, exporter: Exporter) = GephiUtils.withNWLoaderContext { 34 | implicit val implicitWorld = world; 35 | 36 | if (exporter == null) { 37 | throw new ExtensionException("Unable to find exporter for " + file) 38 | } else if (exporter.isInstanceOf[ExporterCSV]) { 39 | throw new ExtensionException("Exporting CSV files is not supported.") 40 | } else if (exporter.isInstanceOf[ExporterGraphML]) { 41 | throw new ExtensionException("You must use nw:save-graphml to save graphml files.") 42 | } 43 | 44 | // Some exporters expect a progress ticker to be passed in :( BCH 5/8/2015 45 | exporter match { 46 | case lt: LongTask => lt.setProgressTicket(new ProgressTicket { 47 | def finish(): Unit = () 48 | 49 | def finish(finishMessage: String): Unit = () 50 | 51 | def getDisplayName: String = "" 52 | 53 | def progress(): Unit = () 54 | 55 | def progress(workunit: Int): Unit = () 56 | 57 | def progress(message: String): Unit = () 58 | 59 | def progress(message: String, workunit: Int): Unit = () 60 | 61 | def switchToIndeterminate(): Unit = () 62 | 63 | def setDisplayName(newDisplayName: String): Unit = () 64 | 65 | def start(): Unit = () 66 | 67 | def start(workunits: Int): Unit = () 68 | 69 | def switchToDeterminate(workunits: Int): Unit = () 70 | } ) 71 | case _ => // ignore 72 | } 73 | val program = world.program 74 | val projectController = Lookup.getDefault.lookup(classOf[ProjectController]) 75 | projectController.newProject() 76 | val gephiWorkspace = projectController.getCurrentWorkspace 77 | val graphModel = Lookup.getDefault.lookup(classOf[GraphController]).getGraphModel 78 | val graph = (context.links.exists(_.isDirectedLink), context.links.exists(!_.isDirectedLink)) match { 79 | case (true, false) => graphModel.getDirectedGraph 80 | case (false, true) => graphModel.getUndirectedGraph 81 | case _ => graphModel.getGraph 82 | } 83 | 84 | val nodeAttributes = graphModel.getNodeTable 85 | 86 | val turtlesOwnAttributes: Map[String, Column] = program.turtlesOwn.map { name => 87 | val kind = getBestType(world.turtles.asIterable[Turtle].map(t => t.getTurtleOrLinkVariable(name))) 88 | name -> Option(nodeAttributes.getColumn(name)).getOrElse(nodeAttributes.addColumn(name, kind)) 89 | }.toMap 90 | 91 | val breedsOwnAttributes: Map[AgentSet, Map[String, Column]] = program.breeds.collect { 92 | case (breedName, breed: Breed) => 93 | world.getBreed(breedName) -> breed.owns.map { name => 94 | val kind = getBestType(world.getBreed(breedName).asIterable[Turtle].map(t => t.getBreedVariable(name))) 95 | name -> Option(nodeAttributes.getColumn(name)).getOrElse(nodeAttributes.addColumn(name, kind)) 96 | }.toMap 97 | }.toMap 98 | 99 | val nodes = context.nodes.unsorted.map { turtle => 100 | val node = graphModel.factory.newNode(turtle.toString.split(" ").mkString("-")) 101 | turtlesOwnAttributes.foreach { case (name, col) => 102 | node.setAttribute(col, coerce(turtle.getTurtleOrLinkVariable(name), col.getTypeClass)) 103 | } 104 | if (turtle.getBreed != world.turtles) { 105 | breedsOwnAttributes(turtle.getBreed).foreach { case (name, col) => 106 | node.setAttribute(col, coerce(turtle.getBreedVariable(name), col.getTypeClass)) 107 | } 108 | } 109 | val color = api.Color.getColor(turtle.color()) 110 | node.setColor(color) 111 | node.setAlpha(color.getAlpha / 255f) 112 | graph.addNode(node) 113 | turtle -> node 114 | }.toMap 115 | 116 | val edgeAttributes = graphModel.getEdgeTable 117 | val linksOwnAttributes: Map[String, Column] = program.linksOwn.map { name => 118 | val kind = getBestType(world.links.asIterable[Link].map(l => l.getTurtleOrLinkVariable(name))) 119 | name -> Option(edgeAttributes.getColumn(name)).getOrElse(edgeAttributes.addColumn(name, kind)) 120 | }.toMap 121 | 122 | val linkBreedsOwnAttributes: Map[AgentSet, Map[String, Column]] = program.linkBreeds.collect { 123 | case (breedName, breed: Breed) => 124 | val breedSet = world.getLinkBreed(breedName.toUpperCase) 125 | breedSet -> breed.owns.map { name => 126 | lazy val kind = getBestType(breedSet.asIterable[Link].map(_.getLinkBreedVariable(name))) 127 | name -> Option(edgeAttributes.getColumn(name)).getOrElse(edgeAttributes.addColumn(name, kind)) 128 | }.toMap 129 | }.toMap 130 | 131 | context.links.foreach { link => 132 | val edge = graphModel.factory.newEdge(nodes(link.end1), nodes(link.end2), 1, link.isDirectedLink) 133 | linksOwnAttributes.foreach { case (name, col) => 134 | edge.setAttribute(col, coerce(link.getTurtleOrLinkVariable(name), col.getTypeClass)) 135 | } 136 | if (link.getBreed != world.links) { 137 | linkBreedsOwnAttributes(link.getBreed).foreach { case (name, col) => 138 | edge.setAttribute(col, coerce(link.getLinkBreedVariable(name), col.getTypeClass)) 139 | } 140 | } 141 | val color = api.Color.getColor(link.color()) 142 | edge.setColor(color) 143 | edge.setAlpha(color.getAlpha / 255f) 144 | graph.addEdge(edge) 145 | } 146 | gephiWorkspace.add(graphModel) 147 | exportController.exportFile(file, exporter) 148 | } 149 | 150 | private type JDouble = java.lang.Double 151 | private type JBoolean = java.lang.Boolean 152 | private val DoubleClass = classOf[java.lang.Double] 153 | private val FloatClass = classOf[java.lang.Float] 154 | private val BooleanClass = classOf[java.lang.Boolean] 155 | private val StringClass = classOf[java.lang.String] 156 | 157 | private def getBestType(values: Iterable[AnyRef]): Class[?] = { 158 | if (values.forall(_.isInstanceOf[Number])){ 159 | DoubleClass 160 | } else if (values.forall(_.isInstanceOf[JBoolean])) { 161 | BooleanClass 162 | } else { 163 | StringClass 164 | } 165 | } 166 | 167 | private def coerce(value: AnyRef, kind: Class[?]): AnyRef = 168 | (value, kind) match { 169 | case (x: Number, DoubleClass) => x.doubleValue: JDouble 170 | case (x: Number, FloatClass) => x.doubleValue: JDouble 171 | case (b: JBoolean, BooleanClass) => b 172 | // For Strings, we want to keep the escaping but ditch the surrounding quotes. BCH 1/26/2015 173 | case (s: String, StringClass) => Dump.logoObject(s, readable = true, exporting = false).drop(1).dropRight(1) 174 | case (s: AnyRef, StringClass) => Dump.logoObject(s, readable = true, exporting = false) 175 | case (o: AnyRef, attributeType) => 176 | throw new ExtensionException(s"Could not coerce ${Dump.logoObject(o, readable = true, exporting = false)} to $attributeType") 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/jung/io/GraphMLImport.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.jung.io 2 | 3 | import java.io.{BufferedReader, FileReader} 4 | import java.util.Locale 5 | 6 | import edu.uci.ics.jung 7 | import edu.uci.ics.jung.graph.util.EdgeType 8 | import edu.uci.ics.jung.io.graphml.Metadata.MetadataType 9 | import edu.uci.ics.jung.io.graphml.{AbstractMetadata, EdgeMetadata, GraphMLReader2, GraphMetadata, Key, NodeMetadata} 10 | import org.nlogo.agent.{Agent, AgentSet, Link, Turtle, World} 11 | import org.nlogo.api.{AgentException, ExtensionException, MersenneTwisterFast} 12 | import org.nlogo.extensions.nw.NetworkExtensionUtil.{createTurtle, using} 13 | import org.nlogo.extensions.nw.jung.{createLink, sparseGraphFactory, transformer} 14 | 15 | import scala.jdk.CollectionConverters.{ CollectionHasAsScala, ListHasAsScala, MapHasAsScala, SetHasAsScala } 16 | 17 | object GraphMLImport { 18 | 19 | trait GraphElement { 20 | val metadata: AbstractMetadata 21 | def id: String 22 | } 23 | 24 | case class Vertex(metadata: NodeMetadata) extends GraphElement { 25 | def id = metadata.getId 26 | } 27 | 28 | case class Edge(metadata: EdgeMetadata) extends GraphElement { 29 | def id = metadata.getId 30 | } 31 | 32 | object Attribute { 33 | def apply(name: String, attributeType: String, value: String) = 34 | new Attribute( 35 | name.toUpperCase(Locale.ENGLISH), 36 | attributeType.toUpperCase(Locale.ENGLISH), 37 | Option(value).getOrElse("")) 38 | } 39 | class Attribute private ( 40 | val name: String, 41 | val attributeType: String, 42 | val value: String) { 43 | def valueObject: AnyRef = 44 | try { 45 | attributeType match { 46 | case "BOOLEAN" => Boolean.box(value.toBoolean) 47 | case "INT" => Double.box(value.toDouble) 48 | case "LONG" => Double.box(value.toDouble) 49 | case "FLOAT" => Double.box(value.toDouble) 50 | case "DOUBLE" => Double.box(value.toDouble) 51 | case "STRING" => value 52 | case _ => 53 | // trial and errors for unknown types 54 | try Double.box(value.toDouble) 55 | catch { 56 | case _: Exception => 57 | try Boolean.box(value.toBoolean) 58 | catch { case _: Exception => value } // string as a final resort 59 | } 60 | } 61 | } catch { 62 | // If anything fails, we return the value as a string. 63 | // TODO: use a proper XML parser eventually! NP 2013-02-15 64 | case e: Exception => value 65 | } 66 | } 67 | 68 | def attributes(e: GraphElement, keys: Seq[Key]): Seq[Attribute] = { 69 | val properties = e.metadata.getProperties.asScala 70 | val as = Attribute("ID", "STRING", e.id) +: 71 | (for { 72 | key <- keys 73 | value <- properties.get(key.getId).orElse(Option(key.defaultValue)) 74 | attributeName = Option(key.getAttributeName).getOrElse(key.getId) 75 | attributeType = Option(key.getAttributeType).getOrElse("") 76 | } yield Attribute(attributeName, attributeType, value)) 77 | as.sortBy(_.name != "BREED") // BREED first 78 | } 79 | 80 | // private def setBreed(agent: Agent, breed: String) { 81 | // agent match { 82 | // case t: Turtle => 83 | // for (breedAgentSet <- Option(agent.world.getBreed(breed))) 84 | // t.setTurtleOrLinkVariable("BREED", breedAgentSet) 85 | // case l: Link => 86 | // for (breedAgentSet <- Option(agent.world.getLinkBreed(breed))) 87 | // l.setTurtleOrLinkVariable("BREED", breedAgentSet) 88 | // } 89 | // } 90 | 91 | private def setAgentVariable(agent: Agent, attribute: Attribute): Unit = { 92 | if (attribute.name != "WHO") // don' try to set WHO 93 | try { 94 | val program = agent.world.program 95 | agent match { 96 | case l: Link => 97 | attribute.name match { 98 | case "BREED" => // Breed is set separately 99 | case v if program.linksOwn.indexOf(v) != -1 => 100 | agent.setTurtleOrLinkVariable(v, attribute.valueObject) 101 | case v => 102 | agent.setLinkBreedVariable(v, attribute.valueObject) 103 | } 104 | case t: Turtle => 105 | attribute.name match { 106 | case "BREED" => // Breed is set separately 107 | case v if program.turtlesOwn.indexOf(v) != -1 => 108 | agent.setTurtleOrLinkVariable(v, attribute.valueObject) 109 | case v => 110 | agent.setBreedVariable(v, attribute.valueObject) 111 | } 112 | case _ => 113 | throw new IllegalStateException 114 | } 115 | } catch { 116 | case e: AgentException => // Variable just does not exist - move on 117 | case e: Exception => throw new ExtensionException(e) 118 | } 119 | } 120 | 121 | private def createAgents[E <: GraphElement, A <: Agent](elements: Iterable[E], 122 | keys: Seq[Key], 123 | defaultBreed: AgentSet, 124 | breeds: String => AgentSet) 125 | (create: (E, AgentSet) => A): Map[E, A] = 126 | elements.map { elem => 127 | val attrs = attributes(elem, keys) 128 | val breed = attrs.find(_.name == "BREED").map{ 129 | _.valueObject.toString.toUpperCase(Locale.ENGLISH) 130 | }.flatMap(name => Option(breeds(name))).getOrElse(defaultBreed) 131 | val agent = create(elem, breed) 132 | attrs.foreach { setAgentVariable(agent, _) } 133 | elem -> agent 134 | }.toMap 135 | 136 | def load(fileName: String, world: World, rng: MersenneTwisterFast): Iterator[Turtle] = { 137 | try { 138 | val graphFactory = sparseGraphFactory[Vertex, Edge] 139 | val graphTransformer = transformer { (_: GraphMetadata) => graphFactory.create } 140 | 141 | using { 142 | new GraphMLReader2[jung.graph.Graph[Vertex, Edge], Vertex, Edge]( 143 | new BufferedReader(new FileReader(fileName)), 144 | graphTransformer, 145 | transformer(Vertex.apply), 146 | transformer(Edge.apply), 147 | transformer(_ => Edge(null))) 148 | } { graphReader => 149 | val graph = graphReader.readGraph() 150 | 151 | val keyMap: Map[MetadataType, Seq[Key]] = 152 | graphReader 153 | .getGraphMLDocument.getKeyMap.entrySet() 154 | .asScala.map(entry => entry.getKey -> entry.getValue.asScala.toSeq).toMap 155 | 156 | // The vertices have non-deterministic order, so we need to sort them by id 157 | // so that the turtles are created in the same order every time. Otherwise 158 | // we end up with an isomorphic, but non-identical network (up to turtle id) 159 | // - BCH 8/7/2018 160 | val vertices = graph.getVertices.asScala.toList.sortBy(_.id) 161 | 162 | val turtles: Map[Vertex, Turtle] = 163 | createAgents(vertices, keyMap(MetadataType.NODE), world.turtles, world.getBreed) { 164 | (_, breed) => createTurtle(world, breed, rng) 165 | } 166 | 167 | createAgents(graph.getEdges.asScala, keyMap(MetadataType.EDGE), world.links, world.getLinkBreed) { 168 | (e: Edge, breed) => 169 | createLink(turtles, graph.getEndpoints(e), graph.getDefaultEdgeType == EdgeType.DIRECTED, breed, world) 170 | } 171 | 172 | // The `turtles` map also has non-deterministic order (likely since the keys 173 | // are being hashed by reference address rather than a deterministic hash 174 | // function). We need to return the turtles in a deterministic order, however, 175 | // so initialization code always runs in the same order. We do this by mapping 176 | // `vertices`, which is ordered, so that we don't have to do another sort. We 177 | // do this lazily to avoid it completely if there is no initialization code. 178 | // - BCH 8/7/2018 179 | vertices.iterator.map(turtles) 180 | } 181 | } catch { 182 | case e: Exception => throw new ExtensionException(e) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/PathFinder.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.agent.World 4 | import scala.util.Random 5 | import collection.mutable 6 | import scala.collection.mutable.ArrayBuffer 7 | import org.nlogo.extensions.nw.util.{Cache, CacheManager} 8 | import org.nlogo.extensions.nw.Graph 9 | 10 | class PathFinder[V,E](graph: Graph[V, E], world: World, weightFunction: (String) => (E) => Double) { 11 | private val distanceCaches = CacheManager[(V, V), Double](world) 12 | private val predecessorCaches = CacheManager(world, (_: Option[String]) => (p: (V, V)) => ArrayBuffer.empty[V]) 13 | private val successorCaches = CacheManager(world, (_: Option[String]) => (p: (V, V)) => ArrayBuffer.empty[V]) 14 | private val singleSourceTraversalCaches = CacheManager[V, Iterator[V]](world, { 15 | case None => { 16 | (source: V) => cachingBFS(source, reverse = false, predecessorCaches(None)) 17 | } 18 | case Some(varName: String) => { 19 | (source: V) => 20 | cachingDijkstra(source, weightFunction(varName), reverse = false, 21 | predecessorCaches(Some(varName)), distanceCaches(Some(varName))) 22 | } 23 | }: (Option[String]) => V => Iterator[V]) 24 | 25 | private val singleDestTraversalCaches = CacheManager[V, Iterator[V]](world, { 26 | case None => { 27 | (source: V) => cachingBFS(source, reverse = true, successorCaches(None)) 28 | } 29 | case Some(varName: String) => { 30 | (source: V) => 31 | cachingDijkstra(source, weightFunction(varName), reverse = true, 32 | successorCaches(Some(varName)), distanceCaches(Some(varName))) 33 | } 34 | }: (Option[String]) => V => Iterator[V]) 35 | 36 | private var lastSource: Option[V] = None 37 | private var lastDest: Option[V] = None 38 | 39 | /** 40 | * Attempts to expand the cache with the least duplicated amount of work possible. It tries to detect users doing 41 | * single-source and single-destination. Failing that, it expands the caching BFS that is the furthest along, since 42 | * that one is guaranteed to finish the quickest. 43 | * @param source 44 | * @param dest 45 | */ 46 | private def expandBestTraversal(variable: Option[String], source: V, dest: V): Unit = { 47 | val sourceTraversal = singleSourceTraversalCaches(variable)(source) 48 | val destTraversal = singleDestTraversalCaches(variable)(dest) 49 | // If one doesn't have a next, the nodes are disconnected 50 | if (sourceTraversal.hasNext && destTraversal.hasNext) { 51 | if (lastSource.exists(_ == source)) { 52 | sourceTraversal find { _ == dest } 53 | } else if (lastDest.exists(_ == dest)) { 54 | destTraversal find { _ == source } 55 | } else { 56 | val sourcePosition = sourceTraversal.next() 57 | val destPosition = destTraversal.next() 58 | if (sourcePosition != dest && destPosition != source) { 59 | if (distanceCaches(variable)((source, sourcePosition)) >= distanceCaches(variable)((destPosition, dest))) { 60 | sourceTraversal find { _ == dest } 61 | } 62 | if (distanceCaches(variable)((source, sourcePosition)) <= distanceCaches(variable)((destPosition, dest))) { 63 | destTraversal find { _ == source } 64 | } 65 | } 66 | } 67 | } 68 | lastSource = Some(source) 69 | lastDest = Some(dest) 70 | } 71 | 72 | private def cachedPath(cache: ((V, V)) => ArrayBuffer[V], source: V, dest: V, rng: Random): Option[List[V]] 73 | = { 74 | if (source == dest) { 75 | Some(List(dest)) 76 | } else { 77 | val availableSuccessors = cache((source, dest)) 78 | if (availableSuccessors.nonEmpty) { 79 | val succ = availableSuccessors(rng.nextInt(availableSuccessors.length)) 80 | cachedPath(cache, succ, dest, rng) map {source :: _ } 81 | } else { 82 | None 83 | } 84 | } 85 | } 86 | 87 | private def cachedPath(variable: Option[String], source: V, dest: V, rng: Random): Option[List[V]] = 88 | cachedPath(successorCaches(variable), source, dest, rng) orElse cachedPath(predecessorCaches(variable), dest, source, rng).map(_.reverse) 89 | 90 | def path(source: V, dest: V, rng: Random, weightVariable: Option[String] = None): Option[Iterable[V]] = { 91 | cachedPath(weightVariable, source, dest, rng) orElse { 92 | expandBestTraversal(weightVariable, source, dest) 93 | cachedPath(weightVariable, source, dest, rng) 94 | } 95 | } 96 | 97 | def distance(source: V, dest: V, weightVariable: Option[String] = None): Option[Double] = { 98 | distanceCaches(weightVariable).get((source, dest)) orElse { 99 | expandBestTraversal(weightVariable, source, dest) 100 | distanceCaches(weightVariable).get((source, dest)) 101 | } 102 | } 103 | 104 | // TODO: Separate caching and traversing by having traversal return an iterator over (V, Distance, Predecessor) 105 | // This may be impossible: the caching needs to know about items not actually returned by the traversal (it needs 106 | // to visit each node once for each predecessor, rather than just once). I tried just having the traversal return 107 | // nodes for each predecessor but performance was insane. -BCH 4/30/2014 108 | /* 109 | This allows us to calculate and store the min spanning tree of start lazily. 110 | As it traverses the tree, it stores the predecessor and distance information. 111 | Although the iterator returns one turtle at a time, data about turtles is 112 | computed a layer at a time so that the cache ends up with complete predecessor 113 | information for any turtle appearing there. This is crucial or else this class 114 | will thinks it's done computing paths for a certain pair when it has not. 115 | */ 116 | private def cachingBFS(start: V, reverse: Boolean, predecessorCache: ((V, V)) => ArrayBuffer[V]): Iterator[V] = { 117 | val neighbors = if (reverse) graph.inNeighbors else graph.outNeighbors 118 | val dists = mutable.Map[(V,V), Int]() 119 | dists((start, start)) = 0 120 | 121 | // note that I can't use the global distances cache to detect visited nodes since 122 | // the same slot can be filled by either a BFS or reverse BFS. 123 | val distances = distanceCaches(None) 124 | distances((start, start)) = 0 125 | Iterator.iterate(List(start))((last) => { 126 | var layer: List[V] = List() 127 | for { 128 | node <- last 129 | distance = dists((start, node)) 130 | neighbor <- neighbors(node) 131 | } { 132 | if (!dists.contains((start, neighbor))) { 133 | dists((start, neighbor)) = distance + 1 134 | if (reverse) { 135 | distances((neighbor, start)) = distance + 1 136 | } else { 137 | distances((start, neighbor)) = distance + 1 138 | } 139 | layer = neighbor :: layer 140 | } 141 | if (dists((start, neighbor)) == distance + 1) { 142 | predecessorCache((neighbor, start)).append(node) 143 | } 144 | } 145 | layer 146 | }).takeWhile(_.nonEmpty).flatten 147 | } 148 | 149 | private def cachingDijkstra(start: V, weight: E => Double, reverse: Boolean, predecessorCache: ((V, V)) => ArrayBuffer[V], distanceCache: Cache[(V, V), Double]): Iterator[V] = { 150 | val edges = if (reverse) graph.inEdges else graph.outEdges 151 | val dists = mutable.Map[V, Double]() 152 | val heap = mutable.PriorityQueue[(V, Double, V)]()(using Ordering[Double].on(-_._2)) 153 | distanceCache(start -> start) = 0 154 | Iterator.continually { 155 | val curDistance = heap.headOption map { _._2 } getOrElse 0.0 156 | heap.enqueue((start, 0, start)) 157 | var layer: List[V] = List() 158 | while (heap.nonEmpty && heap.head._2 <= curDistance) { 159 | val (turtle, distance, predecessor) = heap.dequeue() 160 | val alreadyAdded = dists contains turtle 161 | if (!alreadyAdded || dists(turtle) >= distance) { 162 | if (!alreadyAdded) { 163 | layer = turtle :: layer 164 | dists(turtle) = distance 165 | if (reverse) { 166 | distanceCache(turtle -> start) = distance 167 | } else { 168 | distanceCache(start -> turtle) = distance 169 | } 170 | edges(turtle).foreach { link => 171 | val other = graph.otherEnd(turtle)(link) 172 | val dist = distance + weight(link) 173 | if (!(dists contains other)) { 174 | heap.enqueue((other, dist, turtle)) 175 | } 176 | } 177 | } 178 | if (turtle != predecessor) predecessorCache((turtle, start)).append(predecessor) 179 | 180 | } 181 | } 182 | layer 183 | }.takeWhile(x => heap.nonEmpty).flatten 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /USING.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | The first thing that one needs to understand in order to work with the network extension is how to tell the extension _which_ network to work with. Consider the following example situation: 4 | 5 | ``` 6 | breed [ bankers banker ] 7 | breed [ clients client ] 8 | 9 | undirected-link-breed [ friendships friendship ] 10 | directed-link-breed [ accounts account ] 11 | ``` 12 | 13 | Basically, you have bankers and clients. Clients can have accounts with bankers. Bankers can probably have account with other bankers, and anyone can be friends with anyone. 14 | 15 | Now we might want to consider this whole thing as one big network. If that is the case, there is nothing special to do: by default, the NW extension primitives consider all turtles and all links to be part of the current network. 16 | 17 | We could also, however, be only interested in a subset of the network. Maybe we want to consider only friendship relations. Furthermore, maybe we want to consider only the friendships _between bankers_. After all, having a very high centrality in a network of banker friendships is very different from having a high centrality in a network of client friendships. 18 | 19 | To specify such networks, we need to tell the extension _both_ which turtles _and_ which links we are interested in. All the turtles from the specified set of turtles will be included in the network, and only the links from the specified set of links that are between turtles of the specified set will be included. For example, if you ask for `bankers` and `friendships`, even the lonely bankers with no friends will be included, but friendship links between bankers and clients will **not** be included. The way to tell the extension about this is with the [`nw:set-context`](#nwset-context) primitive, which you must call _prior_ to doing any operations on a network. 20 | 21 | Some examples: 22 | 23 | - `nw:set-context turtles links` will give you everything: bankers and clients, friendships and accounts, as one big network. 24 | - `nw:set-context turtles friendships` will give you all the bankers and clients and friendships between any of them. 25 | - `nw:set-context bankers friendships` will give you all the bankers, and only friendships between bankers. 26 | - `nw:set-context bankers links` will give you all the bankers, and any links between them, whether these links are friendships or accounts. 27 | - `nw:set-context clients accounts` will give you all the clients, and accounts between each other, but since in our fictional example clients can only have accounts with bankers, this will be a completely disconnected network. 28 | 29 | ### Special agentsets vs normal agentsets 30 | 31 | It must be noted that NetLogo has two types of agentsets that behave slightly differently, and that this has an impact on the way `nw:set-context` works. We will say a few words about these concepts here but, for a thorough understanding, it is highly recommended that you read [the section on agentsets in the NetLogo programming guide](http://ccl.northwestern.edu/netlogo/docs/programming.html#agentsets). 32 | 33 | The "special" agentsets in NetLogo are `turtles`, `links` and the different "breed" agentsets. What is special about them is that they can grow: if you create a new turtle, it will be added to the `turtles` agentset. If you have a `bankers` breed and you create a new banker, it will be added to the `bankers` agentset and to the `turtles` agentset. Same goes for links. Other agentsets, such as those created with the `with` primitive (e.g., `turtles with [ color = red ]`) or the `turtle-set` and `link-set` primitives) are never added to. The content of normal agentsets will only change if the agents that they contain die. 34 | 35 | To show how different types of agentsets interact with [`nw:set-context`](#nwset-context), let's create a very simple network: 36 | 37 | ```NetLogo 38 | clear-all 39 | create-turtles 3 [ create-links-with other turtles ] 40 | ``` 41 | 42 | Let's set the context to `turtles` and `links` (which is the default anyway) and use [`nw:get-context`](#nwget-context) to see what we have: 43 | 44 | ```NetLogo 45 | nw:set-context turtles links 46 | show map sort nw:get-context 47 | ``` 48 | 49 | We get all three turtles and all three links: 50 | 51 | ```NetLogo 52 | [[(turtle 0) (turtle 1) (turtle 2)] [(link 0 1) (link 0 2) (link 1 2)]] 53 | ``` 54 | 55 | Now let's kill one turtle: 56 | 57 | ```NetLogo 58 | ask one-of turtles [ die ] 59 | show map sort nw:get-context 60 | ``` 61 | 62 | As expected, the context is updated to reflect the death of the turtle and of the two links that died with it: 63 | 64 | ```NetLogo 65 | [[(turtle 0) (turtle 1)] [(link 0 1)]] 66 | ``` 67 | 68 | What if we now create a new turtle? 69 | 70 | ```NetLogo 71 | create-turtles 1 72 | show map sort nw:get-context 73 | ``` 74 | 75 | Since our context is using the special `turtles` agentset, the new turtle is automatically added: 76 | 77 | ```NetLogo 78 | [[(turtle 0) (turtle 1) (turtle 3)] [(link 0 1)]] 79 | ``` 80 | 81 | Now let's demonstrate how it works with normal agentsets. We start over with a new network of red turtles: 82 | 83 | ```NetLogo 84 | clear-all 85 | create-turtles 3 [ 86 | create-links-with other turtles 87 | set color red 88 | ] 89 | ``` 90 | 91 | And we set the context to `turtles with [ color = red ])` and `links` 92 | 93 | ```NetLogo 94 | nw:set-context (turtles with [ color = red ]) links 95 | show map sort nw:get-context 96 | ``` 97 | 98 | Since all turtles are red, we get everything in our context: 99 | 100 | ```NetLogo 101 | [[(turtle 0) (turtle 1) (turtle 2)] [(link 0 1) (link 0 2) (link 1 2)]] 102 | ``` 103 | 104 | But what if we ask one of them to turn blue? 105 | 106 | ```NetLogo 107 | ask one-of turtles [ set color blue ] 108 | show map sort nw:get-context 109 | ``` 110 | 111 | No change. The agentset used in our context remains unaffected: 112 | 113 | ```NetLogo 114 | [[(turtle 0) (turtle 1) (turtle 2)] [(link 0 1) (link 0 2) (link 1 2)]] 115 | ``` 116 | 117 | If we kill one of them, however... 118 | 119 | ```NetLogo 120 | ask one-of turtles [ die ] 121 | show map sort nw:get-context 122 | ``` 123 | 124 | It gets removed from the set: 125 | 126 | ```NetLogo 127 | [[(turtle 0) (turtle 2)] [(link 0 2)]] 128 | ``` 129 | 130 | What if we add a new red turtle? 131 | 132 | ```NetLogo 133 | create-turtles 1 [ set color red ] 134 | show map sort nw:get-context 135 | ``` 136 | 137 | Nope: 138 | 139 | ```NetLogo 140 | [[(turtle 0) (turtle 2)] [(link 0 2)]] 141 | ``` 142 | 143 | ## A note regarding floating point calculations 144 | 145 | Neither [JGraphT](https://github.com/jgrapht) nor [Jung](http://jung.sourceforge.net/), the two network libraries that we use internally, use [`strictfp` floating point calculations](https://en.wikipedia.org/wiki/Strictfp). This does mean that exact reproducibility of results involving floating point calculations _between different hardware architectures_ is not fully guaranteed. (NetLogo itself [always uses strict math](http://ccl.northwestern.edu/netlogo/docs/faq.html#are-netlogo-models-runs-scientifically-reproducible) so this only applies to some primitives of the NW extension.) 146 | 147 | ## Performance 148 | 149 | In order to be fast in as many circumstances as possible, the NW extension tries hard to never calculate things twice. It remembers all paths, distances, and centralities that it calculates. So, while the first time you ask for the distance between `turtle 0` and `turtle 3782` may take some time, after that, it should be almost instantaneous. Furthermore, it keeps track of values it just happened to calculate along the way. For example, if `turtle 297` is closer to `turtle 0` than `turtle 3782` is, it may just happen to figure out the distance between `turtle 0` and `turtle 297` while it figures out the distance between `turtle 0` and `turtle 3782`. It will remember this value, so that if you ask it for the distance between `turtle 0` and `turtle 297`, it doesn't have to do all that work again. 150 | 151 | There are a few circumstances where the NW extension has to forget things. If the network changes at all (you add turtles or links, or remove turtles or links), it has to forget everything. For weighted primitives, if the value of the weight variable changes for any of the links in the network, it will forget the values associated with that weight variable. 152 | 153 | If you're working on a network that can change regularly, try to do all your network calculations at once, then all your network changes at once. The more your interweave network calculations and network changes, the more the NW extension will have to recalculate things. For example, if you have a traffic model, and cars need to figure out the shortest path to their destination based on the traffic each tick, have all the cars find their shortest paths, then change the network weights to account for how traffic has changed. 154 | 155 | There may be rare occasions in which you don't want the NW extension to remember values. For example, if you're working on an extremely large network, remembering all those values may take more memory than you have. In that case, you can just call `nw:set-context (first nw:get-context) (last nw:get-context)` to force the NW extension to immediately forget everything. 156 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/algorithms/Clustering.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.algorithms 2 | 3 | import org.nlogo.extensions.nw.Graph 4 | import org.nlogo.extensions.nw.algorithms.Louvain.CommunityStructure.Community 5 | 6 | import scala.util.Random 7 | 8 | object ClusteringMetrics { 9 | def clusteringCoefficient[V,E](graph: Graph[V,E], node: V): Double = { 10 | val neighbors = graph.outNeighbors(node) 11 | val neighborSet = neighbors.toSet 12 | if (neighbors.size < 2) { 13 | 0 14 | } else { 15 | val neighborLinkCounts = neighbors map { 16 | (t: V) => graph.outNeighbors(t).count(neighborSet.contains) 17 | } 18 | 19 | neighborLinkCounts.sum.toDouble / (neighbors.size * (neighbors.size - 1)) 20 | } 21 | } 22 | 23 | def modularity[V,E](graph: Graph[V, E], communities: Iterable[Set[V]]): Double = { 24 | communities.map(c => communityModularity(graph, c)).sum 25 | } 26 | 27 | def communityModularity[V,E](graph: Graph[V,E], community: Set[V]): Double = { 28 | var totalIn: Double = 0 29 | var totalOut: Double = 0 30 | var internal: Double = 0 31 | community.foreach { node => 32 | graph.outEdges(node).foreach { edge => 33 | val weight = graph.weight(edge) 34 | if (community contains graph.otherEnd(node)(edge)) internal += weight 35 | totalOut += weight 36 | } 37 | graph.inEdges(node).foreach { edge => totalIn += graph.weight(edge) } 38 | } 39 | (internal - totalIn * totalOut / graph.totalArcWeight) / graph.totalArcWeight 40 | } 41 | 42 | } 43 | 44 | object Louvain { 45 | def cluster[V,E](graph: Graph[V,E], rng: Random): Seq[Seq[V]] = { 46 | val initComms = clusterLocally(graph, rng) 47 | if (initComms.communities.size < graph.nodes.size) { 48 | val mGraph = MergedGraph(graph, initComms) 49 | val metaComms: Seq[Seq[Community[V]]] = cluster(mGraph, rng) 50 | metaComms.map(_.flatMap(initComms.members)) 51 | } else { 52 | initComms.communities.map(initComms.members) 53 | } 54 | } 55 | 56 | 57 | def clusterLocally[V,E](graph: Graph[V,E], rng: Random): CommunityStructure[V,E] = { 58 | var comStruct = CommunityStructure(graph) 59 | 60 | var switchOccurred = true 61 | // Note that GraphContext.nodes is a SortedSet, so we need to convert to a Seq to get a deterministic shuffle order. 62 | val nodesSeq = graph.nodes.toSeq 63 | while (switchOccurred) { 64 | switchOccurred = false 65 | rng.shuffle(nodesSeq).foreach { v => 66 | // Note that the original community is almost certainly in the connected communities, so we remove it 67 | val originalCommunity: Community[V] = comStruct.community(v) 68 | val connectedCommunities: Seq[Community[V]] = rng.shuffle( 69 | graph.outNeighbors(v).map(comStruct.community).filterNot(_ == originalCommunity).distinct 70 | ) 71 | if (connectedCommunities.nonEmpty) { 72 | val newComStruct = connectedCommunities.map(comStruct.move(v, _)).maxBy(_.modularity) 73 | // Require a strictly better score to switch. 74 | if (newComStruct.modularity > comStruct.modularity) { 75 | comStruct = newComStruct 76 | switchOccurred = true 77 | } 78 | } 79 | } 80 | } 81 | comStruct 82 | } 83 | 84 | object CommunityStructure { 85 | type Community[V] = Int 86 | 87 | def apply[V,E](graph: Graph[V,E]): CommunityStructure[V,E] = { 88 | val comMap: Map[V, Community[V]] = graph.nodes.zipWithIndex.toMap 89 | val internal = Array.fill(graph.nodes.size)(0.0) 90 | val totalIn = Array.fill(graph.nodes.size)(0.0) 91 | val totalOut = Array.fill(graph.nodes.size)(0.0) 92 | graph.nodes.foreach { node => 93 | val com = comMap(node) 94 | graph.outEdges(node).foreach { edge => 95 | val weight = graph.weight(edge) 96 | val other = graph.otherEnd(node)(edge) 97 | if (com == comMap(other)) internal(com) += weight 98 | totalOut(com) += weight 99 | totalIn(comMap(other)) += weight 100 | } 101 | } 102 | val mod = internal.lazyZip(totalIn).lazyZip(totalOut).map { case (intern: Double, in: Double, out: Double) => 103 | (intern - in * out / graph.totalArcWeight) / graph.totalArcWeight 104 | }.sum 105 | new CommunityStructure[V,E](graph, comMap, internal.toVector, totalIn.toVector, totalOut.toVector, mod) 106 | } 107 | 108 | // Very slow; only used for testing. Could speed it up by calculating the initial values for each of the vectors 109 | // but am too lazy for test code. 110 | def apply[V,E](graph: Graph[V,E], groups: Seq[Seq[V]]): CommunityStructure[V,E] = 111 | groups.zipWithIndex.flatMap { case (vs, i) => vs.map(_ -> i) }.foldLeft(CommunityStructure(graph)) { 112 | (cs: CommunityStructure[V,E], pair: (V, Community[V])) => cs.move(pair._1, pair._2) 113 | } 114 | } 115 | 116 | class CommunityStructure[V,E](graph: Graph[V,E], 117 | comMap: Map[V, Community[V]], 118 | internal: Vector[Double], 119 | totalIn: Vector[Double], 120 | totalOut: Vector[Double], 121 | val modularity: Double) { 122 | 123 | def community(node: V): Community[V] = comMap(node) 124 | 125 | def move(node: V, newCommunity: Community[V]): CommunityStructure[V,E] = { 126 | val originalCommunity = community(node) 127 | val otherEnd = graph.otherEnd(node) 128 | var inDegree = 0.0 129 | var outDegree = 0.0 130 | var internalOriginal = 0.0 131 | var internalNew = 0.0 132 | graph.outEdges(node).foreach { link => 133 | val other = otherEnd(link) 134 | val comOther = community(other) 135 | val weight = graph.weight(link) 136 | outDegree += weight 137 | if (other == node) { 138 | internalOriginal += weight 139 | internalNew += weight 140 | } else if (comOther == originalCommunity) { 141 | internalOriginal += weight 142 | } else if (comOther == newCommunity) { 143 | internalNew += weight 144 | } 145 | } 146 | graph.inEdges(node).foreach { link => 147 | val other = otherEnd(link) 148 | val comOther = community(other) 149 | val weight = graph.weight(link) 150 | inDegree += weight 151 | if (other == node) { 152 | internalOriginal += weight 153 | internalNew += weight 154 | } else if (comOther == originalCommunity) { 155 | internalOriginal += weight 156 | } else if (comOther == newCommunity) { 157 | internalNew += weight 158 | } 159 | } 160 | 161 | val newTotalIn = totalIn 162 | .updated(originalCommunity, totalIn(originalCommunity) - inDegree) 163 | .updated(newCommunity, totalIn(newCommunity) + inDegree) 164 | val newTotalOut = totalOut 165 | .updated(originalCommunity, totalOut(originalCommunity) - outDegree) 166 | .updated(newCommunity, totalOut(newCommunity) + outDegree) 167 | val newInternal = internal 168 | .updated(originalCommunity, internal(originalCommunity) - internalOriginal) 169 | .updated(newCommunity, internal(newCommunity) + internalNew) 170 | val contrib = (com: Int, intern: Vector[Double], in: Vector[Double], out: Vector[Double]) => 171 | (intern(com) - in(com) * out(com) / graph.totalArcWeight) / graph.totalArcWeight 172 | 173 | val deltaOriginal = 174 | contrib(originalCommunity, newInternal, newTotalIn, newTotalOut) - 175 | contrib(originalCommunity, internal, totalIn, totalOut) 176 | val deltaNew = 177 | contrib(newCommunity, newInternal, newTotalIn, newTotalOut) - 178 | contrib(newCommunity, internal, totalIn, totalOut) 179 | new CommunityStructure[V,E](graph, 180 | comMap.updated(node, newCommunity), 181 | newInternal, newTotalIn, newTotalOut, modularity + deltaOriginal + deltaNew) 182 | } 183 | 184 | lazy val communities: Seq[Community[V]] = comMap.values.toSeq.distinct.sorted 185 | 186 | private lazy val _members: Map[Community[V], Seq[V]] = graph.nodes.toSeq.groupBy(comMap) 187 | def members(community: Community[V]): Seq[V] = _members(community) 188 | 189 | } 190 | 191 | case class WeightedLink[V](end1:V, end2: V, weight: Double) 192 | 193 | case class MergedGraph[V,E](graph: Graph[V, E], communityStructure: CommunityStructure[V,E]) 194 | extends Graph[Community[V], WeightedLink[Community[V]]] { 195 | 196 | override val nodes: Seq[Community[V]] = communityStructure.communities 197 | 198 | val edges: Seq[WeightedLink[Community[V]]] = communityStructure.communities.flatMap { (sourceCom: Community[V]) => 199 | communityStructure.members(sourceCom).flatMap { source => 200 | graph.outEdges(source).map { e => 201 | communityStructure.community(graph.otherEnd(source)(e)) -> graph.weight(e) 202 | } 203 | }.groupBy(_._1).map { case (targetCom, ws) => 204 | WeightedLink(sourceCom, targetCom, ws.map(_._2).sum) 205 | } 206 | } 207 | 208 | val outEdgeMap: Map[Community[V], Seq[WeightedLink[Community[V]]]] = 209 | edges.groupBy(_.end1).withDefaultValue(Seq.empty[WeightedLink[Community[V]]]) 210 | val inEdgeMap: Map[Community[V], Seq[WeightedLink[Community[V]]]] = 211 | edges.groupBy(_.end2).withDefaultValue(Seq.empty[WeightedLink[Community[V]]]) 212 | 213 | override def inEdges(node: Community[V]): Seq[WeightedLink[Community[V]]] = inEdgeMap(node) 214 | override def outEdges(node: Community[V]): Seq[WeightedLink[Community[V]]] = outEdgeMap(node) 215 | override def weight(link: WeightedLink[Community[V]]): Double = link.weight 216 | 217 | override def ends(link: WeightedLink[Community[V]]): (Community[V], Community[V]) = link.end1 -> link.end2 218 | } 219 | } 220 | 221 | -------------------------------------------------------------------------------- /src/main/org/nlogo/extensions/nw/gephi/GephiImport.scala: -------------------------------------------------------------------------------- 1 | package org.nlogo.extensions.nw.gephi 2 | 3 | import java.awt.Color 4 | import java.io.{ File, FileReader, IOException } 5 | import java.lang.{ Boolean => JBoolean, Double => JDouble } 6 | import java.util.Collection 7 | 8 | import scala.collection.Map 9 | import scala.jdk.CollectionConverters.{ CollectionHasAsScala, IterableHasAsScala, MapHasAsScala } 10 | import scala.language.postfixOps 11 | import scala.util.control.Exception.allCatch 12 | 13 | import org.nlogo.agent.{ Agent, AgentSet, Turtle, World } 14 | import org.nlogo.api._ 15 | import org.nlogo.core.LogoList 16 | 17 | import org.gephi.graph.api.types.TimeMap 18 | import org.gephi.io.importer.api.{ EdgeDirection, EdgeDirectionDefault, EdgeDraft, ElementDraft, ImportController, NodeDraft } 19 | import org.gephi.io.importer.plugin.file.{ ImporterGDF, ImporterGraphML } 20 | import org.gephi.io.importer.plugin.file.spreadsheet.ImporterSpreadsheetCSV 21 | import org.gephi.io.importer.spi.FileImporter 22 | import org.openide.util.Lookup 23 | 24 | import org.nlogo.extensions.nw.NetworkExtensionUtil._ 25 | 26 | object GephiImport { 27 | val importController = GephiUtils.withNWLoaderContext { 28 | Lookup.getDefault.lookup(classOf[ImportController]) 29 | } 30 | 31 | def load(file: File, world: World, 32 | defaultTurtleBreed: AgentSet, defaultLinkBreed: AgentSet, 33 | initTurtles: IterableOnce[Turtle] => Unit): Unit = GephiUtils.withNWLoaderContext { 34 | load(file, world, defaultTurtleBreed, defaultLinkBreed, initTurtles, importController.getFileImporter(file)) 35 | } 36 | 37 | def load(file: File, world: World, 38 | defaultTurtleBreed: AgentSet, defaultLinkBreed: AgentSet, 39 | initTurtles: IterableOnce[Turtle] => Unit, 40 | extension: String): Unit = GephiUtils.withNWLoaderContext { 41 | load(file, world, defaultTurtleBreed, defaultLinkBreed, initTurtles, importController.getFileImporter(extension)) 42 | } 43 | 44 | def load(file: File, world: World, 45 | defaultTurtleBreed: AgentSet, defaultLinkBreed: AgentSet, 46 | initTurtles: IterableOnce[Turtle] => Unit, 47 | importer: FileImporter): Unit = GephiUtils.withNWLoaderContext { 48 | if (!file.exists) { 49 | throw new ExtensionException("The file " + file + " cannot be found.") 50 | } 51 | 52 | val isGdfImport = importer match { 53 | case null => 54 | throw new ExtensionException("Unable to find importer for " + file) 55 | 56 | case _: ImporterSpreadsheetCSV => 57 | throw new ExtensionException("Importing CSV files is not supported.") 58 | 59 | case _: ImporterGraphML => 60 | throw new ExtensionException("You must use nw:load-graphml to load graphml files.") 61 | 62 | case _: ImporterGDF => 63 | true 64 | 65 | case _ => 66 | false 67 | } 68 | 69 | val turtleBreeds = world.breeds.asScala 70 | val linkBreeds = world.linkBreeds.asScala 71 | 72 | val container = try { 73 | using(new FileReader(file))(r => importController.importFile(r, importer)) 74 | } catch { 75 | case e: IOException => 76 | throw new ExtensionException(e) 77 | } 78 | 79 | val unloader = container.getUnloader 80 | 81 | val defaultDirected = unloader.getEdgeDefault == EdgeDirectionDefault.DIRECTED 82 | 83 | val nodes: Iterable[NodeDraft] = unloader.getNodes.asScala 84 | val edges: Iterable[EdgeDraft] = unloader.getEdges.asScala 85 | 86 | val nodeToTurtle: Map[NodeDraft, Turtle] = nodes zip nodes.map { 87 | node => { 88 | val attrs = getAttributes(node) ++ 89 | pair("LABEL", node.getLabel) ++ 90 | pair("LABEL-COLOR", node.getLabelColor) ++ 91 | pair("COLOR", node.getColor) 92 | 93 | // Note that node's have a getSize. This does not correspond to the `size` attribute in files so should not be 94 | // used. BCH 1/21/2015 95 | 96 | val breed = getBreed(attrs, turtleBreeds).getOrElse(defaultTurtleBreed) 97 | val turtle = createTurtle(world, breed, world.mainRNG) 98 | (attrs - "BREED") foreach (setAttribute(world, turtle)).tupled 99 | turtle 100 | } 101 | } toMap 102 | 103 | val badEdges: Iterable[EdgeDraft] = edges.map { edge => 104 | val source = nodeToTurtle(edge.getSource) 105 | val target = nodeToTurtle(edge.getTarget) 106 | // There are three gephi edge types: directed, undirected, and mutual. Mutual is pretty much just indicating that 107 | // and edge goes both ways/there are two edges in either direction, so we treat it as either. BCH 1/22/2015 108 | val attrs = getAttributes(edge) ++ 109 | pair("LABEL", edge.getLabel) ++ 110 | pair("LABEL-COLOR", edge.getLabelColor) ++ 111 | pair("COLOR", edge.getColor) ++ 112 | pair("WEIGHT", edge.getWeight.toDouble: JDouble) 113 | 114 | val breed = getBreed(attrs, linkBreeds).getOrElse(defaultLinkBreed) 115 | 116 | val gephiDirected = edge.getDirection == EdgeDirection.DIRECTED 117 | val gephiUndirected = edge.getDirection == EdgeDirection.UNDIRECTED 118 | val gephiUnset = edge.getDirection == null 119 | 120 | val bad = if (breed.isDirected == breed.isUndirected) { 121 | // This happens when the directedness of the default breed hasn't been set yet 122 | if (gephiUnset) { 123 | breed.setDirected(defaultDirected) 124 | } else { 125 | breed.setDirected(gephiDirected) 126 | } 127 | false 128 | } else { 129 | // We have to special case on the GDF import as it defaults to undirected edges on 130 | // import in the latest gephi version. Those will conflict, but it's not "wrong", 131 | // that's just how they come out. https://github.com/gephi/gephi/issues/906 132 | // -Jeremy B July 2022 133 | ((breed.isDirected && gephiUndirected && !isGdfImport) || (breed.isUndirected && gephiDirected)) 134 | } 135 | 136 | val links = List(world.linkManager.createLink(source, target, breed)) ++ { 137 | if (breed.isDirected && gephiUnset) 138 | Some(world.linkManager.createLink(target, source, breed)) 139 | else 140 | None 141 | } 142 | 143 | links foreach { l => (attrs - "BREED") foreach (setAttribute(world, l)).tupled } 144 | 145 | if (bad) { 146 | Some(edge) 147 | } else { 148 | None 149 | } 150 | }.collect({ case Some(e: EdgeDraft) => e }) 151 | 152 | initTurtles(nodeToTurtle.values) 153 | 154 | if (badEdges.nonEmpty) { 155 | val edgesList = badEdges.map(e => e.getSource.getId + "->" + e.getTarget.getId).mkString(", ") 156 | val errorMsg = 157 | "The following edges had a directedness different than their assigned breed. They have been given " + 158 | "the directedness of their breed. If you wish to ignore this error, wrap this command in a CAREFULLY:" 159 | throw new ExtensionException(errorMsg + " " + edgesList) 160 | } 161 | } 162 | 163 | private def convertColor(c: Color): LogoList = { 164 | val l = LogoList( 165 | c.getRed.toDouble: JDouble 166 | , c.getGreen.toDouble: JDouble 167 | , c.getBlue.toDouble: JDouble) 168 | if (c.getAlpha != 255) l.lput(c.getAlpha.toDouble: JDouble) 169 | l 170 | } 171 | 172 | private def pair(key: String, value: AnyRef): Option[(String, AnyRef)] = 173 | Option(value) map (v => key -> convertAttribute(key, v)) 174 | 175 | private def getAttributes(el: ElementDraft): scala.collection.immutable.Map[String, AnyRef] = 176 | el.getColumns.asScala.map { c => 177 | val name = c.getId.toUpperCase 178 | val rawValue = el.getValue(c.getId) 179 | val value = convertAttribute(name, rawValue) 180 | (name, value) 181 | }.filter({ case (_, v) => v != null }).toMap 182 | 183 | private def getBreed(attributes: Map[String, AnyRef], breeds: Map[String, AgentSet]): Option[AgentSet] = { 184 | attributes 185 | .get("BREED") 186 | .collect({ case s: String => s.toUpperCase }) 187 | .flatMap( s => breeds.get(s) ) 188 | } 189 | 190 | private val doubleBuiltins = Set("XCOR", "YCOR", "HEADING", "PEN-SIZE", "THICKNESS", "SIZE") 191 | private val booleanBuiltins = Set("HIDDEN") 192 | private val colorBuiltins = Set("COLOR", "LABEL-COLOR") 193 | 194 | private def convertAttribute(name: String, o: Any): AnyRef = { 195 | if (doubleBuiltins.contains(name)) o match { 196 | case x: Number => x.doubleValue: JDouble 197 | case s: String => allCatch.opt(s.toDouble: JDouble).getOrElse(0.0: JDouble) 198 | case _ => "" // purposely invalid so it won't set. Might want to throw error instead. BCH 1/25/2015 199 | 200 | } else if (booleanBuiltins.contains(name)) o match { 201 | case x: Number => x.doubleValue != 0: JBoolean 202 | case b: JBoolean => b 203 | case s: String => allCatch.opt(s.toBoolean: JBoolean).getOrElse(false: JBoolean) 204 | case _ => "" // purposely invalid so it won't set. Might want to throw error instead. BCH 1/25/2015 205 | 206 | } else if (colorBuiltins.contains(name)) o match { 207 | case c: Color => convertColor(c) 208 | case x: Number => x.doubleValue: JDouble 209 | case c: Collection[?] => LogoList.fromIterator(c.asScala.map(convertAttribute).iterator) 210 | case s: String => allCatch.opt(s.toDouble: JDouble).getOrElse("") 211 | case _ => "" // purposely invalid so it won't set. Might want to throw error instead. BCH 1/25/2015 212 | 213 | } else { 214 | convertAttribute(o) 215 | } 216 | } 217 | 218 | private def convertAttribute(o: Any): AnyRef = o match { 219 | case c: Color => convertColor(c) 220 | case n: Number => n.doubleValue: JDouble 221 | case b: JBoolean => b 222 | case ll: LogoList => ll 223 | case c: Collection[?] => LogoList.fromIterator(c.asScala.map(x => convertAttribute(x)).iterator) 224 | // There may be a better handling of dynamic values, but this seems good enough for now. BCH 1/21/2015 225 | case d: TimeMap[?, ?] => LogoList.fromIterator(d.toValuesArray.map(x => convertAttribute(x)).iterator) 226 | case a: Array[?] => LogoList.fromIterator(a.map(convertAttribute).iterator) 227 | // Gephi attributes are strongly typed. Many formats, however, are not, and neither is NetLogo. Thus, when we use 228 | // String as a kind of AnyRef. This is a bad solution, but better than alternatives. BCH 1/25/2015 229 | case null => "" 230 | case x => x.toString 231 | } 232 | 233 | private def setAttribute(world: World, agent: Agent)(name: String, value: AnyRef): Unit = { 234 | val i = world.indexOfVariable(agent, name) 235 | if (i != -1) { 236 | try { 237 | agent.setVariable(i, value) 238 | } catch { 239 | case e: AgentException => /*Invalid variable or value, so skip*/ 240 | } 241 | } 242 | } 243 | } 244 | --------------------------------------------------------------------------------