From 1ae03cfb4336af43dd3df0fb81d4893bc5ed23dc Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Thu, 28 Feb 2019 11:53:56 -0500 Subject: [PATCH] Add mandlebrot generator example --- mandelbrot/Cargo.toml | 11 ++ mandelbrot/src/main.rs | 224 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 mandelbrot/Cargo.toml create mode 100644 mandelbrot/src/main.rs diff --git a/mandelbrot/Cargo.toml b/mandelbrot/Cargo.toml new file mode 100644 index 0000000..d4e36c3 --- /dev/null +++ b/mandelbrot/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mandelbrot" +version = "0.1.0" +authors = ["Timothy Warren "] +edition = "2018" + +[dependencies] +crossbeam = "0.2.8" +image = "0.13.0" +num = "0.1.27" + diff --git a/mandelbrot/src/main.rs b/mandelbrot/src/main.rs new file mode 100644 index 0000000..50958a0 --- /dev/null +++ b/mandelbrot/src/main.rs @@ -0,0 +1,224 @@ +extern crate crossbeam; +extern crate image; +extern crate num; + +use image::ColorType; +use image::png::PNGEncoder; +use num::Complex; +use std::fs::File; +use std::io::Write; +use std::str::FromStr; + +/// Parse the string `s` as a coordinate pair, like `"400x600"` or `"1.0,0.5"`. +/// +/// Specifically, `s` should have the form , where is +/// the character given by the `separator` argument, and and are both +/// strings that can be parsed by `T::from_str`. +/// +/// If `s` has the proper form, return `Some<(x, y)>`. If it doesn't parse +/// correctly, return `None` +fn parse_pair(s: &str, separator: char) -> Option<(T,T)> { + match s.find(separator) { + None => None, + Some(index) => { + match (T::from_str(&s[..index]), T::from_str(&s[index + 1..])) { + (Ok(l), Ok(r)) => Some((l,r)), + _ => None + } + } + } +} + +#[test] +fn test_parse_pair() { + assert_eq!(parse_pair::("", ','), None); + assert_eq!(parse_pair::("10,", ','), None); + assert_eq!(parse_pair::("10,20", ','), Some((10, 20))); + assert_eq!(parse_pair::("10,20xy", ','), None); + assert_eq!(parse_pair::("0.5x", 'x'), None); + assert_eq!(parse_pair::("0.5x1.5", 'x'), Some((0.5, 1.5))); +} + +/// Try to determine if `c` is in the Mandelbrot set, using at most `limit` +/// iterations to decide. +/// +/// If `c` is not a member, return `Some(i)` where `i` is the number of +/// iterations it took for `c` to leave the circle of radius two centered on the +/// origin. If `c` seems to be a member (more precisely, if we reached the +/// iteration limit without being able to prove that `c` is not a member), +/// return `None` +fn escape_time(c: Complex, limit: u32) -> Option { + let mut z = Complex { re: 0.0, im: 0.0 }; + + for i in 0..limit { + z = z * z + c; + if z.norm_sqr() > 4.0 { + return Some(i); + } + } + + None +} + +/// Parse a pair of floating-point numbers separated by a comma as a complex +/// number +fn parse_complex(s: &str) -> Option> { + match parse_pair(s, ',') { + Some((re, im)) => Some(Complex { re, im }), + None => None, + } +} + +#[test] +fn test_parse_complex() { + assert_eq!(parse_complex("1.25,-0.0625"), Some(Complex { re: 1.25, im: -0.0625 })); + assert_eq!(parse_complex(",-0.0625"), None); +} + +/// Given the row and column of a pixel in the output image, return the +/// corresponding point on the complex plan. +/// +/// `bounds` is a pair giving the width and height of the image in pixels. +/// `pixel` is a (column, row) pair indicating a particular pixel in that image. +/// The `upper_left` and `lower_right` parameters are pionts on the complex +/// plain designating the area our image covers +fn pixel_to_point( + bounds: (usize, usize), + pixel: (usize, usize), + upper_left: Complex, + lower_right: Complex +) -> Complex { + let (width, height) = (lower_right.re - upper_left.re, upper_left.im - lower_right.im); + Complex { + re: upper_left.re + pixel.0 as f64 * width / bounds.0 as f64, + im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64, + // Why subtraction here? pixel.1 increases as we go down, + // but the imaginary component increases as we go up. + } +} + +#[test] +fn test_pixel_to_point() { + assert_eq!( + pixel_to_point( + (100, 100), + (25, 75), + Complex { re: -1.0, im: 1.0 }, + Complex { re: 1.0, im: -1.0 } + ), + Complex { re: -0.5, im: -0.5 } + ); +} + +/// Render a rectangle of the Mandlebrot set into a buffer of pixels. +/// +/// The `bounds` argument gives the width and height of the buffer `pixels`, +/// which holds one grayscale pixel per byte. The `upper_left` and `lower_right` +/// arguments specify points on the complex plane corresponding to the upper- +/// left and lower-right corners of the pixel buffer +fn render( + pixels: &mut [u8], + bounds: (usize, usize), + upper_left: Complex, + lower_right: Complex +) { + assert!(pixels.len() == bounds.0 * bounds.1); + + for row in 0..bounds.1 { + for column in 0..bounds.0 { + let point = pixel_to_point( + bounds, + (column, row), + upper_left, + lower_right + ); + + pixels[row * bounds.0 + column] = match escape_time(point, 255) { + None => 0, + Some(count) => 255 - count as u8, + }; + } + } +} + +/// Write the buffer `pixels`, whose dimensions are given by `bounds`, to the +/// file named `filename` +fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize)) + -> Result<(), std::io::Error> { + let output = File::create(filename)?; + + let encoder = PNGEncoder::new(output); + encoder.encode( + &pixels, + bounds.0 as u32, + bounds.1 as u32, + ColorType::Gray(8) + )?; + + Ok(()) +} + +#[allow(dead_code)] +fn complex_square_add_loop(c: Complex) { + let mut z = Complex { re: 0.0, im: 0.0 }; + loop { + z = z * z + c; + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + + if args.len() != 5 { + writeln!( + std::io::stderr(), + "Usage: mandlebrot FILE PIXELS UPPERLEFT LOWERRIGHT" + ).unwrap(); + + writeln!( + std::io::stderr(), + "Example: {} mandle.png 1000x750 -1.20,0.35 -1,0.20", + args[0] + ).unwrap(); + + std::process::exit(1); + } + + let bounds = parse_pair(&args[2], 'x') + .expect("error parsing image dimensions"); + + let upper_left = parse_complex(&args[3]) + .expect("error parsing upper left corner point"); + + let lower_right = parse_complex(&args[4]) + .expect("error parsing lower right corner point"); + + let mut pixels = vec![0; bounds.0 * bounds.1]; + + // render(&mut pixels, bounds, upper_left, lower_right); + + let threads = 8; + let rows_per_band = bounds.1 / threads + 1; + + { + let bands: Vec<&mut [u8]> = pixels + .chunks_mut(rows_per_band * bounds.0) + .collect(); + crossbeam::scope(|spawner| { + for (i, band) in bands.into_iter().enumerate() { + let top = rows_per_band * i; + let height = band.len() / bounds.0; + let band_bounds = (bounds.0, height); + let band_upper_left = pixel_to_point(bounds, (0, top), upper_left, lower_right); + let band_lower_right = pixel_to_point(bounds, (bounds.0, top + height), upper_left, lower_right); + + spawner.spawn(move || { + render(band, band_bounds, band_upper_left, band_lower_right); + }); + } + }); + } + + write_image(&args[1], &pixels, bounds) + .expect("error writing PNG file"); +}