├── .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 |
--------------------------------------------------------------------------------