11"""
2- Plot the Mandelbrot set using parallel workers.
2+ Synthesize an image in chunks using parallel workers.
33
44Usage:
5- $ python parallelization .py
5+ $ python parallel_demo .py
66"""
77
88import functools
99import multiprocessing
10- import os
1110import time
1211from dataclasses import dataclass
1312from math import log
14- from typing import Callable , Iterable
13+ from os import cpu_count
14+ from typing import Callable , Iterable , Iterator
1515
1616import numpy as np
1717from PIL import Image
1818
19- from splitting . multidimensional import Bounds , split_multi
19+ from spatial_splitting import Bounds , split_multi
2020
21- IMAGE_WIDTH , IMAGE_HEIGHT = 1280 , 720
21+ IMAGE_WIDTH , IMAGE_HEIGHT = 1920 , 1080
2222CENTER = - 0.7435 + 0.1314j
23- SCALE = 0.0000025
23+ SCALE = 0.0000015
2424MAX_ITERATIONS = 256
2525ESCAPE_RADIUS = 1000
26- NUM_CHUNKS = os . cpu_count () or 4
26+ NUM_CHUNKS = cpu_count () or 4
2727
2828
2929class Chunk :
@@ -35,12 +35,10 @@ def __init__(self, bounds: Bounds) -> None:
3535 self .width = bounds .size [1 ]
3636 self .pixels = np .zeros ((self .height , self .width ), dtype = np .uint8 )
3737
38- def __getitem__ (self , coordinates ) -> int :
39- """Get the value of a pixel at the given absolute coordinates."""
38+ def __getitem__ (self , coordinates : tuple [int , int ]) -> int :
4039 return self .pixels [self .bounds .offset (* coordinates )]
4140
42- def __setitem__ (self , coordinates , value : int ) -> None :
43- """Set the value of a pixel at the given absolute coordinates."""
41+ def __setitem__ (self , coordinates : tuple [int , int ], value : int ) -> None :
4442 self .pixels [self .bounds .offset (* coordinates )] = value
4543
4644
@@ -49,14 +47,14 @@ class MandelbrotSet:
4947 max_iterations : int
5048 escape_radius : float = 2.0
5149
52- def __contains__ (self , c : complex ) -> bool :
50+ def __contains__ (self , c ) :
5351 return self .stability (c ) == 1
5452
55- def stability (self , c : complex , smooth = False , clamp = True ) -> float :
53+ def stability (self , c , smooth = False , clamp = True ):
5654 value = self .escape_count (c , smooth ) / self .max_iterations
5755 return max (0.0 , min (value , 1.0 )) if clamp else value
5856
59- def escape_count (self , c : complex , smooth = False ) -> int | float :
57+ def escape_count (self , c , smooth = False ):
6058 z = 0 + 0j
6159 for iteration in range (self .max_iterations ):
6260 z = z ** 2 + c
@@ -67,6 +65,32 @@ def escape_count(self, c: complex, smooth=False) -> int | float:
6765 return self .max_iterations
6866
6967
68+ def transform (y : int , x : int ) -> complex :
69+ """Transform the given pixel coordinates to the complex plane."""
70+ im = SCALE * (IMAGE_HEIGHT / 2 - y )
71+ re = SCALE * (x - IMAGE_WIDTH / 2 )
72+ return complex (re , im ) + CENTER
73+
74+
75+ def generate_chunk (bounds : Bounds ) -> Chunk :
76+ """Generate a chunk of pixels for the given bounds."""
77+ chunk = Chunk (bounds )
78+ mandelbrot_set = MandelbrotSet (MAX_ITERATIONS , ESCAPE_RADIUS )
79+ for y , x in bounds :
80+ c = transform (y , x )
81+ instability = 1 - mandelbrot_set .stability (c , smooth = True )
82+ chunk [y , x ] = int (instability * 255 )
83+ return chunk
84+
85+
86+ def combine (chunks : Iterable [Chunk ]) -> Image .Image :
87+ """Combine the chunks into a single image."""
88+ pixels = np .zeros ((IMAGE_HEIGHT , IMAGE_WIDTH ), dtype = np .uint8 )
89+ for chunk in chunks :
90+ pixels [chunk .bounds .slices ()] = chunk .pixels
91+ return Image .fromarray (pixels , mode = "L" )
92+
93+
7094def timed (function : Callable ) -> Callable :
7195 @functools .wraps (function )
7296 def wrapper (* args , ** kwargs ):
@@ -79,53 +103,23 @@ def wrapper(*args, **kwargs):
79103 return wrapper
80104
81105
82- def main () -> None :
83- for worker in (process_chunks_parallel , process_chunks_sequential ):
84- image = compute (worker )
85- image .show ()
86-
106+ def process_sequentially (bounds_iter : Iterator [Bounds ]) -> Iterator [Chunk ]:
107+ return map (generate_chunk , bounds_iter )
87108
88- @timed
89- def process_chunks_sequential (chunked_bounds : Iterable [Bounds ]) -> list [Chunk ]:
90- return list (map (generate_chunk , chunked_bounds ))
91109
92-
93- @timed
94- def process_chunks_parallel (chunked_bounds : Iterable [Bounds ]) -> list [Chunk ]:
110+ def process_in_parallel (bounds_iter : Iterator [Bounds ]) -> list [Chunk ]:
95111 with multiprocessing .Pool () as pool :
96- return pool .map (generate_chunk , chunked_bounds )
97-
98-
99- def generate_chunk (bounds : Bounds ) -> Chunk :
100- """Generate a chunk of pixels for the given bounds."""
101- chunk = Chunk (bounds )
102- mandelbrot_set = MandelbrotSet (MAX_ITERATIONS , ESCAPE_RADIUS )
103- for y , x in bounds :
104- c = transform (y , x )
105- instability = 1 - mandelbrot_set .stability (c , smooth = True )
106- chunk [y , x ] = int (instability * 255 )
107- return chunk
108-
109-
110- def transform (y : int , x : int ) -> complex :
111- """Transform the given pixel coordinates to the complex plane."""
112- im = SCALE * (IMAGE_HEIGHT / 2 - y )
113- re = SCALE * (x - IMAGE_WIDTH / 2 )
114- return complex (re , im ) + CENTER
112+ return pool .map (generate_chunk , bounds_iter )
115113
116114
115+ @timed
117116def compute (worker : Callable ) -> Image .Image :
118- """Render the image using the given worker function."""
119- chunks = worker (split_multi (NUM_CHUNKS , IMAGE_HEIGHT , IMAGE_WIDTH ))
120- return combine (chunks )
117+ return combine (worker (split_multi (NUM_CHUNKS , IMAGE_HEIGHT , IMAGE_WIDTH )))
121118
122119
123- def combine (chunks : list [Chunk ]) -> Image .Image :
124- """Combine the chunks into a single image."""
125- pixels = np .zeros ((IMAGE_HEIGHT , IMAGE_WIDTH ), dtype = np .uint8 )
126- for chunk in chunks :
127- pixels [chunk .bounds .slices ()] = chunk .pixels
128- return Image .fromarray (pixels , mode = "L" )
120+ def main () -> None :
121+ for worker in (process_sequentially , process_in_parallel ):
122+ compute (worker ).show ()
129123
130124
131125if __name__ == "__main__" :
0 commit comments