From b06114622ff92eadb9f774f78fd753e5988df232 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 19 Feb 2019 20:44:23 +0100 Subject: [PATCH] Add barneshut assignment --- .gitignore | 16 + .gitlab-ci.yml | 36 ++ .vscode/settings.json | 8 + assignment.sbt | 9 + build.sbt | 13 + grading-tests.jar | Bin 0 -> 59019 bytes project/MOOCSettings.scala | 46 +++ project/StudentTasks.scala | 318 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 5 + project/plugins.sbt | 2 + src/main/scala/barneshut/BarnesHut.scala | 151 +++++++++ src/main/scala/barneshut/Interfaces.scala | 23 ++ .../scala/barneshut/SimulationCanvas.scala | 146 ++++++++ .../scala/barneshut/SimulationModel.scala | 73 ++++ src/main/scala/barneshut/Simulator.scala | 103 ++++++ src/main/scala/barneshut/conctrees/Conc.scala | 148 ++++++++ .../barneshut/conctrees/ConcBuffer.scala | 86 +++++ .../scala/barneshut/conctrees/package.scala | 16 + src/main/scala/barneshut/package.scala | 273 +++++++++++++++ src/test/scala/barneshut/BarnesHutSuite.scala | 138 ++++++++ 21 files changed, 1611 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .vscode/settings.json create mode 100644 assignment.sbt create mode 100644 build.sbt create mode 100644 grading-tests.jar create mode 100644 project/MOOCSettings.scala create mode 100644 project/StudentTasks.scala create mode 100644 project/build.properties create mode 100644 project/buildSettings.sbt create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/barneshut/BarnesHut.scala create mode 100644 src/main/scala/barneshut/Interfaces.scala create mode 100644 src/main/scala/barneshut/SimulationCanvas.scala create mode 100644 src/main/scala/barneshut/SimulationModel.scala create mode 100644 src/main/scala/barneshut/Simulator.scala create mode 100644 src/main/scala/barneshut/conctrees/Conc.scala create mode 100644 src/main/scala/barneshut/conctrees/ConcBuffer.scala create mode 100644 src/main/scala/barneshut/conctrees/package.scala create mode 100644 src/main/scala/barneshut/package.scala create mode 100644 src/test/scala/barneshut/BarnesHutSuite.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..349d2e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# General +*.DS_Store +*.swp +*~ + +# Dotty +*.class +*.tasty +*.hasTasty + +# sbt +target/ + +# Dotty IDE +/.dotty-ide-artifact +/.dotty-ide.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0ded59d --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,36 @@ +# DO NOT EDIT THIS FILE + +stages: + - build + - grade + +compile: + stage: build + image: lampepfl/moocs:dotty-2020-02-12 + except: + - tags + tags: + - cs206 + script: + - sbt packageSubmission + artifacts: + expire_in: 1 day + paths: + - submission.jar + +grade: + stage: grade + except: + - tags + tags: + - cs206 + image: + name: smarter3/moocs:parprog1-barneshut-2020-03-09 + entrypoint: [""] + allow_failure: true + before_script: + - mkdir -p /shared/submission/ + - cp submission.jar /shared/submission/submission.jar + script: + - cd /grader + - /grader/grade | /grader/feedback-printer diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a35362b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "dotty": { + "trace": { + "remoteTracingUrl": "wss://lamppc36.epfl.ch/dotty-remote-tracer/upload/lsp.log", + "server": { "format": "JSON", "verbosity": "verbose" } + } + } +} diff --git a/assignment.sbt b/assignment.sbt new file mode 100644 index 0000000..f7d3ae1 --- /dev/null +++ b/assignment.sbt @@ -0,0 +1,9 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) + +courseraId := ch.epfl.lamp.CourseraId( + key = "itfW99oJEeWXuxJgUJEB-Q", + itemId = "z1ugn", + premiumItemId = Some("xGkV0"), + partId = "ep95q" +) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..21e5745 --- /dev/null +++ b/build.sbt @@ -0,0 +1,13 @@ +course := "parprog1" +assignment := "barneshut" + +scalaVersion := "0.23.0-bin-20200211-5b006fb-NIGHTLY" +scalacOptions ++= Seq("-language:implicitConversions", "-deprecation") +libraryDependencies ++= Seq( + "com.storm-enroute" %% "scalameter-core" % "0.19", + "org.scala-lang.modules" %% "scala-parallel-collections" % "0.2.0", + "com.novocode" % "junit-interface" % "0.11" % Test +).map(_.withDottyCompat(scalaVersion.value)) + +testOptions in Test += Tests.Argument(TestFrameworks.JUnit, "-a", "-v", "-s") +testSuite := "barneshut.BarnesHutSuite" diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..b13b8694ff96f3a3286bd5d3aff3a9aa770f7dee GIT binary patch literal 59019 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

