From d92e0cb72ad5aaf32a66dec0427ae5cf3365e7dc Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 19 Feb 2019 20:44:23 +0100 Subject: [PATCH] Add reductions assignment --- .gitignore | 16 + .gitlab-ci.yml | 36 ++ .vscode/settings.json | 8 + assignment.sbt | 9 + build.sbt | 13 + grading-tests.jar | Bin 0 -> 32614 bytes project/MOOCSettings.scala | 46 +++ project/StudentTasks.scala | 318 ++++++++++++++++++ project/build.properties | 1 + project/buildSettings.sbt | 5 + project/plugins.sbt | 2 + src/main/scala/reductions/Interfaces.scala | 26 ++ src/main/scala/reductions/LineOfSight.scala | 89 +++++ .../reductions/ParallelCountChange.scala | 79 +++++ .../ParallelParenthesesBalancing.scala | 64 ++++ src/main/scala/reductions/package.scala | 58 ++++ .../scala/reductions/ReductionsSuite.scala | 124 +++++++ 17 files changed, 894 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/reductions/Interfaces.scala create mode 100644 src/main/scala/reductions/LineOfSight.scala create mode 100644 src/main/scala/reductions/ParallelCountChange.scala create mode 100644 src/main/scala/reductions/ParallelParenthesesBalancing.scala create mode 100644 src/main/scala/reductions/package.scala create mode 100644 src/test/scala/reductions/ReductionsSuite.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..0d187c4 --- /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-reductions-2020-02-24-2 + 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..42cdd4c --- /dev/null +++ b/assignment.sbt @@ -0,0 +1,9 @@ +// Student tasks (i.e. submit, packageSubmission) +enablePlugins(StudentTasks) + +courseraId := ch.epfl.lamp.CourseraId( + key = "lUUWddoGEeWPHw6r45-nxw", + itemId = "U1eU3", + premiumItemId = Some("4rXwX"), + partId = "gmSnR" +) diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..de34f52 --- /dev/null +++ b/build.sbt @@ -0,0 +1,13 @@ +course := "parprog1" +assignment := "reductions" + +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 := "reductions.ReductionsSuite" diff --git a/grading-tests.jar b/grading-tests.jar new file mode 100644 index 0000000000000000000000000000000000000000..81ac694dc7e478e5f2cbd2d9c27b62899bda30e7 GIT binary patch literal 32614 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 literal 0 HcmV?d00001 diff --git a/project/MOOCSettings.scala b/project/MOOCSettings.scala new file mode 100644 index 0000000..171244f --- /dev/null +++ b/project/MOOCSettings.scala @@ -0,0 +1,46 @@ +package ch.epfl.lamp + +import sbt._ +import sbt.Keys._ + +/** + * Coursera uses two versions of each assignment. They both have the same assignment key and part id but have + * different item ids. + * + * @param key Assignment key + * @param partId Assignment partId + * @param itemId Item id of the non premium version + * @param premiumItemId Item id of the premium version (`None` if the assignment is optional) + */ +case class CourseraId(key: String, partId: String, itemId: String, premiumItemId: Option[String]) + +/** + * Settings shared by all assignments, reused in various tasks. + */ +object MOOCSettings extends AutoPlugin { + + object autoImport { + val course = SettingKey[String]("course") + val assignment = SettingKey[String]("assignment") + val options = SettingKey[Map[String, Map[String, String]]]("options") + val courseraId = settingKey[CourseraId]("Coursera-specific information identifying the assignment") + val testSuite = settingKey[String]("Fully qualified name of the test suite of this assignment") + // Convenient alias + type CourseraId = ch.epfl.lamp.CourseraId + val CourseraId = ch.epfl.lamp.CourseraId + } + + import autoImport._ + + override val globalSettings: Seq[Def.Setting[_]] = Seq( + // supershell is verbose, buggy and useless. + useSuperShell := false + ) + + override val projectSettings: Seq[Def.Setting[_]] = Seq( + parallelExecution in Test := false, + // Report test result after each test instead of waiting for every test to finish + logBuffered in Test := false, + name := s"${course.value}-${assignment.value}" + ) +} diff --git a/project/StudentTasks.scala b/project/StudentTasks.scala new file mode 100644 index 0000000..7604830 --- /dev/null +++ b/project/StudentTasks.scala @@ -0,0 +1,318 @@ +package ch.epfl.lamp + +import sbt._ +import Keys._ + +// import scalaj.http._ +import java.io.{File, FileInputStream, IOException} +import org.apache.commons.codec.binary.Base64 +// import play.api.libs.json.{Json, JsObject, JsPath} +import scala.util.{Failure, Success, Try} + +/** + * Provides tasks for submitting the assignment + */ +object StudentTasks extends AutoPlugin { + + override def requires = super.requires && MOOCSettings + + object autoImport { + val packageSourcesOnly = TaskKey[File]("packageSourcesOnly", "Package the sources of the project") + val packageBinWithoutResources = TaskKey[File]("packageBinWithoutResources", "Like packageBin, but without the resources") + val packageSubmissionZip = TaskKey[File]("packageSubmissionZip") + val packageSubmission = inputKey[Unit]("package solution as an archive file") + val runGradingTests = taskKey[Unit]("run black-box tests used for final grading") + } + + + import autoImport._ + import MOOCSettings.autoImport._ + + override lazy val projectSettings = Seq( + packageSubmissionSetting, + // submitSetting, + runGradingTestsSettings, + + fork := true, + connectInput in run := true, + outputStrategy := Some(StdoutOutput), + ) ++ packageSubmissionZipSettings + + lazy val runGradingTestsSettings = runGradingTests := { + val testSuiteJar = "grading-tests.jar" + if (!new File(testSuiteJar).exists) { + throw new MessageOnlyException(s"Could not find tests JarFile: $testSuiteJar") + } + + val classPath = s"${(Test / dependencyClasspath).value.map(_.data).mkString(File.pathSeparator)}${File.pathSeparator}$testSuiteJar" + val junitProcess = + Fork.java.fork( + ForkOptions(), + "-cp" :: classPath :: + "org.junit.runner.JUnitCore" :: + (Test / testSuite).value :: + Nil + ) + + // Wait for tests to complete. + junitProcess.exitValue() + } + + + /** ********************************************************** + * SUBMITTING A SOLUTION TO COURSERA + */ + + val packageSubmissionZipSettings = Seq( + packageSubmissionZip := { + val submission = crossTarget.value / "submission.zip" + val sources = (packageSourcesOnly in Compile).value + val binaries = (packageBinWithoutResources in Compile).value + IO.zip(Seq(sources -> "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/reductions/Interfaces.scala b/src/main/scala/reductions/Interfaces.scala new file mode 100644 index 0000000..4f64d60 --- /dev/null +++ b/src/main/scala/reductions/Interfaces.scala @@ -0,0 +1,26 @@ +package reductions + +// Interfaces used by the grading infrastructure. Do not change signatures +// or your submission will fail with a NoSuchMethodError. + +trait LineOfSightInterface { + def lineOfSight(input: Array[Float], output: Array[Float]): Unit + def upsweepSequential(input: Array[Float], from: Int, until: Int): Float + def upsweep(input: Array[Float], from: Int, end: Int, threshold: Int): Tree + def downsweepSequential(input: Array[Float], output: Array[Float], startingAngle: Float, from: Int, until: Int): Unit + def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float, tree: Tree): Unit + def parLineOfSight(input: Array[Float], output: Array[Float], threshold: Int): Unit +} + +trait ParallelCountChangeInterface { + def countChange(money: Int, coins: List[Int]): Int + def parCountChange(money: Int, coins: List[Int], threshold: (Int, List[Int]) => Boolean): Int + def moneyThreshold(startingMoney: Int): (Int, List[Int]) => Boolean + def totalCoinsThreshold(totalCoins: Int): (Int, List[Int]) => Boolean + def combinedThreshold(startingMoney: Int, allCoins: List[Int]): (Int, List[Int]) => Boolean +} + +trait ParallelParenthesesBalancingInterface { + def balance(chars: Array[Char]): Boolean + def parBalance(chars: Array[Char], threshold: Int): Boolean +} diff --git a/src/main/scala/reductions/LineOfSight.scala b/src/main/scala/reductions/LineOfSight.scala new file mode 100644 index 0000000..5d7ebfb --- /dev/null +++ b/src/main/scala/reductions/LineOfSight.scala @@ -0,0 +1,89 @@ +package reductions + +import org.scalameter._ + +object LineOfSightRunner { + + val standardConfig = config( + Key.exec.minWarmupRuns -> 40, + Key.exec.maxWarmupRuns -> 80, + Key.exec.benchRuns -> 100, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val length = 10000000 + val input = (0 until length).map(_ % 100 * 1.0f).toArray + val output = new Array[Float](length + 1) + val seqtime = standardConfig measure { + LineOfSight.lineOfSight(input, output) + } + println(s"sequential time: $seqtime") + + val partime = standardConfig measure { + LineOfSight.parLineOfSight(input, output, 10000) + } + println(s"parallel time: $partime") + println(s"speedup: ${seqtime.value / partime.value}") + } +} + +sealed abstract class Tree { + def maxPrevious: Float +} + +case class Node(left: Tree, right: Tree) extends Tree { + val maxPrevious = left.maxPrevious.max(right.maxPrevious) +} + +case class Leaf(from: Int, until: Int, maxPrevious: Float) extends Tree + +object LineOfSight extends LineOfSightInterface { + + def lineOfSight(input: Array[Float], output: Array[Float]): Unit = { + ??? + } + + /** Traverses the specified part of the array and returns the maximum angle. + */ + def upsweepSequential(input: Array[Float], from: Int, until: Int): Float = { + ??? + } + + /** Traverses the part of the array starting at `from` and until `end`, and + * returns the reduction tree for that part of the array. + * + * The reduction tree is a `Leaf` if the length of the specified part of the + * array is smaller or equal to `threshold`, and a `Node` otherwise. + * If the specified part of the array is longer than `threshold`, then the + * work is divided and done recursively in parallel. + */ + def upsweep(input: Array[Float], from: Int, end: Int, + threshold: Int): Tree = { + ??? + } + + /** Traverses the part of the `input` array starting at `from` and until + * `until`, and computes the maximum angle for each entry of the output array, + * given the `startingAngle`. + */ + def downsweepSequential(input: Array[Float], output: Array[Float], + startingAngle: Float, from: Int, until: Int): Unit = { + ??? + } + + /** Pushes the maximum angle in the prefix of the array to each leaf of the + * reduction `tree` in parallel, and then calls `downsweepSequential` to write + * the `output` angles. + */ + def downsweep(input: Array[Float], output: Array[Float], startingAngle: Float, + tree: Tree): Unit = { + ??? + } + + /** Compute the line-of-sight in parallel. */ + def parLineOfSight(input: Array[Float], output: Array[Float], + threshold: Int): Unit = { + ??? + } +} diff --git a/src/main/scala/reductions/ParallelCountChange.scala b/src/main/scala/reductions/ParallelCountChange.scala new file mode 100644 index 0000000..b5bd150 --- /dev/null +++ b/src/main/scala/reductions/ParallelCountChange.scala @@ -0,0 +1,79 @@ +package reductions + +import org.scalameter._ + +object ParallelCountChangeRunner { + + @volatile var seqResult = 0 + + @volatile var parResult = 0 + + val standardConfig = config( + Key.exec.minWarmupRuns -> 20, + Key.exec.maxWarmupRuns -> 40, + Key.exec.benchRuns -> 80, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val amount = 250 + val coins = List(1, 2, 5, 10, 20, 50) + val seqtime = standardConfig measure { + seqResult = ParallelCountChange.countChange(amount, coins) + } + println(s"sequential result = $seqResult") + println(s"sequential count time: $seqtime") + + def measureParallelCountChange(threshold: => ParallelCountChange.Threshold): Unit = try { + val fjtime = standardConfig measure { + parResult = ParallelCountChange.parCountChange(amount, coins, threshold) + } + println(s"parallel result = $parResult") + println(s"parallel count time: $fjtime") + println(s"speedup: ${seqtime.value / fjtime.value}") + } catch { + case e: NotImplementedError => + println("Not implemented.") + } + + println("\n# Using moneyThreshold\n") + measureParallelCountChange(ParallelCountChange.moneyThreshold(amount)) + println("\n# Using totalCoinsThreshold\n") + measureParallelCountChange(ParallelCountChange.totalCoinsThreshold(coins.length)) + println("\n# Using combinedThreshold\n") + measureParallelCountChange(ParallelCountChange.combinedThreshold(amount, coins)) + } +} + +object ParallelCountChange extends ParallelCountChangeInterface { + + /** Returns the number of ways change can be made from the specified list of + * coins for the specified amount of money. + */ + def countChange(money: Int, coins: List[Int]): Int = { + ??? + } + + type Threshold = (Int, List[Int]) => Boolean + + /** In parallel, counts the number of ways change can be made from the + * specified list of coins for the specified amount of money. + */ + def parCountChange(money: Int, coins: List[Int], threshold: Threshold): Int = { + ??? + } + + /** Threshold heuristic based on the starting money. */ + def moneyThreshold(startingMoney: Int): Threshold = + ??? + + /** Threshold heuristic based on the total number of initial coins. */ + def totalCoinsThreshold(totalCoins: Int): Threshold = + ??? + + + /** Threshold heuristic based on the starting money and the initial list of coins. */ + def combinedThreshold(startingMoney: Int, allCoins: List[Int]): Threshold = { + ??? + } +} diff --git a/src/main/scala/reductions/ParallelParenthesesBalancing.scala b/src/main/scala/reductions/ParallelParenthesesBalancing.scala new file mode 100644 index 0000000..fcf1c36 --- /dev/null +++ b/src/main/scala/reductions/ParallelParenthesesBalancing.scala @@ -0,0 +1,64 @@ +package reductions + +import scala.annotation._ +import org.scalameter._ + +object ParallelParenthesesBalancingRunner { + + @volatile var seqResult = false + + @volatile var parResult = false + + val standardConfig = config( + Key.exec.minWarmupRuns -> 40, + Key.exec.maxWarmupRuns -> 80, + Key.exec.benchRuns -> 120, + Key.verbose -> false + ) withWarmer(new Warmer.Default) + + def main(args: Array[String]): Unit = { + val length = 100000000 + val chars = new Array[Char](length) + val threshold = 10000 + val seqtime = standardConfig measure { + seqResult = ParallelParenthesesBalancing.balance(chars) + } + println(s"sequential result = $seqResult") + println(s"sequential balancing time: $seqtime") + + val fjtime = standardConfig measure { + parResult = ParallelParenthesesBalancing.parBalance(chars, threshold) + } + println(s"parallel result = $parResult") + println(s"parallel balancing time: $fjtime") + println(s"speedup: ${seqtime.value / fjtime.value}") + } +} + +object ParallelParenthesesBalancing extends ParallelParenthesesBalancingInterface { + + /** Returns `true` iff the parentheses in the input `chars` are balanced. + */ + def balance(chars: Array[Char]): Boolean = { + ??? + } + + /** Returns `true` iff the parentheses in the input `chars` are balanced. + */ + def parBalance(chars: Array[Char], threshold: Int): Boolean = { + + def traverse(idx: Int, until: Int, arg1: Int, arg2: Int) /*: ???*/ = { + ??? + } + + def reduce(from: Int, until: Int) /*: ???*/ = { + ??? + } + + reduce(0, chars.length) == ??? + } + + // For those who want more: + // Prove that your reduction operator is associative! + +} diff --git a/src/main/scala/reductions/package.scala b/src/main/scala/reductions/package.scala new file mode 100644 index 0000000..59e0438 --- /dev/null +++ b/src/main/scala/reductions/package.scala @@ -0,0 +1,58 @@ +import java.util.concurrent._ +import scala.util.DynamicVariable + +import org.scalameter._ + +package object reductions { + 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) + } + + // 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/test/scala/reductions/ReductionsSuite.scala b/src/test/scala/reductions/ReductionsSuite.scala new file mode 100644 index 0000000..11c8250 --- /dev/null +++ b/src/test/scala/reductions/ReductionsSuite.scala @@ -0,0 +1,124 @@ +package reductions + +import java.util.concurrent._ +import scala.collection._ +import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory +import org.junit._ +import org.junit.Assert.assertEquals + +class ReductionsSuite { + /***************** + * LINE OF SIGHT * + *****************/ + + import LineOfSight._ + @Test def `lineOfSight should correctly handle an array of size 4`: Unit = { + val output = new Array[Float](4) + lineOfSight(Array[Float](0f, 1f, 8f, 9f), output) + assertEquals(List(0f, 1f, 4f, 4f), output.toList) + } + + + + + /******************************* + * PARALLEL COUNT CHANGE SUITE * + *******************************/ + + import ParallelCountChange._ + + @Test def `countChange should return 0 for money < 0`: Unit = { + def check(money: Int, coins: List[Int]) = + assert(countChange(money, coins) == 0, + s"countChang($money, _) should be 0") + + check(-1, List()) + check(-1, List(1, 2, 3)) + check(-Int.MinValue, List()) + check(-Int.MinValue, List(1, 2, 3)) + } + + @Test def `countChange should return 1 when money == 0`: Unit = { + def check(coins: List[Int]) = + assert(countChange(0, coins) == 1, + s"countChang(0, _) should be 1") + + check(List()) + check(List(1, 2, 3)) + check(List.range(1, 100)) + } + + @Test def `countChange should return 0 for money > 0 and coins = List()`: Unit = { + def check(money: Int) = + assert(countChange(money, List()) == 0, + s"countChang($money, List()) should be 0") + + check(1) + check(Int.MaxValue) + } + + @Test def `countChange should work when there is only one coin`: Unit = { + def check(money: Int, coins: List[Int], expected: Int) = + assert(countChange(money, coins) == expected, + s"countChange($money, $coins) should be $expected") + + check(1, List(1), 1) + check(2, List(1), 1) + check(1, List(2), 0) + check(Int.MaxValue, List(Int.MaxValue), 1) + check(Int.MaxValue - 1, List(Int.MaxValue), 0) + } + + @Test def `countChange should work for multi-coins`: Unit = { + def check(money: Int, coins: List[Int], expected: Int) = + assert(countChange(money, coins) == expected, + s"countChange($money, $coins) should be $expected") + + check(50, List(1, 2, 5, 10), 341) + check(250, List(1, 2, 5, 10, 20, 50), 177863) + } + + + /********************************** + * PARALLEL PARENTHESES BALANCING * + **********************************/ + + import ParallelParenthesesBalancing._ + + @Test def `balance should work for empty string`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("", true) + } + + @Test def `balance should work for string of length 1`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("(", false) + check(")", false) + check(".", true) + } + + @Test def `balance should work for string of length 2`: Unit = { + def check(input: String, expected: Boolean) = + assert(balance(input.toArray) == expected, + s"balance($input) should be $expected") + + check("()", true) + check(")(", false) + check("((", false) + check("))", false) + check(".)", false) + check(".(", false) + check("(.", false) + check(").", false) + } + + + @Rule def individualTestTimeout = new org.junit.rules.Timeout(10 * 1000) +} +