diff --git a/src/main/scala/org/learningconcurrency/exercises/ch5/ex3.scala b/src/main/scala/org/learningconcurrency/exercises/ch5/ex3.scala new file mode 100755 index 0000000..ebacb6f --- /dev/null +++ b/src/main/scala/org/learningconcurrency/exercises/ch5/ex3.scala @@ -0,0 +1,200 @@ +package org.learningconcurrency.exercises.ch5 + +import java.awt.Color +import java.awt.image.BufferedImage +import java.io.File +import java.util.concurrent.Executors +import javax.imageio.ImageIO + +import scala.annotation.tailrec +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.io.StdIn +import scala.util.{Success, Try} + +/** + * Implement a program that renders the Mandelbrot set in parallel. + */ +object Ex3 extends App { + + import org.learningconcurrency.ch5.warmedTimed + + case class MandelbrotParams(imageWidth: Int, + imageHeight: Int, + xMin: Double, + xMax: Double, + yMin: Double, + yMax: Double, + maxIterations: Int) { + val xStep = if (xMin >= 0 && xMax >= 0) { + (xMin + xMax) / imageWidth + } else if (xMin < 0 && xMax >= 0) { + (xMax - xMin) / imageWidth + } else { + (-xMin + xMax) / imageWidth + } + + val yStep = if (yMin >= 0 && yMax >= 0) { + (yMin + yMax) / imageHeight + } else if (xMin < 0 && yMax >= 0) { + (yMax - yMin) / imageHeight + } else { + (-yMin + yMax) / imageHeight + } + } + + type MandelbrotSet = Seq[Seq[Int]] + + trait MandelbrotSetBuilder { + def apply(params: MandelbrotParams): Future[MandelbrotSet] + + protected final def calculateMandelbrotElement(point: Complex, + maxIterations: Int = 1000 + ): Int = { + @tailrec + def iterate(i: Int = 0, z: Complex = Complex(0, 0)): Int = { + if (i < maxIterations && z.abs < 4) { + val newPoint = z.sqr + point + iterate(i + 1, newPoint) + } else { + // todo Either[Unit, Int] + if (i == maxIterations) -1 else i + } + } + iterate() + } + } + + case class Complex(x: Double, y: Double) { + def abs: Double = x * x + y * y + + def sqr: Complex = Complex(x * x - y * y, 2 * x * y) + + def +(c: Complex): Complex = Complex(x + c.x, y + c.y) + } + + class SingleThreadBlockingGenerator extends MandelbrotSetBuilder { + override def apply(params: MandelbrotParams): Future[Seq[Seq[Int]]] = { + import params._ + + val result = for { + y0 <- 0 until imageHeight + } yield for { + x0 <- 0 until imageWidth + } yield { + val xToCheck = xMin + x0 * xStep + val yToCheck = yMin + y0 * yStep + val complexValueToCheck = Complex(xToCheck, yToCheck) + calculateMandelbrotElement(complexValueToCheck, maxIterations = params.maxIterations) + } + Future.successful(result) + } + } + + class MultiThreadGenerator(segments: Int)(implicit ec: ExecutionContext) extends MandelbrotSetBuilder { + override def apply(params: MandelbrotParams): Future[Seq[Seq[Int]]] = { + import params._ + + val segmentHeight = params.imageHeight / segments + + val fs = for { + segment <- 1 to segments + } yield { + Future { + (segment, for { + y0 <- ((segment - 1) * segmentHeight) to (segment * segmentHeight) + } yield { + for { + x0 <- 0 until imageWidth + } yield { + val xToCheck = xMin + x0 * xStep + val yToCheck = yMin + y0 * yStep + val complexValueToCheck = Complex(xToCheck, yToCheck) + calculateMandelbrotElement(complexValueToCheck, maxIterations = params.maxIterations) + } + }) + } + } + + Future.sequence(fs).map(_.sortBy(_._1).flatMap(_._2)) + } + } + + class MandelbrotSetImageSaver(format: String, filePath: String) { + def apply(params: MandelbrotParams, set: MandelbrotSet): Unit = { + val bi = new BufferedImage(params.imageWidth, params.imageHeight, BufferedImage.TYPE_INT_RGB) + + val g = bi.createGraphics() + + import params._ + + val histogram = set.flatten + .groupBy(identity) + .map(g => (g._1, g._2.size)) + .filter(_._1 >= 0) + .map(g => (g._1 - 1, g._2)) + .withDefaultValue(0) + + val total = histogram.values.sum + + for { + px <- 0 until imageWidth + py <- 0 until imageHeight + } { + val numIters = set(py)(px) + + var colorVal = 0f + for (i <- 0 until numIters) { + colorVal += histogram(i) / total.toFloat + } + val rgb = Color.HSBtoRGB(0.1f + colorVal, 1.0f, colorVal * colorVal) + + g.setPaint(new Color(rgb)) + g.drawLine(px, py, px, py) + } + + ImageIO.write(bi, format, new File(filePath)) + } + } + + val processors = Runtime.getRuntime.availableProcessors() + + val pool = Executors.newFixedThreadPool(processors * 4) + implicit val ec = ExecutionContext.fromExecutor(pool) + + val singleThreadGenerator = new SingleThreadBlockingGenerator + val multiThreadGenerator = new MultiThreadGenerator(segments = processors * 10) + + val params = MandelbrotParams(900, 600, -2f, 1f, -1f, 1f, 1000) + + val warmupTimes = 30 + + print("Warmup single thread") + val singleThreadGeneratorTime = warmedTimed(warmupTimes) { + print(".") + singleThreadGenerator(params) + } + println + + println(s"Single thread generator time: $singleThreadGeneratorTime") + + print("Warmup future multi thread") + val multiThreadGeneratorTime = warmedTimed(warmupTimes) { + print(".") + Await.result(multiThreadGenerator(params), Duration.Inf) + } + println + + println(s"Multi thread generator time: $multiThreadGeneratorTime") + + println("Save result to PNG file? [y/n]") + Try(StdIn.readChar()) match { + case Success('y') => + val mandelbrotElements = Await.result(multiThreadGenerator(params), Duration.Inf) + val fileName = "mandelbrot.png" + new MandelbrotSetImageSaver("PNG", fileName).apply(params, mandelbrotElements) + println(s"See '$fileName' file with mandelbrot set in project folder") + case _ => + } + pool.shutdown() +} \ No newline at end of file diff --git a/src/main/scala/org/learningconcurrency/exercises/ch5/ex4.scala b/src/main/scala/org/learningconcurrency/exercises/ch5/ex4.scala new file mode 100755 index 0000000..d734b1b --- /dev/null +++ b/src/main/scala/org/learningconcurrency/exercises/ch5/ex4.scala @@ -0,0 +1,177 @@ +package org.learningconcurrency.exercises.ch5 + +import java.awt.Color +import java.awt.image.BufferedImage +import java.io.File +import java.util.concurrent.Executors +import javax.imageio.ImageIO + +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.io.StdIn +import scala.util.{Success, Try} + +/** + * Implement a program that simulates a cellular automaton in parallel. + */ +object Ex4 extends App { + + import org.learningconcurrency.ch5.warmedTimed + + trait CellularAutomator { + type State + type Locality + type Field <: Seq[State] + type Position + type Rule = Locality => State + + def rule: Rule + + def localitySize: Int + + def calculateNextState(currentState: Field): Field + + def calculateNextStates(currentState: Field, n: Int): Iterator[Field] + } + + object Rule30 extends OneDimentionCellularAutomator#Rule { + override def apply(locality: Byte): Boolean = locality match { + case 7 /*111*/ => false + case 6 /*110*/ => false + case 5 /*101*/ => false + case 4 /*100*/ => true + case 3 /*011*/ => true + case 2 /*010*/ => true + case 1 /*001*/ => true + case 0 /*000*/ => false + case v => throw new IllegalArgumentException(s"Value must be < 8 (with 3 cell counts), but $v found") + } + } + + trait OneDimentionCellularAutomator extends CellularAutomator { + override type State = Boolean + override type Locality = Byte + override type Field = Seq[Boolean] + override type Position = Int + + private val doubleLocality = localitySize * 2 + + protected def localityStates(pos: Position, currentState: Seq[Boolean]): Locality = { + currentState + .slice(pos - localitySize, pos + localitySize + 1) + .zipWithIndex + .foldLeft(0) { + case (r, (v, index)) => if (v) r | 1 << doubleLocality - index else r + }.toByte + } + } + + class SingleThreadOneDimentionCellularAutomator(override val rule: Byte => Boolean, + override val localitySize: Int) extends OneDimentionCellularAutomator { + override def calculateNextState(currentState: Seq[Boolean]): Seq[Boolean] = { + for { + x <- currentState.indices + } yield rule(localityStates(x, currentState)) + } + + override def calculateNextStates(currentState: Seq[Boolean], n: Int): Iterator[Seq[Boolean]] = { + Iterator.iterate(currentState)(calculateNextState).take(n) + } + } + + class FutureMultiThreadOneDimentionCellularAutomator(segments: Int, + override val rule: Byte => Boolean, + override val localitySize: Int)(implicit ec: ExecutionContext) extends OneDimentionCellularAutomator { + override def calculateNextState(currentState: Seq[Boolean]): Seq[Boolean] = { + // todo запускать рассчет соседей и после этого рассчет этой же точки в следующей генерации + val segmentSize = currentState.size / segments + val fs = for { + segment <- 1 to segments + } yield Future { + (segment, for { + x <- (segment - 1) * segmentSize until segment * segmentSize + } yield rule(localityStates(x, currentState))) + } + Await.result(Future.sequence(fs), Duration.Inf).sortBy(_._1).flatMap(_._2) + } + + override def calculateNextStates(currentState: Seq[Boolean], n: Int): Iterator[Seq[Boolean]] = { + Iterator.iterate(currentState)(calculateNextState).take(n) + } + } + + class ImageSaver(format: String, filePath: String) { + def apply(field: Iterator[Seq[Boolean]], size: Int, steps: Int) = { + val bi = new BufferedImage(size, steps, BufferedImage.TYPE_BYTE_BINARY) + + val g = bi.createGraphics() + g.setPaint(Color.WHITE) + + for { + (xs, y) <- field.zipWithIndex + (v, x) <- xs.zipWithIndex + if !v + } { + g.drawLine(x, y, x, y) + } + + ImageIO.write(bi, format, new File(filePath)) + } + } + + val singleThreadAutomator = new SingleThreadOneDimentionCellularAutomator(Rule30, 1) + + val processors = Runtime.getRuntime.availableProcessors() + + val pool = Executors.newFixedThreadPool(processors * 4) + implicit val ec = ExecutionContext.fromExecutor(pool) + val futureMultiThreadAutomator = new FutureMultiThreadOneDimentionCellularAutomator(processors * 10, Rule30, 1) + + val startState = { + val size = 50000 + val a = Array.fill(size)(false) + a(size / 2) = true + a + } + + val steps = 100 + val warmUps = 10 + + print("Warmup single thread") + val singleThreadTime = warmedTimed(warmUps) { + print(".") + singleThreadAutomator.calculateNextStates(startState, steps).toList + } + println + + println(s"Single thread calculation time: $singleThreadTime") + + print("Warmup future multi thread") + val futureMultiThreadTime = warmedTimed(warmUps) { + print(".") + futureMultiThreadAutomator.calculateNextStates(startState, steps).toList + } + println + + println(s"Future multi thread calculation time: $futureMultiThreadTime") + + println("Print result to console? [y/n]") + Try(StdIn.readChar()) match { + case Success('y') => + val result = futureMultiThreadAutomator.calculateNextStates(startState, steps) + result.foreach(field => println(field.map(v => if (v) '▉' else ' ').mkString)) + case _ => + } + + println("Save result to PNG file? [y/n]") + Try(StdIn.readChar()) match { + case Success('y') => + val result = futureMultiThreadAutomator.calculateNextStates(startState, steps) + val fileName = "cellular-automaton.png" + new ImageSaver("PNG", fileName).apply(result, startState.length, steps) + println(s"See '$fileName' file with cellular automaton evolution image in project folder") + case _ => + } + + pool.shutdown() +} diff --git a/src/main/scala/org/learningconcurrency/exercises/ch5/ex5.scala b/src/main/scala/org/learningconcurrency/exercises/ch5/ex5.scala new file mode 100755 index 0000000..20c8330 --- /dev/null +++ b/src/main/scala/org/learningconcurrency/exercises/ch5/ex5.scala @@ -0,0 +1,307 @@ +package org.learningconcurrency.exercises.ch5 + +import scala.swing.Panel + +/** + * Implement a parallel Barnes-Hut N-Body simulation algorithm. + */ +object Ex5 extends scala.swing.SimpleSwingApplication { + + import math._ + + type Mass = Double + type Distance = Double + type Angle = Double + + object Vector { + val empty = new Vector(Point.empty, 0, 0) + val x = new Vector(Point.empty, 0, 1) + } + + case class Vector(point: Point, angle: Angle = 0f, length: Double = 0f) { + def this(from: Point, to: Point, length: Double) = { + this(from, from.angle(to), length) + } + + def this(from: Point, to: Point) = { + this(from, from.angle(to), from.distance(to)) + } + + def +(other: Vector): Vector = { + if (other == Vector.empty) { + this + } else { + val newProjectionX = length * cos(angle) + other.length * cos(other.angle) + val newProjectionY = length * sin(angle) + other.length * sin(other.angle) + val newPoint = Point(point.x + other.point.x, point.y + other.point.y) + val res = new Vector(newPoint, newPoint + Point(newProjectionX, newProjectionY)) + res + } + } + + def unit = new Vector(Point(point.x / length, point.y / length), angle, 1f) + + def to: Point = Point(point.x + length * cos(angle), point.y + length * sin(angle)) + + def /(n: Double): Vector = copy(length = length / n) + + def *(n: Double): Vector = copy(length = length * n) + + def *(other: Vector): Vector = new Vector(point * other.point, point * other.point) + + def rotate(n: Angle): Vector = copy(angle = angle + n) + + // todo def rotateTo(n: Angle): Vector = copy(angle = angle + n) + + override def toString: String = { + f"Vector[point: $point, angle: $angle%.3f rad (${toDegrees(angle)}%.1f degrees), length: $length]" + } + } + + object Point { + val empty = Point(0, 0) + } + + case class Point(x: Double, y: Double) { + def +(other: Point) = + Point(x + other.x, y + other.y) + + def *(other: Point) = + Point(x * other.x, y * other.y) + + def distance(other: Point): Distance = + sqrt(pow(x - other.x, 2) + pow(y - other.y, 2)) + + def angle(other: Point /*, axleVector: Vector = Vector.x*/): Angle = { + val d = distance(other) + if (d == 0) { + 0f + } else { + // todo? + // if (difference == 0) { + // 0f + // } else { + // val cos_a = difference / d + // assert(cos_a <= 1) + // (if (difference < 0) -1 else 1) * acos(cos_a) + // } + // val d = distance(other) + // todo файри прислал ФОРМУЛУ + // val axleVectorFromPoint = axleVector.copy(point = this, length = distance(other)) + // val distanceBetweenOtherAndAxleVector = other.distance(axleVectorFromPoint.to) + // val vectorBetweenPoints = new Vector(this, other) + // val cosA = (axleVectorFromPoint * this) / d * axleVectorFromPoint.length + // val r = acos(cosA) + // r + val differenceX = other.x - x + val differenceY = other.y - y + (if (differenceY < 0 || differenceY < 0) -1 else 1) * + acos(differenceX / sqrt(pow(differenceX, 2) + pow(differenceY, 2))) + } + } + + // override def toString: String = f"Point[$x, $y]" + } + + abstract class Force { + def vector: Vector + + def apply(body: Body): Body + + def +(other: Force): Force + + override def toString: String = { + s"${getClass.getSimpleName}[$vector]" + } + } + + object Gravity { + val g = 6.67e-11f + + def apply(actBody: Body, toBody: Body): Gravity = { + val distance = actBody.position.distance(toBody.position) + val value = if (distance == 0) { + 0f + } else { + g * (actBody.mass * toBody.mass / pow(distance, 2)) + } + new Gravity(new Vector(toBody.position, actBody.position, value)) + } + } + + class Gravity(val vector: Vector = Vector.empty) extends Force { + def apply(body: Body): Body = { + body.copy(velocity = body.velocity.map(_ + Velocity((vector / body.mass).copy(point = Point.empty))).orElse(Some(Velocity((vector / body.mass).copy(point = Point.empty))))) + } + + def +(other: Force): Force = { + ??? + } + } + + // todo эти силы всегда направлены от тела + case class Velocity(val vector: Vector = Vector.empty) extends Force { + def apply(body: Body): Body = { + body.copy(speed = body.speed.map(_ + new Speed(vector)).orElse(Some(new Speed(vector)))) + } + + override def +(other: Force): Force = { + new Velocity(vector + other.vector) + } + } + + // todo эти силы всегда направлены от тела + class Speed(val vector: Vector = Vector.empty) extends Force { + def apply(body: Body): Body = { + body.copy(position = body.position + vector.to) + } + + def +(other: Force): Force = { + new Speed(vector + other.vector) + } + } + + object Body { + def massCenter(bodies: Seq[Body]): Body = { + bodies.reduce[Body] { + case (first, second) => + val mass = first.mass + second.mass + val x = (first.position.x * first.mass) + (second.position.x * second.mass) / mass + val y = (first.position.y * first.mass) + (second.position.y * second.mass) / mass + static(None, mass, Point(x, y)) + } + } + + def act(id: Option[Int], mass: Mass, position: Point): Body = { + Body(id, mass, position, None, None, act = true) + } + + def static(id: Option[Int], mass: Mass, position: Point): Body = { + Body(id, mass, position, None, None, act = false) + } + } + + case class Body private(id: Option[Int], mass: Mass, position: Point, speed: Option[Force], velocity: Option[Force], act: Boolean = false) { + override def toString: String = s"Body[id: $id, mass: $mass, position: $position, speed: $speed, velocity: $velocity, act: $act]" + } + + trait Simulator { + type State = Seq[Body] + + def bodies: State + + def nextStates(n: Int): Iterator[State] + } + + class BruteForceNaiveSimulator(override val bodies: Body*) extends Simulator { + def nextStates(n: Int): Iterator[State] = { + Iterator.iterate(bodies)(newState).take(n) + } + + protected def newState(currentState: State): State = { + val (actBodies, staticBodies) = currentState.partition(_.act) + val r = actBodies.map(body => { + val otherBodies = currentState.filterNot(_.id == body.id) + val forces = otherBodies.map(otherBody => Gravity(otherBody, body)) + newBodyState(body, forces) + }) + r ++ staticBodies + } + + protected def newBodyState(body: Body, forces: Seq[Force]): Body = { + // todo fold or sum? + // forces.reduce(_ + _).apply(body) + val bodyAfterForces = forces + .filterNot(_.vector == Vector.empty) + .foldLeft(body) { + case (b, force) => + println(s"Apply $force") + val res = force(b) + // println(s"Body after apply $force:\n $res") + res + } + println(s"Body after forces: $bodyAfterForces") + val bodyAfterVelocity = bodyAfterForces.velocity.map(_.apply(bodyAfterForces)).getOrElse(bodyAfterForces) + println(s"Body after velocity ${bodyAfterForces.velocity}: $bodyAfterVelocity") + val bodyAfterSpeed = bodyAfterVelocity.speed.map(s => s.apply(bodyAfterVelocity)).getOrElse(bodyAfterVelocity) + println(s"Body after speed ${bodyAfterVelocity.speed}: $bodyAfterSpeed") + bodyAfterSpeed + } + } + + val earth = Body.act(Some(1), "5.972e24".toDouble, Point(0, 0)) + val sun = Body.act(Some(2), "1.989e30".toDouble, Point("1.496e11".toDouble, 0)) + val simulator = new BruteForceNaiveSimulator(earth, sun) + + class SimulatorStatePanel(simulator: Simulator) extends Panel { + private val statesIterator = simulator.nextStates(Int.MaxValue) + private var currentStateOpt: Option[Simulator#State] = None + + private def scaleF: Double = "1.496e12".toDouble / size.width + + private def scale(value: Double): Int = (value / scaleF).toInt + + private val maxRadius = 10 + private var currentStateNumberOpt: Option[Int] = None + + listenTo(mouse.clicks) + nextState() + + def nextState(): Unit = { + val by = 1000 + currentStateOpt = Some(statesIterator.drop(by).next()) + currentStateNumberOpt = Some(currentStateNumberOpt.getOrElse(0) + by) + println(s"New step: $currentStateOpt") + repaint() + } + + override protected def paintComponent(g: swing.Graphics2D): Unit = { + val maxMass = currentStateOpt.map(_.map(_.mass).max).getOrElse(0L) + currentStateOpt match { + case Some(currentState) => currentState.foreach(body => { + // val radius = (maxRadius * (body.mass / maxMass)).toInt + val radius = maxRadius + val x = scale(body.position.x) - radius + val y = scale(body.position.y) - radius + println(s"Draw x: $x y: $y radius: $radius") + g.drawOval(x, y, 2 * radius, 2 * radius) + g.drawString(body.id.get.toString, 6 + x + (g.getFontMetrics.getWidths.head / 2).toFloat, y + g.getFontMetrics.getHeight.toFloat) + g.drawString(s"Step ${currentStateNumberOpt.get}", 10, 100) + }) + case None => + println("There're no state") + } + } + } + + // todo вынести + // todo доделать задание и все из этой главы + import rx.lang.scala._ + + import scala.swing._ + import scala.swing.event._ + + def top = new MainFrame { + title = "Swing Observables" + + // val button = new Button { + // text = "Click" + // } + + val panel = new SimulatorStatePanel(simulator) + // listenTo(panel.mouse.clicks) + contents = panel + + size = new Dimension(800, 600) + + val panelClicks = Observable[Unit] { sub => + panel.reactions += { + case e: MouseClicked => sub.onNext(()) + } + } + + panelClicks.subscribe(_ => { + panel.nextState + }) + } +}