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
178///   the image dimension ratio.)
179/// - Infinity distance
180/// - Upper bound on the iteration count
181///
182/// See the [Julia set theory] for more details regarding these parameters.
183///
184/// [Julia set theory]: index.html#theory
185#[derive(Debug, Clone)]
186pub struct Params {
187    view_center: [f32; 2],
188    view_height: f32,
189    inf_distance: f32,
190    image_size: [u32; 2],
191    max_iterations: u8,
192}
193
194impl Params {
195    /// Creates a new set of params with the specified `image_dimensions` and the `view_height`
196    /// of the rendered area.
197    ///
198    /// The remaining parameters are set as follows:
199    ///
200    /// - The width of the rendered area is inferred from these params.
201    /// - The view is centered at `0`.
202    /// - The infinity distance is set at `3`.
203    ///
204    /// # Panics
205    ///
206    /// Panics if any of the following conditions do not hold:
207    ///
208    /// - `image_dimensions` are positive
209    /// - `view_height` is positive
210    pub fn new(image_dimensions: [u32; 2], view_height: f32) -> Self {
211        assert!(image_dimensions[0] > 0);
212        assert!(image_dimensions[1] > 0);
213        assert!(view_height > 0.0, "`view_height` should be positive");
214
215        Self {
216            view_center: [0.0, 0.0],
217            view_height,
218            inf_distance: 3.0,
219            image_size: image_dimensions,
220            max_iterations: 100,
221        }
222    }
223
224    /// Sets the view center.
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    pub fn with_infinity_distance(mut self, inf_distance: f32) -> Self {
236        assert!(inf_distance > 0.0, "`inf_distance` should be positive");
237        self.inf_distance = inf_distance;
238        self
239    }
240
241    /// Sets the maximum iteration count.
242    ///
243    /// # Panics
244    ///
245    /// Panics if `max_iterations` is zero.
246    pub fn with_max_iterations(mut self, max_iterations: u8) -> Self {
247        assert_ne!(max_iterations, 0, "Max iterations must be positive");
248        self.max_iterations = max_iterations;
249        self
250    }
251
252    #[cfg(any(
253        feature = "cpu_backend",
254        feature = "opencl_backend",
255        feature = "vulkan_backend"
256    ))]
257    #[allow(clippy::cast_precision_loss)] // loss of precision is acceptable
258    pub(crate) fn view_width(&self) -> f32 {
259        self.view_height * (self.image_size[0] as f32) / (self.image_size[1] as f32)
260    }
261}