julia_set/
lib.rs

1//! [Julia set] boundary computation and rendering.
2//!
3//! # Theory
4//!
5//! Informally, the Julia set for a complex-valued function `f` (in Rust terms,
6//! `fn(Complex32) -> Complex32`) is a set of complex points for which an infinitely small
7//! perturbation can lead to drastic changes in the sequence of iterated function applications
8//! (that is, `f(z)`, `f(f(z))`, `f(f(f(z)))` and so on).
9//!
10//! For many functions `f`, the iterated sequence may tend to infinity. Hence, the
11//! commonly used computational way to render the Julia set boundary is as follows:
12//!
13//! 1. For each complex value `z` within a rectangular area, perform steps 2-3.
14//! 2. Compute the minimum iteration `0 < i <= MAX_I` such that `|f(f(f(...(z)))| > R`.
15//!    Here, `f` is applied `i` times; `R` is a positive real-valued constant
16//!    (the *infinity distance*); `MAX_I` is a positive integer constant (maximum iteration count).
17//! 3. Associate `z` with a color depending on `i`. For example, `i == 1` may be rendered as black,
18//!    `i == MAX_I` as white, and values between it may get the corresponding shades of gray.
19//! 4. Render the rectangular area as a (two-dimensional) image, with each pixel corresponding
20//!    to a separate value of `z`.
21//!
22//! This is exactly the way Julia set rendering is implemented in this crate.
23//!
24//! [Julia set]: https://en.wikipedia.org/wiki/Julia_set
25//!
26//! # Backends
27//!
28//! The crate supports several computational [`Backend`]s.
29//!
30//! | Backend | Crate feature | Hardware | Crate dep(s) |
31//! |---------|---------------|----------|------------|
32//! | [`OpenCl`] | `opencl_backend` | GPU, CPU | [`ocl`] |
33//! | [`Vulkan`] | `vulkan_backend` | GPU | [`vulkano`], [`shaderc`] |
34//! | [`Cpu`] | `cpu_backend` | CPU | [`rayon`] |
35//! | [`Cpu`] | `dyn_cpu_backend` | CPU | [`rayon`] |
36//!
37//! None of the backends are on by default. A backend can be enabled by switching
38//! on the corresponding crate feature. `dyn_cpu_backend` requires `cpu_backend` internally.
39//!
40//! All backends except for `cpu_backend` require parsing the complex-valued [`Function`] from
41//! a string presentation, e.g., `"z * z - 0.4i"`. The [`arithmetic-parser`] crate is used for this
42//! purpose. For `cpu_backend`, the function is defined directly in Rust.
43//!
44//! For efficiency and modularity, a [`Backend`] creates a *program* for each function.
45//! (In case of OpenCL, a program is a kernel, and in Vulkan a program is a compute shader.)
46//! The program can then be [`Render`]ed with various [`Params`].
47//!
48//! Backends targeting GPUs (i.e., `OpenCl` and `Vulkan`) should be much faster than CPU-based
49//! backends. Indeed, the rendering task is [embarrassingly parallel] (could be performed
50//! independently for each point).
51//!
52//! [`ocl`]: https://crates.io/crates/ocl
53//! [`vulkano`]: https://crates.io/crates/vulkano
54//! [`shaderc`]: https://crates.io/crates/shaderc
55//! [`rayon`]: https://crates.io/crates/rayon
56//! [`arithmetic-parser`]: https://crates.io/crates/arithmetic-parser
57//! [embarrassingly parallel]: https://en.wikipedia.org/wiki/Embarrassingly_parallel
58//!
59//! # Examples
60//!
61//! Using Rust function definition with `cpu_backend`:
62//!
63//! ```
64//! use julia_set::{Backend, Cpu, Params, Render};
65//! use num_complex::Complex32;
66//!
67//! # fn main() -> anyhow::Result<()> {
68//! let program = Cpu.create_program(|z: Complex32| z * z + Complex32::new(-0.4, 0.5))?;
69//! let render_params = Params::new([50, 50], 4.0).with_infinity_distance(5.0);
70//! let image = program.render(&render_params)?;
71//! // Do something with the image...
72//! # Ok(())
73//! # }
74//! ```
75//!
76//! Using interpreted function definition with `dyn_cpu_backend`:
77//!
78//! ```
79//! use julia_set::{Backend, Cpu, Function, Params, Render};
80//! use num_complex::Complex32;
81//!
82//! # fn main() -> anyhow::Result<()> {
83//! let function: Function = "z * z - 0.4 + 0.5i".parse()?;
84//! let program = Cpu.create_program(&function)?;
85//! let render_params = Params::new([50, 50], 4.0).with_infinity_distance(5.0);
86//! let image = program.render(&render_params)?;
87//! // Do something with the image...
88//! # Ok(())
89//! # }
90//! ```
91
92#![cfg_attr(docsrs, feature(doc_cfg))]
93#![doc(html_root_url = "https://docs.rs/julia-set/0.1.0")]
94#![warn(missing_docs, missing_debug_implementations, bare_trait_objects)]
95#![warn(clippy::all, clippy::pedantic)]
96#![allow(
97    clippy::missing_errors_doc,
98    clippy::must_use_candidate,
99    clippy::module_name_repetitions,
100    clippy::doc_markdown
101)]
102
103use std::fmt;
104
105#[cfg(feature = "cpu_backend")]
106pub use crate::cpu::{ComputePoint, Cpu, CpuProgram};
107#[cfg(feature = "arithmetic-parser")]
108pub use crate::function::{FnError, Function};
109#[cfg(feature = "opencl_backend")]
110pub use crate::opencl::{OpenCl, OpenClProgram};
111#[cfg(feature = "vulkan_backend")]
112pub use crate::vulkan::{Vulkan, VulkanProgram};
113
114#[cfg(any(feature = "opencl_backend", feature = "vulkan_backend"))]
115mod compiler;
116#[cfg(feature = "cpu_backend")]
117mod cpu;
118#[cfg(feature = "arithmetic-parser")]
119mod function;
120#[cfg(feature = "opencl_backend")]
121mod opencl;
122pub mod transform;
123#[cfg(feature = "vulkan_backend")]
124mod vulkan;
125
126/// Image buffer output by a [`Backend`].
127pub type ImageBuffer = image::GrayImage;
128
129/// Backend capable of converting an input (the type parameter) into a program. The program
130/// then can be used to [`Render`] the Julia set with various rendering [`Params`].
131pub trait Backend<In>: Default {
132    /// Error that may be returned during program creation.
133    type Error: fmt::Debug + fmt::Display;
134    /// Program output by the backend.
135    type Program: Render;
136
137    /// Creates a program.
138    ///
139    /// # Errors
140    ///
141    /// May return an error if program cannot be created (out of memory, etc.).
142    fn create_program(&self, function: In) -> Result<Self::Program, Self::Error>;
143}
144
145/// Program for a specific [`Backend`] (e.g., OpenCL) corresponding to a specific Julia set.
146/// A single program can be rendered with different parameters (e.g., different output sizes),
147/// but the core settings (e.g., the complex-valued function describing the set) are fixed.
148pub trait Render {
149    /// Error that may be returned during rendering.
150    type Error: fmt::Debug + fmt::Display;
151
152    /// Renders the Julia set with the specified parameters.
153    ///
154    /// The rendered image is grayscale; each pixel represents the number of iterations to reach
155    /// infinity [as per the Julia set boundary definition](index.html#theory). This number is
156    /// normalized to the `[0, 255]` range regardless of the maximum iteration count from `params`.
157    ///
158    /// You can use the [`transform`] module and/or tools from the [`image`] / [`imageproc`] crates
159    /// to post-process the image.
160    ///
161    /// [`image`]: https://crates.io/crates/image
162    /// [`imageproc`]: https://crates.io/crates/imageproc
163    ///
164    /// # Errors
165    ///
166    /// May return an error if the backend does not support rendering with the specified params
167    /// or due to external reasons (out of memory, etc.).
168    fn render(&self, params: &Params) -> Result<ImageBuffer, Self::Error>;
169}
170
171/// Julia set rendering parameters.
172///
173/// The parameters are:
174///
175/// - Image dimensions (in pixels)
176/// - View dimensions and view center determining the rendered area. (Only the view height
177///   is specified explicitly; the width is inferred from the height and the image dimension ratio.)
178/// - Infinity distance
179/// - Upper bound on the iteration count
180///
181/// See the [Julia set theory] for more details regarding these parameters.
182///
183/// [Julia set theory]: index.html#theory
184#[derive(Debug, Clone)]
185pub struct Params {
186    view_center: [f32; 2],
187    view_height: f32,
188    inf_distance: f32,
189    image_size: [u32; 2],
190    max_iterations: u8,
191}
192
193impl Params {
194    /// Creates a new set of params with the specified `image_dimensions` and the `view_height`
195    /// of the rendered area.
196    ///
197    /// The remaining parameters are set as follows:
198    ///
199    /// - The width of the rendered area is inferred from these params.
200    /// - The view is centered at `0`.
201    /// - The infinity distance is set at `3`.
202    ///
203    /// # Panics
204    ///
205    /// Panics if any of the following conditions do not hold:
206    ///
207    /// - `image_dimensions` are positive
208    /// - `view_height` is positive
209    pub fn new(image_dimensions: [u32; 2], view_height: f32) -> Self {
210        assert!(image_dimensions[0] > 0);
211        assert!(image_dimensions[1] > 0);
212        assert!(view_height > 0.0, "`view_height` should be positive");
213
214        Self {
215            view_center: [0.0, 0.0],
216            view_height,
217            inf_distance: 3.0,
218            image_size: image_dimensions,
219            max_iterations: 100,
220        }
221    }
222
223    /// Sets the view center.
224    #[must_use]
225    pub fn with_view_center(mut self, view_center: [f32; 2]) -> Self {
226        self.view_center = view_center;
227        self
228    }
229
230    /// Sets the infinity distance.
231    ///
232    /// # Panics
233    ///
234    /// Panics if the provided distance is not positive.
235    #[must_use]
236    pub fn with_infinity_distance(mut self, inf_distance: f32) -> Self {
237        assert!(inf_distance > 0.0, "`inf_distance` should be positive");
238        self.inf_distance = inf_distance;
239        self
240    }
241
242    /// Sets the maximum iteration count.
243    ///
244    /// # Panics
245    ///
246    /// Panics if `max_iterations` is zero.
247    #[must_use]
248    pub fn with_max_iterations(mut self, max_iterations: u8) -> Self {
249        assert_ne!(max_iterations, 0, "Max iterations must be positive");
250        self.max_iterations = max_iterations;
251        self
252    }
253
254    #[cfg(any(
255        feature = "cpu_backend",
256        feature = "opencl_backend",
257        feature = "vulkan_backend"
258    ))]
259    #[allow(clippy::cast_precision_loss)] // loss of precision is acceptable
260    pub(crate) fn view_width(&self) -> f32 {
261        self.view_height * (self.image_size[0] as f32) / (self.image_size[1] as f32)
262    }
263}
264
265#[cfg(doctest)]
266doc_comment::doctest!("../README.md");