Bzk7TJ%)>2 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 "sources.zip", binaries -> "binaries.jar"), submission) + submission + }, + artifactClassifier in packageSourcesOnly := Some("sources"), + artifact in (Compile, packageBinWithoutResources) ~= (art => art.withName(art.name + "-without-resources")) + ) ++ + inConfig(Compile)( + Defaults.packageTaskSettings(packageSourcesOnly, Defaults.sourceMappings) ++ + Defaults.packageTaskSettings(packageBinWithoutResources, Def.task { + val relativePaths = + (unmanagedResources in Compile).value.flatMap(Path.relativeTo((unmanagedResourceDirectories in Compile).value)(_)) + (mappings in (Compile, packageBin)).value.filterNot { case (_, path) => relativePaths.contains(path) } + }) + ) + + val maxSubmitFileSize = { + val mb = 1024 * 1024 + 10 * mb + } + + /** Check that the jar exists, isn't empty, isn't crazy big, and can be read + * If so, encode jar as base64 so we can send it to Coursera + */ + def prepareJar(jar: File, s: TaskStreams): String = { + val errPrefix = "Error submitting assignment jar: " + val fileLength = jar.length() + if (!jar.exists()) { + s.log.error(errPrefix + "jar archive does not exist\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength == 0L) { + s.log.error(errPrefix + "jar archive is empty\n" + jar.getAbsolutePath) + failSubmit() + } else if (fileLength > maxSubmitFileSize) { + s.log.error(errPrefix + "jar archive is too big. Allowed size: " + + maxSubmitFileSize + " bytes, found " + fileLength + " bytes.\n" + + jar.getAbsolutePath) + failSubmit() + } else { + val bytes = new Array[Byte](fileLength.toInt) + val sizeRead = try { + val is = new FileInputStream(jar) + val read = is.read(bytes) + is.close() + read + } catch { + case ex: IOException => + s.log.error(errPrefix + "failed to read sources jar archive\n" + ex.toString) + failSubmit() + } + if (sizeRead != bytes.length) { + s.log.error(errPrefix + "failed to read the sources jar archive, size read: " + sizeRead) + failSubmit() + } else encodeBase64(bytes) + } + } + + /** Task to package solution to a given file path */ + lazy val packageSubmissionSetting = packageSubmission := { + val args: Seq[String] = Def.spaceDelimited("[path]").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val base64Jar = prepareJar(jar, s) + + val path = args.headOption.getOrElse((baseDirectory.value / "submission.jar").absolutePath) + scala.tools.nsc.io.File(path).writeAll(base64Jar) + } + +/* + /** Task to submit a solution to coursera */ + val submit = inputKey[Unit]("submit solution to Coursera") + lazy val submitSetting = submit := { + // Fail if scalafix linting does not pass. + scalafixLinting.value + + val args: Seq[String] = Def.spaceDelimited("").parsed + val s: TaskStreams = streams.value // for logging + val jar = (packageSubmissionZip in Compile).value + + val assignmentDetails = + courseraId.?.value.getOrElse(throw new MessageOnlyException("This assignment can not be submitted to Coursera because the `courseraId` setting is undefined")) + val assignmentKey = assignmentDetails.key + val courseName = + course.value match { + case "capstone" => "scala-capstone" + case "bigdata" => "scala-spark-big-data" + case other => other + } + + val partId = assignmentDetails.partId + val itemId = assignmentDetails.itemId + val premiumItemId = assignmentDetails.premiumItemId + + val (email, secret) = args match { + case email :: secret :: Nil => + (email, secret) + case _ => + val inputErr = + s"""|Invalid input to `submit`. The required syntax for `submit` is: + |submit + | + |The submit token is NOT YOUR LOGIN PASSWORD. + |It can be obtained from the assignment page: + |https://www.coursera.org/learn/$courseName/programming/$itemId + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + """.stripMargin + s.log.error(inputErr) + failSubmit() + } + + val base64Jar = prepareJar(jar, s) + val json = + s"""|{ + | "assignmentKey":"$assignmentKey", + | "submitterEmail":"$email", + | "secret":"$secret", + | "parts":{ + | "$partId":{ + | "output":"$base64Jar" + | } + | } + |}""".stripMargin + + def postSubmission[T](data: String): Try[HttpResponse[String]] = { + val http = Http("https://www.coursera.org/api/onDemandProgrammingScriptSubmissions.v1") + val hs = List( + ("Cache-Control", "no-cache"), + ("Content-Type", "application/json") + ) + s.log.info("Connecting to Coursera...") + val response = Try(http.postData(data) + .headers(hs) + .option(HttpOptions.connTimeout(10000)) // scalaj default timeout is only 100ms, changing that to 10s + .asString) // kick off HTTP POST + response + } + + val connectMsg = + s"""|Attempting to submit "${assignment.value}" assignment in "$courseName" course + |Using: + |- email: $email + |- submit token: $secret""".stripMargin + s.log.info(connectMsg) + + def reportCourseraResponse(response: HttpResponse[String]): Unit = { + val code = response.code + val respBody = response.body + + /* Sample JSON response from Coursera + { + "message": "Invalid email or token.", + "details": { + "learnerMessage": "Invalid email or token." + } + } + */ + + // Success, Coursera responds with 2xx HTTP status code + if (response.is2xx) { + val successfulSubmitMsg = + s"""|Successfully connected to Coursera. (Status $code) + | + |Assignment submitted successfully! + | + |You can see how you scored by going to: + |https://www.coursera.org/learn/$courseName/programming/$itemId/ + |${ + premiumItemId.fold("") { id => + s"""or (for premium learners): + |https://www.coursera.org/learn/$courseName/programming/$id + """.stripMargin + } + } + |and clicking on "My Submission".""".stripMargin + s.log.info(successfulSubmitMsg) + } + + // Failure, Coursera responds with 4xx HTTP status code (client-side failure) + else if (response.is4xx) { + val result = Try(Json.parse(respBody)).toOption + val learnerMsg = result match { + case Some(resp: JsObject) => + (JsPath \ "details" \ "learnerMessage").read[String].reads(resp).get + case Some(x) => // shouldn't happen + "Could not parse Coursera's response:\n" + x + case None => + "Could not parse Coursera's response:\n" + respBody + } + val failedSubmitMsg = + s"""|Submission failed. + |There was something wrong while attempting to submit. + |Coursera says: + |$learnerMsg (Status $code)""".stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera responds with 5xx HTTP status code (server-side failure) + else if (response.is5xx) { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera seems to be unavailable at the moment (Status $code) + |Check https://status.coursera.org/ and try again in a few minutes. + """.stripMargin + s.log.error(failedSubmitMsg) + } + + // Failure, Coursera repsonds with an unexpected status code + else { + val failedSubmitMsg = + s"""|Submission failed. + |Coursera replied with an unexpected code (Status $code) + """.stripMargin + s.log.error(failedSubmitMsg) + } + } + + // kick it all off, actually make request + postSubmission(json) match { + case Success(resp) => reportCourseraResponse(resp) + case Failure(e) => + val failedConnectMsg = + s"""|Connection to Coursera failed. + |There was something wrong while attempting to connect to Coursera. + |Check your internet connection. + |${e.toString}""".stripMargin + s.log.error(failedConnectMsg) + } + + } +*/ + + def failSubmit(): Nothing = { + sys.error("Submission failed") + } + + /** + * ***************** + * DEALING WITH JARS + */ + def encodeBase64(bytes: Array[Byte]): String = + new String(Base64.encodeBase64(bytes)) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..a919a9b --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.8 diff --git a/project/buildSettings.sbt b/project/buildSettings.sbt new file mode 100644 index 0000000..8fac702 --- /dev/null +++ b/project/buildSettings.sbt @@ -0,0 +1,5 @@ +// Used for Coursera submission (StudentPlugin) +// libraryDependencies += "org.scalaj" %% "scalaj-http" % "2.4.2" +// libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4" +// Used for Base64 (StudentPlugin) +libraryDependencies += "commons-codec" % "commons-codec" % "1.10" diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..017735d --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0") diff --git a/src/main/scala/barneshut/BarnesHut.scala b/src/main/scala/barneshut/BarnesHut.scala new file mode 100644 index 0000000..d84f337 --- /dev/null +++ b/src/main/scala/barneshut/BarnesHut.scala @@ -0,0 +1,151 @@ +package barneshut + +import java.awt._ +import java.awt.event._ +import javax.swing._ +import javax.swing.event._ +import scala.collection.parallel._ +import scala.collection.mutable.ArrayBuffer +import scala.reflect.ClassTag + +object BarnesHut { + + val model = new SimulationModel + + var simulator: Simulator = _ + + def initialize(parallelismLevel: Int, pattern: String, nbodies: Int): Unit = { + model.initialize(parallelismLevel, pattern, nbodies) + model.timeStats.clear() + simulator = new Simulator(model.taskSupport, model.timeStats) + } + + class BarnesHutFrame extends JFrame("Barnes-Hut") { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE) + setSize(1024, 600) + setLayout(new BorderLayout) + + val rightpanel = new JPanel + rightpanel.setBorder(BorderFactory.createEtchedBorder(border.EtchedBorder.LOWERED)) + rightpanel.setLayout(new BorderLayout) + add(rightpanel, BorderLayout.EAST) + + val controls = new JPanel + controls.setLayout(new GridLayout(0, 2)) + rightpanel.add(controls, BorderLayout.NORTH) + + val parallelismLabel = new JLabel("Parallelism") + controls.add(parallelismLabel) + + val items = (1 to Runtime.getRuntime.availableProcessors).map(_.toString).toArray + val parcombo = new JComboBox[String](items) + parcombo.setSelectedIndex(items.length - 1) + parcombo.addActionListener(new ActionListener { + def actionPerformed(e: ActionEvent) = { + initialize(getParallelism, "two-galaxies", getTotalBodies) + canvas.repaint() + } + }) + controls.add(parcombo) + + val bodiesLabel = new JLabel("Total bodies") + controls.add(bodiesLabel) + + val bodiesSpinner = new JSpinner(new SpinnerNumberModel(25000, 32, 1000000, 1000)) + bodiesSpinner.addChangeListener(new ChangeListener { + def stateChanged(e: ChangeEvent) = { + if (frame != null) { + initialize(getParallelism, "two-galaxies", getTotalBodies) + canvas.repaint() + } + } + }) + controls.add(bodiesSpinner) + + val stepbutton = new JButton("Step") + stepbutton.addActionListener(new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + stepThroughSimulation() + } + }) + controls.add(stepbutton) + + val startButton = new JToggleButton("Start/Pause") + val startTimer = new javax.swing.Timer(0, new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + stepThroughSimulation() + } + }) + startButton.addActionListener(new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + if (startButton.isSelected) startTimer.start() + else startTimer.stop() + } + }) + controls.add(startButton) + + val quadcheckbox = new JToggleButton("Show quad") + quadcheckbox.addActionListener(new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + model.shouldRenderQuad = quadcheckbox.isSelected + repaint() + } + }) + controls.add(quadcheckbox) + + val clearButton = new JButton("Restart") + clearButton.addActionListener(new ActionListener { + def actionPerformed(e: ActionEvent): Unit = { + initialize(getParallelism, "two-galaxies", getTotalBodies) + } + }) + controls.add(clearButton) + + val info = new JTextArea(" ") + info.setBorder(BorderFactory.createLoweredBevelBorder) + rightpanel.add(info, BorderLayout.SOUTH) + + val canvas = new SimulationCanvas(model) + add(canvas, BorderLayout.CENTER) + setVisible(true) + + def updateInformationBox(): Unit = { + val text = model.timeStats.toString + frame.info.setText("--- Statistics: ---\n" + text) + } + + def stepThroughSimulation(): Unit = { + SwingUtilities.invokeLater(new Runnable { + def run() = { + val (bodies, quad) = simulator.step(model.bodies) + model.bodies = bodies + model.quad = quad + updateInformationBox() + repaint() + } + }) + } + + def getParallelism = { + val selidx = parcombo.getSelectedIndex + parcombo.getItemAt(selidx).toInt + } + + def getTotalBodies = bodiesSpinner.getValue.asInstanceOf[Int] + + initialize(getParallelism, "two-galaxies", getTotalBodies) + } + + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()) + } catch { + case _: Exception => println("Cannot set look and feel, using the default one.") + } + + val frame = new BarnesHutFrame + + def main(args: Array[String]): Unit = { + frame.repaint() + } + +} diff --git a/src/main/scala/barneshut/Interfaces.scala b/src/main/scala/barneshut/Interfaces.scala new file mode 100644 index 0000000..9d06659 --- /dev/null +++ b/src/main/scala/barneshut/Interfaces.scala @@ -0,0 +1,23 @@ +package barneshut + +import conctrees.ConcBuffer + +// Interfaces used by the grading infrastructure. Do not change signatures +// or your submission will fail with a NoSuchMethodError. + +trait SectorMatrixInterface { + def +=(b: Body): SectorMatrix + def combine(that: SectorMatrix): SectorMatrix + def apply(x: Int, y: Int): ConcBuffer[Body] +} + +trait QuadInterface { + def massX: Float + def massY: Float + def mass: Float + def centerX: Float + def centerY: Float + def size: Float + def total: Int + def insert(b: Body): Quad +} diff --git a/src/main/scala/barneshut/SimulationCanvas.scala b/src/main/scala/barneshut/SimulationCanvas.scala new file mode 100644 index 0000000..24f7d4c --- /dev/null +++ b/src/main/scala/barneshut/SimulationCanvas.scala @@ -0,0 +1,146 @@ +package barneshut + +import java.awt._ +import java.awt.event._ +import javax.swing._ +import javax.swing.event._ + +class SimulationCanvas(val model: SimulationModel) extends JComponent { + + val MAX_RES = 3000 + + val pixels = new Array[Int](MAX_RES * MAX_RES) + + override def paintComponent(gcan: Graphics) = { + super.paintComponent(gcan) + + val width = getWidth + val height = getHeight + val img = new image.BufferedImage(width, height, image.BufferedImage.TYPE_INT_ARGB) + + // clear canvas pixels + for (x <- 0 until MAX_RES; y <- 0 until MAX_RES) pixels(y * width + x) = 0 + + // count number of bodies in each pixel + for (b <- model.bodies) { + val px = ((b.x - model.screen.minX) / model.screen.width * width).toInt + val py = ((b.y - model.screen.minY) / model.screen.height * height).toInt + if (px >= 0 && px < width && py >= 0 && py < height) pixels(py * width + px) += 1 + } + + // set image intensity depending on the number of bodies in the pixel + for (y <- 0 until height; x <- 0 until width) { + val count = pixels(y * width + x) + val intensity = if (count > 0) math.min(255, 70 + count * 50) else 0 + val color = (255 << 24) | (intensity << 16) | (intensity << 8) | intensity + img.setRGB(x, y, color) + } + + // for debugging purposes, if the number of bodies is small, output their locations + val g = img.getGraphics.asInstanceOf[Graphics2D] + g.setColor(Color.GRAY) + if (model.bodies.length < 350) for (b <- model.bodies) { + def round(x: Float) = (x * 100).toInt / 100.0f + val px = ((b.x - model.screen.minX) / model.screen.width * width).toInt + val py = ((b.y - model.screen.minY) / model.screen.height * height).toInt + if (px >= 0 && px < width && py >= 0 && py < height) { + g.drawString(s"${round(b.x)}, ${round(b.y)}", px, py) + } + } + + // render quad if necessary + if (model.shouldRenderQuad) { + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + val green = new Color(0, 225, 80, 150) + val red = new Color(200, 0, 0, 150) + g.setColor(green) + def drawQuad(depth: Int, quad: Quad): Unit = { + def drawRect(fx: Float, fy: Float, fsz: Float, q: Quad, fill: Boolean = false): Unit = { + val x = ((fx - model.screen.minX) / model.screen.width * width).toInt + val y = ((fy - model.screen.minY) / model.screen.height * height).toInt + val w = ((fx + fsz - model.screen.minX) / model.screen.width * width).toInt - x + val h = ((fy + fsz - model.screen.minY) / model.screen.height * height).toInt - y + g.drawRect(x, y, w, h) + if (fill) g.fillRect(x, y, w, h) + if (depth <= 5) g.drawString("#:" + q.total, x + w / 2, y + h / 2) + } + quad match { + case Fork(nw, ne, sw, se) => + val cx = quad.centerX + val cy = quad.centerY + val sz = quad.size + drawRect(cx - sz / 2, cy - sz / 2, sz / 2, nw) + drawRect(cx - sz / 2, cy, sz / 2, sw) + drawRect(cx, cy - sz / 2, sz / 2, ne) + drawRect(cx, cy, sz / 2, se) + drawQuad(depth + 1, nw) + drawQuad(depth + 1, ne) + drawQuad(depth + 1, sw) + drawQuad(depth + 1, se) + case Empty(_, _, _) | Leaf(_, _, _, _) => + // done + } + } + drawQuad(0, model.quad) + + } + gcan.drawImage(img, 0, 0, null) + } + + // zoom on mouse rotation + addMouseWheelListener(new MouseAdapter { + override def mouseWheelMoved(e: MouseWheelEvent): Unit = { + val rot = e.getWheelRotation + val cx = model.screen.centerX + val cy = model.screen.centerY + val w = model.screen.width + val h = model.screen.height + val factor = { + if (rot > 0) 0.52f + else if (rot < 0) 0.48f + else 0.5f + } + model.screen.minX = cx - w * factor + model.screen.minY = cy - h * factor + model.screen.maxX = cx + w * factor + model.screen.maxY = cy + h * factor + repaint() + } + }) + + // reset the last known mouse drag position on mouse press + var xlast = Int.MinValue + var ylast = Int.MinValue + addMouseListener(new MouseAdapter { + override def mousePressed(e: MouseEvent): Unit = { + xlast = Int.MinValue + ylast = Int.MinValue + } + }) + + // update the last known mouse drag position on mouse drag, + // update the boundaries of the visible area + addMouseMotionListener(new MouseMotionAdapter { + override def mouseDragged(e: MouseEvent): Unit = { + val xcurr = e.getX + val ycurr = e.getY + if (xlast != Int.MinValue) { + val xd = xcurr - xlast + val yd = ycurr - ylast + val w = model.screen.width + val h = model.screen.height + val cx = model.screen.centerX - xd * w / 1000 + val cy = model.screen.centerY - yd * h / 1000 + model.screen.minX = cx - w / 2 + model.screen.minY = cy - h / 2 + model.screen.maxX = cx + w / 2 + model.screen.maxY = cy + h / 2 + println(model.screen) + } + xlast = xcurr + ylast = ycurr + repaint() + } + }) + +} diff --git a/src/main/scala/barneshut/SimulationModel.scala b/src/main/scala/barneshut/SimulationModel.scala new file mode 100644 index 0000000..eb27d56 --- /dev/null +++ b/src/main/scala/barneshut/SimulationModel.scala @@ -0,0 +1,73 @@ +package barneshut + +import java.awt._ +import java.awt.event._ +import javax.swing._ +import javax.swing.event._ +import scala.collection.parallel.{TaskSupport, defaultTaskSupport} +import scala.{collection => coll} + +class SimulationModel { + + var screen = new Boundaries + + var bodies: coll.Seq[Body] = Nil + + var quad: Quad = Empty(screen.centerX, screen.centerY, Float.MaxValue) + + var shouldRenderQuad = false + + var timeStats = new TimeStatistics + + var taskSupport: TaskSupport = defaultTaskSupport + + def initialize(parallelismLevel: Int, pattern: String, totalBodies: Int): Unit = { + taskSupport = new collection.parallel.ForkJoinTaskSupport( + new java.util.concurrent.ForkJoinPool(parallelismLevel)) + + pattern match { + case "two-galaxies" => init2Galaxies(totalBodies) + case _ => sys.error(s"no such initial pattern: $pattern") + } + } + + def init2Galaxies(totalBodies: Int): Unit = { + val bodyArray = new Array[Body](totalBodies) + val random = new scala.util.Random(213L) + + def galaxy(from: Int, num: Int, maxradius: Float, cx: Float, cy: Float, sx: Float, sy: Float): Unit = { + val totalM = 1.5f * num + val blackHoleM = 1.0f * num + val cubmaxradius = maxradius * maxradius * maxradius + for (i <- from until (from + num)) { + val b = if (i == from) { + new Body(blackHoleM, cx, cy, sx, sy) + } else { + val angle = random.nextFloat * 2 * math.Pi + val radius = 25 + maxradius * random.nextFloat + val starx = cx + radius * math.sin(angle).toFloat + val stary = cy + radius * math.cos(angle).toFloat + val speed = math.sqrt(gee * blackHoleM / radius + gee * totalM * radius * radius / cubmaxradius) + val starspeedx = sx + (speed * math.sin(angle + math.Pi / 2)).toFloat + val starspeedy = sy + (speed * math.cos(angle + math.Pi / 2)).toFloat + val starmass = 1.0f + 1.0f * random.nextFloat + new Body(starmass, starx, stary, starspeedx, starspeedy) + } + bodyArray(i) = b + } + } + + galaxy(0, bodyArray.length / 8, 300.0f, 0.0f, 0.0f, 0.0f, 0.0f) + galaxy(bodyArray.length / 8, bodyArray.length / 8 * 7, 350.0f, -1800.0f, -1200.0f, 0.0f, 0.0f) + + bodies = bodyArray.toSeq + + // compute center and boundaries + screen = new Boundaries + screen.minX = -2200.0f + screen.minY = -1600.0f + screen.maxX = 350.0f + screen.maxY = 350.0f + } + +} diff --git a/src/main/scala/barneshut/Simulator.scala b/src/main/scala/barneshut/Simulator.scala new file mode 100644 index 0000000..7fdafb2 --- /dev/null +++ b/src/main/scala/barneshut/Simulator.scala @@ -0,0 +1,103 @@ +package barneshut + +import java.awt._ +import java.awt.event._ +import javax.swing._ +import javax.swing.event._ +import scala.{collection => coll} +import scala.collection.parallel.{TaskSupport, Combiner} +import scala.collection.parallel.mutable.ParHashSet +import scala.collection.parallel.CollectionConverters._ + +class Simulator(val taskSupport: TaskSupport, val timeStats: TimeStatistics) { + + def updateBoundaries(boundaries: Boundaries, body: Body): Boundaries = { + ??? + } + + def mergeBoundaries(a: Boundaries, b: Boundaries): Boundaries = { + ??? + } + + def computeBoundaries(bodies: coll.Seq[Body]): Boundaries = timeStats.timed("boundaries") { + val parBodies = bodies.par + parBodies.tasksupport = taskSupport + parBodies.aggregate(new Boundaries)(updateBoundaries, mergeBoundaries) + } + + def computeSectorMatrix(bodies: coll.Seq[Body], boundaries: Boundaries): SectorMatrix = timeStats.timed("matrix") { + val parBodies = bodies.par + parBodies.tasksupport = taskSupport + ??? + } + + def computeQuad(sectorMatrix: SectorMatrix): Quad = timeStats.timed("quad") { + sectorMatrix.toQuad(taskSupport.parallelismLevel) + } + + def updateBodies(bodies: coll.Seq[Body], quad: Quad): coll.Seq[Body] = timeStats.timed("update") { + val parBodies = bodies.par + parBodies.tasksupport = taskSupport + ??? + } + + def eliminateOutliers(bodies: coll.Seq[Body], sectorMatrix: SectorMatrix, quad: Quad): coll.Seq[Body] = timeStats.timed("eliminate") { + def isOutlier(b: Body): Boolean = { + val dx = quad.massX - b.x + val dy = quad.massY - b.y + val d = math.sqrt(dx * dx + dy * dy) + // object is far away from the center of the mass + if (d > eliminationThreshold * sectorMatrix.boundaries.size) { + val nx = dx / d + val ny = dy / d + val relativeSpeed = b.xspeed * nx + b.yspeed * ny + // object is moving away from the center of the mass + if (relativeSpeed < 0) { + val escapeSpeed = math.sqrt(2 * gee * quad.mass / d) + // object has the espace velocity + -relativeSpeed > 2 * escapeSpeed + } else false + } else false + } + + def outliersInSector(x: Int, y: Int): Combiner[Body, ParHashSet[Body]] = { + val combiner = ParHashSet.newCombiner[Body] + combiner ++= sectorMatrix(x, y).filter(isOutlier) + combiner + } + + val sectorPrecision = sectorMatrix.sectorPrecision + val horizontalBorder = for (x <- 0 until sectorPrecision; y <- Seq(0, sectorPrecision - 1)) yield (x, y) + val verticalBorder = for (y <- 1 until sectorPrecision - 1; x <- Seq(0, sectorPrecision - 1)) yield (x, y) + val borderSectors = horizontalBorder ++ verticalBorder + + // compute the set of outliers + val parBorderSectors = borderSectors.par + parBorderSectors.tasksupport = taskSupport + val outliers = parBorderSectors.map({ case (x, y) => outliersInSector(x, y) }).reduce(_ combine _).result + + // filter the bodies that are outliers + val parBodies = bodies.par + parBodies.filter(!outliers(_)).seq + } + + def step(bodies: coll.Seq[Body]): (coll.Seq[Body], Quad) = { + // 1. compute boundaries + val boundaries = computeBoundaries(bodies) + + // 2. compute sector matrix + val sectorMatrix = computeSectorMatrix(bodies, boundaries) + + // 3. compute quad tree + val quad = computeQuad(sectorMatrix) + + // 4. eliminate outliers + val filteredBodies = eliminateOutliers(bodies, sectorMatrix, quad) + + // 5. update body velocities and positions + val newBodies = updateBodies(filteredBodies, quad) + + (newBodies, quad) + } + +} diff --git a/src/main/scala/barneshut/conctrees/Conc.scala b/src/main/scala/barneshut/conctrees/Conc.scala new file mode 100644 index 0000000..6afb50e --- /dev/null +++ b/src/main/scala/barneshut/conctrees/Conc.scala @@ -0,0 +1,148 @@ +package barneshut +package conctrees + +import scala.annotation.tailrec + +sealed trait Conc[@specialized(Int, Long, Float, Double) +T] { + def level: Int + def size: Int + def left: Conc[T] + def right: Conc[T] + def normalized = this +} + +object Conc { + + case class <>[+T](left: Conc[T], right: Conc[T]) extends Conc[T] { + val level = 1 + math.max(left.level, right.level) + val size = left.size + right.size + } + + sealed trait Leaf[T] extends Conc[T] { + def left = sys.error("Leaves do not have children.") + def right = sys.error("Leaves do not have children.") + } + + case object Empty extends Leaf[Nothing] { + def level = 0 + def size = 0 + } + + class Single[@specialized(Int, Long, Float, Double) T](val x: T) extends Leaf[T] { + def level = 0 + def size = 1 + override def toString = s"Single($x)" + } + + class Chunk[@specialized(Int, Long, Float, Double) T](val array: Array[T], val size: Int, val k: Int) + extends Leaf[T] { + def level = 0 + override def toString = s"Chunk(${array.mkString("", ", ", "")}; $size; $k)" + } + + case class Append[+T](left: Conc[T], right: Conc[T]) extends Conc[T] { + val level = 1 + math.max(left.level, right.level) + val size = left.size + right.size + override def normalized = { + def wrap[T](xs: Conc[T], ys: Conc[T]): Conc[T] = (xs: @unchecked) match { + case Append(ws, zs) => wrap(ws, zs <> ys) + case xs => xs <> ys + } + wrap(left, right) + } + } + + def concatTop[T](xs: Conc[T], ys: Conc[T]) = { + if (xs == Empty) ys + else if (ys == Empty) xs + else concat(xs, ys) + } + + private def concat[T](xs: Conc[T], ys: Conc[T]): Conc[T] = { + val diff = ys.level - xs.level + if (diff >= -1 && diff <= 1) new <>(xs, ys) + else if (diff < -1) { + if (xs.left.level >= xs.right.level) { + val nr = concat(xs.right, ys) + new <>(xs.left, nr) + } else { + val nrr = concat(xs.right.right, ys) + if (nrr.level == xs.level - 3) { + val nl = xs.left + val nr = new <>(xs.right.left, nrr) + new <>(nl, nr) + } else { + val nl = new <>(xs.left, xs.right.left) + val nr = nrr + new <>(nl, nr) + } + } + } else { + if (ys.right.level >= ys.left.level) { + val nl = concat(xs, ys.left) + new <>(nl, ys.right) + } else { + val nll = concat(xs, ys.left.left) + if (nll.level == ys.level - 3) { + val nl = new <>(nll, ys.left.right) + val nr = ys.right + new <>(nl, nr) + } else { + val nl = nll + val nr = new <>(ys.left.right, ys.right) + new <>(nl, nr) + } + } + } + } + + def appendTop[T](xs: Conc[T], ys: Leaf[T]): Conc[T] = (xs: @unchecked) match { + case xs: Append[T] => append(xs, ys) + case _ <> _ => new Append(xs, ys) + case Empty => ys + case xs: Leaf[T] => new <>(xs, ys) + } + @tailrec private def append[T](xs: Append[T], ys: Conc[T]): Conc[T] = { + if (xs.right.level > ys.level) new Append(xs, ys) + else { + val zs = new <>(xs.right, ys) + xs.left match { + case ws @ Append(_, _) => append(ws, zs) + case ws if ws.level <= zs.level => ws <> zs + case ws => new Append(ws, zs) + } + } + } + + def traverse[@specialized(Int, Long, Float, Double) T, @specialized(Int, Long, Float, Double) U](xs: Conc[T], f: T => U): Unit = (xs: @unchecked) match { + case left <> right => + traverse(left, f) + traverse(right, f) + case s: Single[T] => + f(s.x) + case c: Chunk[T] => + val a = c.array + val sz = c.size + var i = 0 + while (i < sz) { + f(a(i)) + i += 1 + } + case Empty => + case Append(left, right) => + traverse(left, f) + traverse(right, f) + case _ => + sys.error("All cases should have been covered: " + xs + ", " + xs.getClass) + } + + def iterator[@specialized(Int, Long, Float, Double) T](xs: Conc[T]): Iterator[T] = (xs: @unchecked) match { + case left <> right => iterator(left) ++ iterator(right) + case s: Single[T] => Iterator.single(s.x) + case c: Chunk[T] => c.array.iterator.take(c.size) + case Empty => Iterator.empty + case Append(left, right) => iterator(left) ++ iterator(right) + case _ => sys.error("All cases should have been covered: " + xs + ", " + xs.getClass) + } + +} diff --git a/src/main/scala/barneshut/conctrees/ConcBuffer.scala b/src/main/scala/barneshut/conctrees/ConcBuffer.scala new file mode 100644 index 0000000..37f451d --- /dev/null +++ b/src/main/scala/barneshut/conctrees/ConcBuffer.scala @@ -0,0 +1,86 @@ +package barneshut +package conctrees + +import scala.reflect.ClassTag +import scala.collection.parallel.CollectionConverters._ +import org.scalameter._ + +class ConcBuffer[@specialized(Byte, Char, Int, Long, Float, Double) T: ClassTag]( + val k: Int, private var conc: Conc[T] +) extends Iterable[T] { + require(k > 0) + + def this() = this(128, Conc.Empty) + + private var chunk: Array[T] = new Array(k) + private var lastSize: Int = 0 + + def iterator: Iterator[T] = Conc.iterator(conc) ++ chunk.iterator.take(lastSize) + + final def +=(elem: T): this.type = { + if (lastSize >= k) expand() + chunk(lastSize) = elem + lastSize += 1 + this + } + + final def combine(that: ConcBuffer[T]): ConcBuffer[T] = { + val combinedConc = this.result <> that.result + this.clear() + that.clear() + new ConcBuffer(k, combinedConc) + } + + private def pack(): Unit = { + conc = Conc.appendTop(conc, new Conc.Chunk(chunk, lastSize, k)) + } + + private def expand(): Unit = { + pack() + chunk = new Array(k) + lastSize = 0 + } + + def clear(): Unit = { + conc = Conc.Empty + chunk = new Array(k) + lastSize = 0 + } + + def result: Conc[T] = { + pack() + conc + } +} + +object ConcBufferRunner { + + val standardConfig = config( + Key.exec.minWarmupRuns -> 20, + Key.exec.maxWarmupRuns -> 40, + Key.exec.benchRuns -> 60, + Key.verbose -> false + ).withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val size = 1000000 + + def run(p: Int): Unit = { + val taskSupport = new collection.parallel.ForkJoinTaskSupport( + new java.util.concurrent.ForkJoinPool(p)) + val strings = (0 until size).map(_.toString) + val time = standardConfig measure { + val parallelized = strings.par + parallelized.tasksupport = taskSupport + parallelized.aggregate(new ConcBuffer[String])(_ += _, _ combine _).result + } + println(s"p = $p, time = ${time.value}") + } + + run(1) + run(2) + run(4) + run(8) + } + +} diff --git a/src/main/scala/barneshut/conctrees/package.scala b/src/main/scala/barneshut/conctrees/package.scala new file mode 100644 index 0000000..627d07c --- /dev/null +++ b/src/main/scala/barneshut/conctrees/package.scala @@ -0,0 +1,16 @@ +package barneshut + +import org.scalameter._ + +package object conctrees { + + implicit class ConcOps[T](val self: Conc[T]) extends AnyVal { + def foreach[U](f: T => U) = Conc.traverse(self, f) + def <>(that: Conc[T]) = Conc.concatTop(self.normalized, that.normalized) + } + + // Workaround Dotty's handling of the existential type KeyValue + implicit def keyValueCoerce[T](kv: (Key[T], T)): KeyValue = { + kv.asInstanceOf[KeyValue] + } +} diff --git a/src/main/scala/barneshut/package.scala b/src/main/scala/barneshut/package.scala new file mode 100644 index 0000000..20b05b4 --- /dev/null +++ b/src/main/scala/barneshut/package.scala @@ -0,0 +1,273 @@ +import java.util.concurrent._ +import scala.{collection => coll} +import scala.util.DynamicVariable +import barneshut.conctrees._ + +package object barneshut { + + class Boundaries { + var minX = Float.MaxValue + + var minY = Float.MaxValue + + var maxX = Float.MinValue + + var maxY = Float.MinValue + + def width = maxX - minX + + def height = maxY - minY + + def size = math.max(width, height) + + def centerX = minX + width / 2 + + def centerY = minY + height / 2 + + override def toString = s"Boundaries($minX, $minY, $maxX, $maxY)" + } + + sealed abstract class Quad extends QuadInterface { + def massX: Float + + def massY: Float + + def mass: Float + + def centerX: Float + + def centerY: Float + + def size: Float + + def total: Int + + def insert(b: Body): Quad + } + + case class Empty(centerX: Float, centerY: Float, size: Float) extends Quad { + def massX: Float = ??? + def massY: Float = ??? + def mass: Float = ??? + def total: Int = ??? + def insert(b: Body): Quad = ??? + } + + case class Fork( + nw: Quad, ne: Quad, sw: Quad, se: Quad + ) extends Quad { + val centerX: Float = ??? + val centerY: Float = ??? + val size: Float = ??? + val mass: Float = ??? + val massX: Float = ??? + val massY: Float = ??? + val total: Int = ??? + + def insert(b: Body): Fork = { + ??? + } + } + + case class Leaf(centerX: Float, centerY: Float, size: Float, bodies: coll.Seq[Body]) + extends Quad { + val (mass, massX, massY) = (??? : Float, ??? : Float, ??? : Float) + val total: Int = ??? + def insert(b: Body): Quad = ??? + } + + def minimumSize = 0.00001f + + def gee: Float = 100.0f + + def delta: Float = 0.01f + + def theta = 0.5f + + def eliminationThreshold = 0.5f + + def force(m1: Float, m2: Float, dist: Float): Float = gee * m1 * m2 / (dist * dist) + + def distance(x0: Float, y0: Float, x1: Float, y1: Float): Float = { + math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)).toFloat + } + + class Body(val mass: Float, val x: Float, val y: Float, val xspeed: Float, val yspeed: Float) { + + def updated(quad: Quad): Body = { + var netforcex = 0.0f + var netforcey = 0.0f + + def addForce(thatMass: Float, thatMassX: Float, thatMassY: Float): Unit = { + val dist = distance(thatMassX, thatMassY, x, y) + /* If the distance is smaller than 1f, we enter the realm of close + * body interactions. Since we do not model them in this simplistic + * implementation, bodies at extreme proximities get a huge acceleration, + * and are catapulted from each other's gravitational pull at extreme + * velocities (something like this: + * http://en.wikipedia.org/wiki/Interplanetary_spaceflight#Gravitational_slingshot). + * To decrease the effect of this gravitational slingshot, as a very + * simple approximation, we ignore gravity at extreme proximities. + */ + if (dist > 1f) { + val dforce = force(mass, thatMass, dist) + val xn = (thatMassX - x) / dist + val yn = (thatMassY - y) / dist + val dforcex = dforce * xn + val dforcey = dforce * yn + netforcex += dforcex + netforcey += dforcey + } + } + + def traverse(quad: Quad): Unit = (quad: Quad) match { + case Empty(_, _, _) => + // no force + case Leaf(_, _, _, bodies) => + // add force contribution of each body by calling addForce + case Fork(nw, ne, sw, se) => + // see if node is far enough from the body, + // or recursion is needed + } + + traverse(quad) + + val nx = x + xspeed * delta + val ny = y + yspeed * delta + val nxspeed = xspeed + netforcex / mass * delta + val nyspeed = yspeed + netforcey / mass * delta + + new Body(mass, nx, ny, nxspeed, nyspeed) + } + + } + + val SECTOR_PRECISION = 8 + + class SectorMatrix(val boundaries: Boundaries, val sectorPrecision: Int) extends SectorMatrixInterface { + val sectorSize = boundaries.size / sectorPrecision + val matrix = new Array[ConcBuffer[Body]](sectorPrecision * sectorPrecision) + for (i <- 0 until matrix.length) matrix(i) = new ConcBuffer + + def +=(b: Body): SectorMatrix = { + ??? + this + } + + def apply(x: Int, y: Int) = matrix(y * sectorPrecision + x) + + def combine(that: SectorMatrix): SectorMatrix = { + ??? + } + + def toQuad(parallelism: Int): Quad = { + def BALANCING_FACTOR = 4 + def quad(x: Int, y: Int, span: Int, achievedParallelism: Int): Quad = { + if (span == 1) { + val sectorSize = boundaries.size / sectorPrecision + val centerX = boundaries.minX + x * sectorSize + sectorSize / 2 + val centerY = boundaries.minY + y * sectorSize + sectorSize / 2 + var emptyQuad: Quad = Empty(centerX, centerY, sectorSize) + val sectorBodies = this(x, y) + sectorBodies.foldLeft(emptyQuad)(_ insert _) + } else { + val nspan = span / 2 + val nAchievedParallelism = achievedParallelism * 4 + val (nw, ne, sw, se) = + if (parallelism > 1 && achievedParallelism < parallelism * BALANCING_FACTOR) parallel( + quad(x, y, nspan, nAchievedParallelism), + quad(x + nspan, y, nspan, nAchievedParallelism), + quad(x, y + nspan, nspan, nAchievedParallelism), + quad(x + nspan, y + nspan, nspan, nAchievedParallelism) + ) else ( + quad(x, y, nspan, nAchievedParallelism), + quad(x + nspan, y, nspan, nAchievedParallelism), + quad(x, y + nspan, nspan, nAchievedParallelism), + quad(x + nspan, y + nspan, nspan, nAchievedParallelism) + ) + Fork(nw, ne, sw, se) + } + } + + quad(0, 0, sectorPrecision, 1) + } + + override def toString = s"SectorMatrix(#bodies: ${matrix.map(_.size).sum})" + } + + class TimeStatistics { + private val timeMap = collection.mutable.Map[String, (Double, Int)]() + + def clear() = timeMap.clear() + + def timed[T](title: String)(body: =>T): T = { + var res: T = null.asInstanceOf[T] + val totalTime = /*measure*/ { + val startTime = System.currentTimeMillis() + res = body + (System.currentTimeMillis() - startTime) + } + + timeMap.get(title) match { + case Some((total, num)) => timeMap(title) = (total + totalTime, num + 1) + case None => timeMap(title) = (0.0, 0) + } + + println(s"$title: ${totalTime} ms; avg: ${timeMap(title)._1 / timeMap(title)._2}") + res + } + + override def toString = { + timeMap map { + case (k, (total, num)) => k + ": " + (total / num * 100).toInt / 100.0 + " ms" + } mkString("\n") + } + } + + val forkJoinPool = new ForkJoinPool + + abstract class TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] + def parallel[A, B](taskA: => A, taskB: => B): (A, B) = { + val right = task { + taskB + } + val left = taskA + (left, right.join()) + } + } + + class DefaultTaskScheduler extends TaskScheduler { + def schedule[T](body: => T): ForkJoinTask[T] = { + val t = new RecursiveTask[T] { + def compute = body + } + Thread.currentThread match { + case wt: ForkJoinWorkerThread => + t.fork() + case _ => + forkJoinPool.execute(t) + } + t + } + } + + val scheduler = + new DynamicVariable[TaskScheduler](new DefaultTaskScheduler) + + def task[T](body: => T): ForkJoinTask[T] = { + scheduler.value.schedule(body) + } + + def parallel[A, B](taskA: => A, taskB: => B): (A, B) = { + scheduler.value.parallel(taskA, taskB) + } + + def parallel[A, B, C, D](taskA: => A, taskB: => B, taskC: => C, taskD: => D): (A, B, C, D) = { + val ta = task { taskA } + val tb = task { taskB } + val tc = task { taskC } + val td = taskD + (ta.join(), tb.join(), tc.join(), td) + } +} diff --git a/src/test/scala/barneshut/BarnesHutSuite.scala b/src/test/scala/barneshut/BarnesHutSuite.scala new file mode 100644 index 0000000..bf947c1 --- /dev/null +++ b/src/test/scala/barneshut/BarnesHutSuite.scala @@ -0,0 +1,138 @@ +package barneshut + +import java.util.concurrent._ +import scala.collection._ +import scala.math._ +import scala.collection.parallel._ +import barneshut.conctrees.ConcBuffer +import org.junit._ +import org.junit.Assert.{assertEquals, fail} + +class BarnesHutSuite { + // test cases for quad tree + +import FloatOps._ + @Test def `Empty: center of mass should be the center of the cell`: Unit = { + val quad = Empty(51f, 46.3f, 5f) + assert(quad.massX == 51f, s"${quad.massX} should be 51f") + assert(quad.massY == 46.3f, s"${quad.massY} should be 46.3f") + } + + @Test def `Empty: mass should be 0`: Unit = { + val quad = Empty(51f, 46.3f, 5f) + assert(quad.mass == 0f, s"${quad.mass} should be 0f") + } + + @Test def `Empty: total should be 0`: Unit = { + val quad = Empty(51f, 46.3f, 5f) + assert(quad.total == 0, s"${quad.total} should be 0") + } + + @Test def `Leaf with 1 body`: Unit = { + val b = new Body(123f, 18f, 26f, 0f, 0f) + val quad = Leaf(17.5f, 27.5f, 5f, Seq(b)) + + assert(quad.mass ~= 123f, s"${quad.mass} should be 123f") + assert(quad.massX ~= 18f, s"${quad.massX} should be 18f") + assert(quad.massY ~= 26f, s"${quad.massY} should be 26f") + assert(quad.total == 1, s"${quad.total} should be 1") + } + + + @Test def `Fork with 3 empty quadrants and 1 leaf (nw)`: Unit = { + val b = new Body(123f, 18f, 26f, 0f, 0f) + val nw = Leaf(17.5f, 27.5f, 5f, Seq(b)) + val ne = Empty(22.5f, 27.5f, 5f) + val sw = Empty(17.5f, 32.5f, 5f) + val se = Empty(22.5f, 32.5f, 5f) + val quad = Fork(nw, ne, sw, se) + + assert(quad.centerX == 20f, s"${quad.centerX} should be 20f") + assert(quad.centerY == 30f, s"${quad.centerY} should be 30f") + assert(quad.mass ~= 123f, s"${quad.mass} should be 123f") + assert(quad.massX ~= 18f, s"${quad.massX} should be 18f") + assert(quad.massY ~= 26f, s"${quad.massY} should be 26f") + assert(quad.total == 1, s"${quad.total} should be 1") + } + + @Test def `Empty.insert(b) should return a Leaf with only that body (2pts)`: Unit = { + val quad = Empty(51f, 46.3f, 5f) + val b = new Body(3f, 54f, 46f, 0f, 0f) + val inserted = quad.insert(b) + inserted match { + case Leaf(centerX, centerY, size, bodies) => + assert(centerX == 51f, s"$centerX should be 51f") + assert(centerY == 46.3f, s"$centerY should be 46.3f") + assert(size == 5f, s"$size should be 5f") + assert(bodies == Seq(b), s"$bodies should contain only the inserted body") + case _ => + fail("Empty.insert() should have returned a Leaf, was $inserted") + } + } + + // test cases for Body + + @Test def `Body.updated should do nothing for Empty quad trees`: Unit = { + val b1 = new Body(123f, 18f, 26f, 0f, 0f) + val body = b1.updated(Empty(50f, 60f, 5f)) + + assertEquals(0f, body.xspeed, precisionThreshold) + assertEquals(0f, body.yspeed, precisionThreshold) + } + + @Test def `Body.updated should take bodies in a Leaf into account (2pts)`: Unit = { + val b1 = new Body(123f, 18f, 26f, 0f, 0f) + val b2 = new Body(524.5f, 24.5f, 25.5f, 0f, 0f) + val b3 = new Body(245f, 22.4f, 41f, 0f, 0f) + + val quad = Leaf(15f, 30f, 20f, Seq(b2, b3)) + + val body = b1.updated(quad) + + assert(body.xspeed ~= 12.587037f) + assert(body.yspeed ~= 0.015557117f) + } + + // test cases for sector matrix + + @Test def `'SectorMatrix.+=' should add a body at (25,47) to the correct bucket of a sector matrix of size 96 (2pts)`: Unit = { + val body = new Body(5, 25, 47, 0.1f, 0.1f) + val boundaries = new Boundaries() + boundaries.minX = 1 + boundaries.minY = 1 + boundaries.maxX = 97 + boundaries.maxY = 97 + val sm = new SectorMatrix(boundaries, SECTOR_PRECISION) + sm += body + val res = sm(2, 3).size == 1 && sm(2, 3).find(_ == body).isDefined + assert(res, s"Body not found in the right sector") + } + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} + +object FloatOps { + val precisionThreshold = 1e-4 + + /** Floating comparison: assert(float ~= 1.7f). */ + implicit class FloatOps(val self: Float) extends AnyVal { + def ~=(that: Float): Boolean = + abs(self - that) < precisionThreshold + } + + /** Long floating comparison: assert(double ~= 1.7). */ + implicit class DoubleOps(val self: Double) extends AnyVal { + def ~=(that: Double): Boolean = + abs(self - that) < precisionThreshold + } + + /** Floating sequences comparison: assert(floatSeq ~= Seq(0.5f, 1.7f). */ + implicit class FloatSequenceOps(val self: Seq[Float]) extends AnyVal { + def ~=(that: Seq[Float]): Boolean = + self.size == that.size && + self.zip(that).forall { case (a, b) => + abs(a - b) < precisionThreshold + } + } +} +