From be9d41ad4d08552bb5498131d2cc67ed53903cd6 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 10 Jul 2021 00:58:18 -0700 Subject: [PATCH 01/44] Several different types of calculation options, mostly available from command line ("--flint" or "--decimal"), except for mpmath. Because of instantiation order, the way to run mpmath is to comment and un-comment the MandlContext instantiation at line ~693, and not specify --flint or --decimal. Didn't want to overly engineer this while prototyping, so just stacking implementations in seemed simple. PROBLEM: Currently command-line parameters for window width, height, and center, aren't parsed into their proper types, so you can only set them properly by editing the code in set_default_params(). Added an attempt at rudimentary caching of calculations before coloring applied. 3 new options with that are "--cache-path=subdir_name", "--build-cache", and "--invalidate_cache" I didn't add any sanity or safety checking for the cache parameters, which is nice and dangerous, because it does actually delete cache files too, depending on parameters. --- clint_runscript.sh | 8 + env.txt | 11 ++ mandelbrot.py | 444 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 412 insertions(+), 51 deletions(-) create mode 100755 clint_runscript.sh diff --git a/clint_runscript.sh b/clint_runscript.sh new file mode 100755 index 0000000..1a7517d --- /dev/null +++ b/clint_runscript.sh @@ -0,0 +1,8 @@ +python3 mandelbrot.py --gif=flint.gif --duration=.25 --color --verbose=3 --burn --flint --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=flint.gif --duration=.25 --color --verbose=3 --burn --flint --build-cache --invalidate-cache --cache-path='cache' + +#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color --verbose=3 --burn --decimal --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color --verbose=3 --burn --decimal --build-cache --invalidate-cache --cache-path='cache' + +#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color --verbose=3 --burn --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' diff --git a/env.txt b/env.txt index b25a969..5346a48 100644 --- a/env.txt +++ b/env.txt @@ -10,3 +10,14 @@ pip3 install ffmpeg movpiepy # needed for live preview /usr/local/bin/python3 -m pip install -U pygame --user + + +# needed for flint +# But, maybe needed first: +# libflint-dev +# libgmp3-dev +# cython +# python3-dev + +/usr/local/bin/python3 -m pip install flint-py + diff --git a/mandelbrot.py b/mandelbrot.py index 2733020..d138177 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -23,19 +23,29 @@ import getopt import sys import math +import os + +from decimal import * # Surely more specific on this will be better, sorry +import multiprocessing # Can't actually make this work yet - gonna need pickling? from collections import defaultdict import numpy as np import mpmath as mp -import moviepy.editor as mpy +import flint +import moviepy.editor as mpy from moviepy.audio.tools.cuts import find_audio_period from PIL import Image, ImageDraw, ImageFont MANDL_VER = "0.1" +DECIMAL_HIGH_PRECISION_SIZE = 16 # 16 places is roughly equivalent to 53 bits for float64, right? +#FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 +FLINT_HIGH_PRECISION_SIZE = 200 +MPMATH_HIGH_PRECISION_SIZE = 53 # 53 bits in a float64 + class MandlPalette: """ Color gradient @@ -125,12 +135,20 @@ def __init__(self, ctxf = None, ctxc = None, mp = None): self.cmplx_width = 0.0 # width of visualization in complex plane self.cmplx_height = 0.0 - + self.cmplx_width_decimal = Decimal('0.0') + self.cmplx_height_decimal = Decimal('0.0') + self.cmplx_width_flint = flint.arb('0.0') + self.cmplx_height_flint = flint.arb('0.0') + # point we're going to dive into - self.cmplx_center = complex(0.0) # center of image in complex plane + self.cmplx_center = self.ctxc(0.0) # center of image in complex plane + self.cmplx_center_real_decimal = Decimal('0.0') + self.cmplx_center_imag_decimal = Decimal('0.0') + self.cmplx_center_flint = flint.acb('0.0') self.max_iter = 0 # int max iterations before bailing self.escape_rad = 0. # float radius mod Z hits before it "escapes" + self.escape_squared = 0.0 # float squared radius, memoized self.scaling_factor = 0.0 # float amount to zoom each epoch self.num_epochs = 0 # int, nuber of epochs into the dive @@ -147,48 +165,167 @@ def __init__(self, ctxf = None, ctxc = None, mp = None): self.palette = None self.burn_in = False + self.use_high_precision = False + self.high_precision_type = 'flint' + + self.cache_path = "cache" + self.build_cache = False + self.invalidate_cache = False self.verbose = 0 # how much to print about progress def zoom_in(self, iterations=1): - while iterations: - self.cmplx_width *= self.scaling_factor - self.cmplx_height *= self.scaling_factor - self.num_epochs += 1 - iterations -= 1 - + if self.use_high_precision == True: + if self.high_precision_type == 'decimal': + oldPrecision = getcontext().prec + getcontext().prec = DECIMAL_HIGH_PRECISION_SIZE + while iterations: + self.cmplx_width_decimal *= Decimal(self.scaling_factor) + self.cmplx_height_decimal *= Decimal(self.scaling_factor) + self.num_epochs += 1 + iterations -= 1 + getcontext().prec = oldPrecision + else: # flint + while iterations: + self.cmplx_width_flint *= self.scaling_factor + self.cmplx_height_flint *= self.scaling_factor + self.num_epochs += 1 + iterations -= 1 + else: # Default native python, or maybe mpmath + while iterations: + self.cmplx_width *= self.scaling_factor + self.cmplx_height *= self.scaling_factor + self.num_epochs += 1 + iterations -= 1 def mandelbrot(self, c): z = self.ctxc(0) n = 0 - squared_escape = self.escape_rad * self.escape_rad + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + while ((z.real*z.real)+(z.imag*z.imag)) <= self.escape_squared and n < self.max_iter: + z = z*z + c + n += 1 + +# if n== self.max_iter: +# return self.max_iter +# +# # The following code smooths out the colors so there aren't bands +# # Algorithm taken from http://linas.org/art-gallery/escape/escape.html +# if self.smoothing: +# z = z*z + c; n+=1 # a couple extra iterations helps +# z = z*z + c; n+=1 # decrease the size of the error +# mu = n + 1 - math.log(self.mp.log2(abs(z))) +# return mu +# else: +# return n + + return n + + def mandelbrot_flint(self, c): + z = flint.acb(0.0) + n = 0 # fabs(z) returns the modulus of a complex number, which is the # distance to 0 (on a 2x2 cartesian plane) # # However, instead we just square both sides of the inequality to # avoid the sqrt - while ((z.real*z.real)+(z.imag*z.imag)) <= squared_escape and n < self.max_iter: + while ((z.real*z.real)+(z.imag*z.imag)) <= self.escape_squared and n < self.max_iter: z = z*z + c n += 1 + return n + + def mandelbrot_decimal(self, param_complex_array): + """Takes the 2 complex number components and returns the + number of iterations it took to become greater than the escape value. + + TODO: really need tests to make sure precision is being properly preserved""" + param_real = param_complex_array[0] + param_imag = param_complex_array[1] + + z_real = Decimal('0.0') + z_imag = Decimal('0.0') + n = 0 - if n== self.max_iter: - return self.max_iter + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + while((z_real*z_real) + (z_imag*z_imag)) <= self.escape_squared and n < self.max_iter: + prev_z_real = z_real + prev_z_imag = z_imag + + z_real = prev_z_real * prev_z_real - prev_z_imag * prev_z_imag + Decimal(param_real) + z_imag = prev_z_real * prev_z_imag * Decimal('2.0') + Decimal(param_imag) + n += 1 + + return n + + def linspace_decimal(self, paramFirst, paramLast, quantity): + first = Decimal(paramFirst) + last = Decimal(paramLast) + dataRange = last - first + answers = np.zeros((quantity, 1), dtype=object) + + for x in range(0, quantity): + answers[x] = first + (dataRange / (quantity - 1)) * x + + return answers + + def linspace(self, paramFirst, paramLast, quantity): + first = paramFirst + last = paramLast + dataRange = last - first + answers = np.zeros((quantity, 1), dtype=object) + + for x in range(0, quantity): + answers[x] = first + (dataRange / (quantity - 1)) * x + + return answers - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if self.smoothing: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - mu = n + 1 - math.log(self.mp.log2(abs(z))) - return mu - else: - return n + def load_frame(self, frame_number, cache_path, build_cache = True, invalidate_cache = False): + """Cache-aware data tuple loading or calculating""" + + cache_file_name = os.path.join(cache_path, u"%d.npy" % frame_number) + #print("cache file %s" % cache_file_name) + + if invalidate_cache == True and os.path.exists(cache_file_name) == True and os.path.isfile(cache_file_name): + os.remove(cache_file_name) + + frame_data = np.zeros((1,1), dtype=np.uint8) + if os.path.exists(cache_file_name) == False: + #print("Calculating epoch data") + if self.use_high_precision == True: + if self.high_precision_type == 'decimal': + #print(" (decimal high precision)") + frame_data = self.calculate_epoch_data_decimal(frame_number) + else: + #print(" (flint high precision)") + frame_data = self.calculate_epoch_data_flint() + else: + #print(" (normal precision as-compiled, perhaps mpmath?)") + frame_data = self.calculate_epoch_data(frame_number) + + if build_cache == True: + #print("Writing cache file") + if not os.path.exists(cache_path): + os.makedirs(cache_path) + + np.save(cache_file_name, frame_data) + else: + #print("Loading cache file") + frame_data = np.load(cache_file_name, allow_pickle=True) - def next_epoch(self, t, snapshot_filename = None): - """Called for each frame of the animation. Will calculate - current view, and then zoom in""" + return frame_data + + def calculate_epoch_data(self, t): + """Generates the data tuple for every pixel position in the frame""" # Use center point to determines the box in the complex plane # we need to calculatee @@ -203,39 +340,194 @@ def next_epoch(self, t, snapshot_filename = None): (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center.real, self.cmplx_center.imag), end = " ") - # Used to create a histogram of the frequency of iteration - # deppths retured by the mandelbrot calculation. Helpful for - # color selection since many iterations never come up so you - # loose fidelity by not focusing on those heavily used - hist = defaultdict(int) - values = {} - - if snapshot_filename: - print("Generating image [", end="") - + values = np.zeros((self.img_width, self.img_height), dtype=np.uint8) for x in range(0, self.img_width): for y in range(0, self.img_height): # map from pixels to complex coordinates - Re_x = self.ctxf(re_start) + (self.ctxf(x) / self.ctxf(self.img_width)) * \ + Re_x = self.ctxf(re_start) + (self.ctxf(x) / self.ctxf(self.img_width - 1)) * \ self.ctxf(re_end - re_start) - Im_y = self.ctxf(im_start) + (self.ctxf(y) / self.ctxf(self.img_height)) * \ + Im_y = self.ctxf(im_start) + (self.ctxf(y) / self.ctxf(self.img_height - 1)) * \ self.ctxf(im_end - im_start) c = self.ctxc(Re_x, Im_y) m = self.mandelbrot(c) - values[(x,y)] = m - if m < self.max_iter: - hist[math.floor(m)] += 1 + values[x,y] = m + return values + + def calculate_epoch_data_flint(self): + # Use center point to determines the box in the complex plane + # we need to calculatee + re_start = self.cmplx_center_flint.real - (self.cmplx_width_flint / 2.0) + re_end = self.cmplx_center_flint.real + (self.cmplx_width_flint / 2.0) + + im_start = self.cmplx_center_flint.imag - (self.cmplx_height_flint / 2.0) + im_end = self.cmplx_center_flint.imag + (self.cmplx_height_flint / 2.0) - if snapshot_filename: - print(".",end="") + if self.verbose > 0: + print("MandlContext starting epoch %d re range %s %s im range %s %s center %s + %s i .... " %\ + (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center_flint.real, self.cmplx_center_flint.imag), + end = " ") + + # Create 1D arrays of the real and complex ranges for each pixel of the current image + real_1d = self.linspace(re_start, re_end, self.img_width) + imag_1d = self.linspace(im_start, im_end, self.img_height) + + # Create two parallel 2D arrays, for the row+column combinations of real and imaginary values, + pixel_real_2d, pixel_imag_2d = np.meshgrid(imag_1d, real_1d) + + # Combine two parallel 2D arrays into a 3D array + # e.g. + # arr1 = [A, B, C, D] + # arr2 = [1, 2, 3] + # pixel_inputs_3 = [[[A,1],[A,2],[A,3]], [[B,1],[B,2],[B,3]], [[C,1],[C,2],[C,3]], [[D,1],[D,2],[D,3]]] + # + # Worth noting that vstack seems to need backwards params to get the ordering right + pixel_inputs_3d = np.vstack(([pixel_imag_2d.T], [pixel_real_2d.T])).T + #print("shape of pixel_inputs_3d: %s" % str(pixel_inputs_3d.shape)) + + pixel_values_2d = np.zeros((self.img_width, self.img_height), dtype=np.uint8) + + show_row_progress = False + if show_row_progress == True: + for x in range(0, self.img_width): + for y in range(0, self.img_height): + pixel_values_2d[x,y] = self.mandelbrot_flint(flint.acb(pixel_inputs_3d[x,y,0], pixel_inputs_3d[x,y,1])) + print("%d-" % x, end="") sys.stdout.flush() + else: + # Probalby not necessary, but lining up the 2-element subarray + pixel_inputs_1d = pixel_inputs_3d.reshape(pixel_inputs_3d.shape[0] * pixel_inputs_3d.shape[1], 2) + #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) + pixel_values_1d = np.array([self.mandelbrot_flint(flint.acb(complexAsArray[0], complexAsArray[1])) for complexAsArray in pixel_inputs_1d]) + + pixel_values_2d = pixel_values_1d.reshape(self.img_width, self.img_height, 1) + pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level + #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) - if snapshot_filename: - print("]") + # + # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here + # somewhere... + # + + # In search of efficient ways to apply the map, and getting stuck with various issues + # like pickling, which are keeping me from using multiprocessing.Pool + + #nope + #theFunction = np.vectorize(self.mandelbrot_flint) + #pixel_values_1d = theFunction(pixel_inputs_1d) + + #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) + # hmm, I see the problem# pixel_values_3d = self.mandelbrot_flint(pixel_inputs_3d) + #pixel_values_1d = self.mandelbrot_decimal(pixel_inputs_1d) + + #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) + #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) + #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) + + # Can't pikcle... hmm + #mandelpool = multiprocessing.Pool(processes = 1) + #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) + #mandelpool.close() + #mandelpool.join() + + return pixel_values_2d + + + def calculate_epoch_data_decimal(self,t): + oldPrecision = getcontext().prec + getcontext().prec = DECIMAL_HIGH_PRECISION_SIZE + + # Use center point to determines the box in the complex plane + # we need to calculatee + re_start = self.cmplx_center_real_decimal - (self.cmplx_width_decimal / Decimal('2.0')) + re_end = self.cmplx_center_real_decimal + (self.cmplx_width_decimal / Decimal('2.0')) + + im_start = self.cmplx_center_imag_decimal - (self.cmplx_height_decimal / Decimal('2.0')) + im_end = self.cmplx_center_imag_decimal + (self.cmplx_height_decimal / Decimal('2.0')) + + if self.verbose > 0: + print("MandlContext starting epoch %d re range %s %s im range %s %s center %s + %s i .... " %\ + (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center_real_decimal, self.cmplx_center_imag_decimal), + end = " ") + # Create 1D arrays of the real and complex ranges for each pixel of the current image + real_1d = self.linspace_decimal(re_start, re_end, self.img_width) + imag_1d = self.linspace_decimal(im_start, im_end, self.img_height) + + # Create two parallel 2D arrays, for the row+column combinations of real and imaginary values, + pixel_real_2d, pixel_imag_2d = np.meshgrid(imag_1d, real_1d) + + # Combine two parallel 2D arrays into a 3D array + # Worth noting that vstack seems to need backwards params to get the ordering right + # arr1 = [A, B, C, D] + # arr2 = [1, 2, 3] + # output_arr = [[[A,1],[A,2],[A,3]], [[B,1],[B,2],[B,3]], [[C,1],[C,2],[C,3]], [[D,1],[D,2],[D,3]]] + pixel_inputs_3d = np.vstack(([pixel_imag_2d.T], [pixel_real_2d.T])).T + #print("shape of pixel_inputs_3d: %s" % str(pixel_inputs_3d.shape)) + + pixel_values_2d = np.zeros((self.img_width, self.img_height), dtype=np.uint8) + + show_row_progress = False + if show_row_progress == True: + for x in range(0, self.img_width): + for y in range(0, self.img_height): + pixel_values_2d[x,y] = self.mandelbrot_decimal(pixel_inputs_3d[x,y]) + print("%d." % x, end="") + sys.stdout.flush() + else: + # Probalby not necessary, but lining up the 2-element subarray + pixel_inputs_1d = pixel_inputs_3d.reshape(pixel_inputs_3d.shape[0] * pixel_inputs_3d.shape[1], 2) + #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) + pixel_values_1d = np.array([self.mandelbrot_decimal(np.array(complexAsArray)) for complexAsArray in pixel_inputs_1d]) + + pixel_values_2d = pixel_values_1d.reshape(self.img_width, self.img_height, 1) + pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level + #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) + + getcontext().prec = oldPrecision + + return pixel_values_2d + + def next_epoch(self, t, snapshot_filename = None): + """Called for each frame of the animation. Will calculate + current view, and then zoom in""" + + ## Use center point to determines the box in the complex plane + ## we need to calculatee + #re_start = self.ctxf(self.cmplx_center.real - (self.cmplx_width / 2.)) + #re_end = self.ctxf(self.cmplx_center.real + (self.cmplx_width / 2.)) + # + #im_start = self.ctxf(self.cmplx_center.imag - (self.cmplx_height / 2.)) + #im_end = self.ctxf(self.cmplx_center.imag + (self.cmplx_height / 2.)) + # + #if self.verbose > 0: + # print("MandlContext starting epoch %d re range %f %f im range %f %f center %f + %f i .... " %\ + # (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center.real, self.cmplx_center.imag), + # end = " ") + + # Used to create a histogram of the frequency of iteration + # deppths retured by the mandelbrot calculation. Helpful for + # color selection since many iterations never come up so you + # loose fidelity by not focusing on those heavily used + hist = defaultdict(int) + values = np.zeros((self.img_width, self.img_height), dtype=np.uint8) + # Really need to build a reference function that's the frame number oracle... + # Until then, here's a quick and dirty way + quick_frame_number = math.floor(view_ctx.fps * t) + pixel_values_2d = self.load_frame(quick_frame_number, build_cache=self.build_cache, cache_path=self.cache_path, invalidate_cache=self.invalidate_cache) + + for x in range(0, self.img_width): + for y in range(0, self.img_height): + if pixel_values_2d[x,y] < self.max_iter: + hist[math.floor(pixel_values_2d[x,y])] += 1 + + #oldImage = Image.fromarray(values.astype('uint8')) + #oldImage.save("calc_plain_%d.gif" % self.num_epochs) + #newImage = Image.fromarray(pixel_values_2d.astype('uint8')) + #newImage.save("calc_precise_%d.gif" % self.num_epochs) + total = sum(hist.values()) hues = [] h = 0 @@ -252,7 +544,7 @@ def next_epoch(self, t, snapshot_filename = None): for x in range(0, self.img_width): for y in range(0, self.img_height): - m = values[(x,y)] + m = pixel_values_2d[x,y] # The color depends on the number of iterations #hue = 255 - int(255 * linear_interpolation(hues[floor(m)], hues[ceil(m)], m % 1)) @@ -271,13 +563,24 @@ def next_epoch(self, t, snapshot_filename = None): #print("Finished iteration IMrange %f:%f (im height: %f)"%(IM_START, IM_END, IM_END - IM_START)) if self.burn_in == True: - burn_in_text = u"%d re range %f %f im range %f %f center %f + %f i" %\ - (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center.real, self.cmplx_center.imag) + center_string = str(self.cmplx_center) + range_string = "(%f,%f)" % (self.cmplx_width, self.cmplx_height) + + if self.use_high_precision == True: + if self.high_precision_type == 'decimal': + center_string = "(%s, %si)" % (str(self.cmplx_center_real_decimal), str(self.cmplx_center_imag_decimal)) + range_string = "(%s,%s)" % (str(self.cmplx_width_decimal), str(self.cmplx_height_decimal)) + else: # flint + center_string = str(self.cmplx_center_flint) + range_string = "(%s,%s)" % (str(self.cmplx_width_flint), str(self.cmplx_height_flint)) + + burn_in_text = u"%d center: %s\n size: %s" %\ + (quick_frame_number, center_string, range_string) burn_in_location = (10,10) burn_in_margin = 5 burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) - burn_in_size = burn_in_font.getsize(burn_in_text) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) @@ -383,6 +686,16 @@ def __repr__(self): # For now, use global context for a single dive per run +flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) +mp.mp.prec = MPMATH_HIGH_PRECISION_SIZE # Sets mpmath's precision (in bits) + +# The extra parameter to instantiation is the way to run with mpmath +#mandl_ctx = MandlContext(ctxc=mp.mpc) + +# mpmath isn't as complete an alternate implementation as the others, because params +# can't change instantiation types while running set_default_params with the +# current setup, and second because I didn't check all instantiations to +# be correctly typed. mandl_ctx = MandlContext() view_ctx = MediaView(16, 16, mandl_ctx) @@ -397,18 +710,30 @@ def set_default_params(): mandl_ctx.img_width = 1024 mandl_ctx.img_height = 768 - mandl_ctx.cmplx_width = mandl_ctx.ctxf(3.) - mandl_ctx.cmplx_height = mandl_ctx.ctxf(2.5) + cmplx_width_str = '3.0' + cmplx_height_str = '2.5' + mandl_ctx.cmplx_width = mandl_ctx.ctxf(float(cmplx_width_str)) + mandl_ctx.cmplx_height = mandl_ctx.ctxf(float(cmplx_height_str)) + mandl_ctx.cmplx_width_decimal = Decimal(cmplx_width_str) + mandl_ctx.cmplx_height_decimal = Decimal(cmplx_height_str) + mandl_ctx.cmplx_width_flint = flint.arb(cmplx_width_str) + mandl_ctx.cmplx_height_flint = flint.arb(cmplx_height_str) # This is close t Misiurewicz point M32,2 # mandl_ctx.cmplx_center = mandl_ctx.ctxc(-.77568377, .13646737) - mandl_ctx.cmplx_center = mandl_ctx.ctxc(-1.769383179195515018213,0.00423684791873677221) + center_real_str = '-1.769383179195515018213' + center_imag_str = '0.00423684791873677221' + mandl_ctx.cmplx_center = mandl_ctx.ctxc(float(center_real_str),float(center_imag_str)) + mandl_ctx.cmplx_center_real_decimal = Decimal(center_real_str) + mandl_ctx.cmplx_center_imag_decimal = Decimal(center_imag_str) + mandl_ctx.cmplx_center_flint = flint.acb(center_real_str, center_imag_str) mandl_ctx.scaling_factor = .97 mandl_ctx.num_epochs = 0 mandl_ctx.max_iter = 255 mandl_ctx.escape_rad = 20 + mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad mandl_ctx.precision = 100 @@ -475,6 +800,11 @@ def parse_options(): "palette-test", "color", "burn", + "flint", + "decimal", + "cache-path=", + "build-cache", + "invalidate-cache", "banner", "smooth"]) @@ -514,6 +844,18 @@ def parse_options(): mandl_ctx.palette = m elif opt in ['--burn']: mandl_ctx.burn_in = True + elif opt in ['--decimal']: + mandl_ctx.use_high_precision = True + mandl_ctx.high_precision_type = "decimal" + elif opt in ['--flint']: + mandl_ctx.use_high_precision = True + mandl_ctx.high_precision_type = "flint" + elif opt in ['--cache-path']: + mandl_ctx.cache_path = arg + elif opt in ['--build-cache']: + mandl_ctx.build_cache = True + elif opt in ['--invalidate-cache']: + mandl_ctx.invalidate_cache = True elif opt in ['--banner']: view_ctx.banner = True elif opt in ['--verbose']: From 8a89f78a757b9ce7a75f5357f6c3bff99b7dab68 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 10 Jul 2021 01:19:37 -0700 Subject: [PATCH 02/44] Adding exponential color to different command line options --- clint_runscript.sh | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 1a7517d..8fc3ba1 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,8 +1,9 @@ -python3 mandelbrot.py --gif=flint.gif --duration=.25 --color --verbose=3 --burn --flint --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=flint.gif --duration=.25 --color --verbose=3 --burn --flint --build-cache --invalidate-cache --cache-path='cache' -#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color --verbose=3 --burn --decimal --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color --verbose=3 --burn --decimal --build-cache --invalidate-cache --cache-path='cache' +python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 --burn --flint --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 --burn --flint --build-cache --invalidate-cache --cache-path='cache' -#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color --verbose=3 --burn --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' +#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --invalidate-cache --cache-path='cache' + +#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' From be85e0a3e7e60e908ec53dbf95466a8ec3a6ca19 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 10 Jul 2021 01:27:21 -0700 Subject: [PATCH 03/44] native float example commands added --- clint_runscript.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clint_runscript.sh b/clint_runscript.sh index 8fc3ba1..ffb3387 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -7,3 +7,6 @@ python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 -- #python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' #python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' + +#python3 mandelbrot.py --gif=native.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' +#python3 mandelbrot.py --gif=native.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' From 86a6217907e6a17748742e0cc22ce5cf53f3a2d0 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 10 Jul 2021 01:32:17 -0700 Subject: [PATCH 04/44] warning about mpmath needed edit to code --- clint_runscript.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clint_runscript.sh b/clint_runscript.sh index ffb3387..a06d2b8 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -5,6 +5,9 @@ python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 -- #python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --cache-path='cache' #python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --invalidate-cache --cache-path='cache' +# +# NOTE: MUST change instantiation of MandlContext() at line ~810 +# #python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' #python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' From 3d6cacae5a76190f199da7e52eb076a8dd172e09 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Mon, 19 Jul 2021 21:04:30 -0700 Subject: [PATCH 05/44] Introduces DiveTimeline, and alternate way of calculating. Removes most arbitrary precision exploration, leaving just flint and native. --- clint_runscript.sh | 16 +- mandelbrot.py | 1744 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 1388 insertions(+), 372 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index a06d2b8..108a1b2 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,15 +1,7 @@ -python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 --burn --flint --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=flint.gif --duration=.25 --color=exp2 --verbose=3 --burn --flint --build-cache --invalidate-cache --cache-path='cache' +#python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo +python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache -#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=decimal.gif --duration=.25 --color=exp2 --verbose=3 --burn --decimal --build-cache --invalidate-cache --cache-path='cache' +#python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint +#python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache -# -# NOTE: MUST change instantiation of MandlContext() at line ~810 -# -#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=mpmath.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' - -#python3 mandelbrot.py --gif=native.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --cache-path='cache' -#python3 mandelbrot.py --gif=native.gif --duration=.25 --color=exp2 --verbose=3 --burn --build-cache --invalidate-cache --cache-path='cache' diff --git a/mandelbrot.py b/mandelbrot.py index d8e9c90..34ad6c0 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -25,14 +25,13 @@ import math import os -from decimal import * # Surely more specific on this will be better, sorry +import pickle + import multiprocessing # Can't actually make this work yet - gonna need pickling? from collections import defaultdict import numpy as np -import mpmath as mp -import flint import moviepy.editor as mpy from scipy.stats import norm @@ -43,10 +42,10 @@ MANDL_VER = "0.1" -DECIMAL_HIGH_PRECISION_SIZE = 16 # 16 places is roughly equivalent to 53 bits for float64, right? -#FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 -FLINT_HIGH_PRECISION_SIZE = 200 -MPMATH_HIGH_PRECISION_SIZE = 53 # 53 bits in a float64 +#FLINT_HIGH_PRECISION_SIZE = 16 # 53 is how many bits are in float64 +FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 +#FLINT_HIGH_PRECISION_SIZE = 200 + def gaussian(x, mu, sig): return np.exp(-np.power(x - mu, 2.) / (2 * np.power(sig, 2.))) @@ -224,43 +223,308 @@ def display(self): clip.preview(fps=1) #fps 1 is really all that works -class MandlContext: +class DiveMathSupport: """ - The context for a single dive + Toolbox for math functions that need to be type-aware, so we + can support different back-end math libraries. + + This exists because globally swapping out types doesn't do enough + to keep numeric types all in line. Some calculations also need additional + steps to make the math behave right, such as if we were using + the Decimal library, and had to keep separate real and imaginary + components of a complex number ourselves. + + Trying to preserve types where possible, without forcing casting, because sometimes + all the math operations will already work for custom numeric types. """ - def __init__(self, ctxf = None, ctxc = None, mp = None): + def createComplex(self, realComponent, imagComponent): + """Compatible complex types will return values for .real() and .imag()""" + return complex(float(realComponent), float(imagComponent)) + + def createFloat(self, floatValue): + return float(floatValue) + + def floor(self, value): + return math.floor(value) + + def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): + """ + Shortcut calculate the starting point for the last frame's properties, + which we'll use for instantiation with specific widths. This is + because if any keyframes are added, the best we can do is measure an + effective zoom factor between any 2 frames. + + Somehow, this seems to be ok for flint too?! + """ + return startValue * (overallZoomFactor ** iterations) + + def createLinspace(self, paramFirst, paramLast, quantity): + """Attempt at a mostly type-agnostic linspace(), seems to work with flint types too""" + dataRange = paramLast - paramFirst + answers = np.zeros((quantity), dtype=object) + + for x in range(0, quantity): + answers[x] = paramFirst + dataRange * (x / (quantity - 1)) - if not ctxf: - self.ctxf = float + return answers + + def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): + """ + Attempt at a mostly type-agnostic linspace-from-center-and-width + + Turns out, we didn't *really* need this for using flint, though it + was an important for a Python/Decimal version to be able to have a + type-specific implementation in what was a DiveMeshDecimal function + """ + return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): + """ + Probably want additional log-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + # y1 = a * ln(b * x1) + # y2 = a * ln(b * x2) + # a = (y1-y2) / ln(x1/x2) + # b = exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) + # + # y = a * ln(b * x) + # answer = a * ln(b * targetX) + # answer = ((y1-y2)/ln(x1/x2)) * ln(exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) * targetX) + # This kinda worked, but keeps the ln function so you just see part of + # some curve through the query points. Really, want to make sure the asymptote + # is at or near the start (or end). + # aVal = ((startY-endY) / math.log(startX / endX)) + # bVal = math.exp(startY / aVal) / startX + # return aVal * math.log(bVal * targetX) + + # This version seems like it worked fine, but ended up with a pretty wrong + # transition + # + # But, if we set the crossover point to the first point (add 1 to xVal), then all we + # need to do is solve for a, right? Also, using the 0-crossover as the + # first point means the asymptote can't be hit, because it's 1 less than + # the start. + # + # endY = a * ln(endX + 1 - startX) + startY + # a = (endY - startY) / ln(endX - startX + 1) + + aVal = (endY - startY) / math.log(endX - startX + 1) + return aVal * math.log(targetX - startX + 1) + startY + + def interpolateRootTo(self, startX, startY, endX, endY, targetX): + """ + Iterative multiplications of window sizes for zooming means we want to be able to + interpolate between two points using the root if the frame count between them as + the scale factor + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + root = endX - startX + scaleFactor = (endY / startY) ** (1 / root) + return startY * (scaleFactor ** targetX) + + def interpolateQuadraticEaseIn(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseIn puts the majority of changes in the start of the X range. + + I *think* this is just the backwards solution to QuadraticEaseOut? + So, just swap in and out points? + + Probably want additional quadratic-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY else: - self.ctxf = ctxf - if not ctxc: - self.ctxc = complex + return self.interpolateQuadraticEaseOut(endX, endY, startX, startY, targetX) + + def interpolateQuadraticEaseOut(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseOut leaves the majority of changes to the end of the X range. + + Probably want additional quadratic params, but for now, let's just bake in one equation + which uses the first point as the vertex, and passes through the second point. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + # Find a, given that the start point is the vertex, and the parabola passes + # through the other point + # y = a * (x - h)**2 + k + # y = a * (x - startX)**2 + startY + # endY = a * (endX - startX)**2 + startY + # a = ((endY-startY)/((endX-startX)**2) + # + # answer = a * (targetX - startX)**2 + startY + # answer = ((endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY + return (endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY + + def interpolateQuadraticEaseInOut(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseInOut finds a (linear) midpoint of the range, then leaves the + majority of the change to the middle of the range, easing both out of + the start, and into the end along separate parabolas. + + CAUTION: This forces the 'middle' X value to floor(). The idea is that X + will be frame numbers, so it should gracefully handle the inflection + around an integer-valued frame, instead of doing a (maybe more + complicated?) rounding inference. + The problem is that this means it isn't a universal solver. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + midpointFrame = self.floor(((endX-startX) * .5)) + startX + midpointY = self.interpolateLinear(startX, startY, endX, endY, midpointFrame) + if targetX <= midpointFrame: + return self.interpolateQuadraticEaseOut(startX, startY, midpointFrame, midpointY, targetX) + else: + return self.interpolateQuadraticEaseIn(midpointFrame, midpointY, endX, endY, targetX) + + def interpolateLinear(self, startX, startY, endX, endY, targetX): + # x1=2, y1=1, x2=12, y2=2, targetX = 7 + # valuesRange = endX - startX = 10 + # targetPercent = (targetX - startX) / valuesRange = .5 + # valuesDomain = endY - startY = 1 + # answer = targetPercent * valuesDomain + startY = 1.5 + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + return (((targetX - startX)/ (endX - startX)) * (endY - startY)) + startY + + def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): + z = self.createComplex(0, 0) + n = 0 + + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + while ((z.real*z.real)+(z.imag*z.imag)) <= escapeSquared and n < maxIter: + z = z*z + c + n += 1 +# + if n == maxIter: + return maxIter + + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + if shouldSmooth == True: + z = z*z + c; n+=1 # a couple extra iterations helps + z = z*z + c; n+=1 # decrease the size of the error + mu = n + 1 - math.log(math.log(abs(z))) + return mu else: - self.ctxc = ctxc - if not mp: - self.mp = math + return n + +class DiveMathSupportFlint(DiveMathSupport): + """ + Overrides to instantiate flint-specific complex types + + Looks like flint types are safe to use in base's createLinspace() + """ + def __init__(self): + super().__init__() + + self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. + self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) + + def createComplex(self, realComponent, imagComponent): + return self.flint.acb(realComponent, imagComponent) + + def createFloat(self, floatValue): + return self.flint.arb(floatValue) + + def floor(self, value): + return self.flint.arb(value).floor() + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): + """ + Probably want additional log-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + aVal = (endY - startY) / (self.flint.arb(endX - startX + 1).log()) + return aVal * (self.flint.arb(targetX - startX + 1).log()) + startY + + def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): + """ + NOTE: Smoothing maybe should be only a post-processing step? Maybe not? + + This flint-specific implementation only really does flint-y logs in the smoothing. + The Decimal-specific implementation needed some extra steps, but I've ditched that one. + """ + z = self.createComplex(0, 0) + n = 0 + + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + # + # Important to cast the escape answer back to float, or the arb gets sheared bizarrely + while (float((z.real*z.real)+(z.imag*z.imag))) <= escapeSquared and n < maxIter: + z = z*z + c + n += 1 + + if n == maxIter: + return maxIter + + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + if shouldSmooth == True: + z = z*z + c; n+=1 # a couple extra iterations helps + z = z*z + c; n+=1 # decrease the size of the error + + #mu = n + 1 - math.log(self.mp.log2(abs(z))) # (and, there IS NO log2 in mpmath... hrm) + # Maybe this is the right way to do the same thing?! + mu = n + 1 - z.abs_lower().log().log() + return mu + else: + return n + + +class MandlContext: + """ + The context for a single dive + """ + + def __init__(self, math_support=DiveMathSupport()): + self.math_support = math_support self.img_width = 0 # int : Wide of Image in pixels self.img_height = 0 # int - self.cmplx_width = 0.0 # width of visualization in complex plane - self.cmplx_height = 0.0 - self.cmplx_width_decimal = Decimal('0.0') - self.cmplx_height_decimal = Decimal('0.0') - self.cmplx_width_flint = flint.arb('0.0') - self.cmplx_height_flint = flint.arb('0.0') - + self.cmplx_width = self.math_support.createFloat(0.0) + self.cmplx_height = self.math_support.createFloat(0.0) + # point we're going to dive into - self.cmplx_center = self.ctxc(0.0) # center of image in complex plane - self.cmplx_center_real_decimal = Decimal('0.0') - self.cmplx_center_imag_decimal = Decimal('0.0') - self.cmplx_center_flint = flint.acb('0.0') + self.cmplx_center = self.math_support.createComplex(0.0, 0.0) # center of image in complex plane self.max_iter = 0 # int max iterations before bailing self.escape_rad = 0. # float radius mod Z hits before it "escapes" - self.escape_squared = 0.0 # float squared radius, memoized + self.escape_squared = 0.0 # float squared radius, (redundant, but helpful) self.scaling_factor = 0.0 # float amount to zoom each epoch self.num_epochs = 0 # int, nuber of epochs into the dive @@ -270,48 +534,33 @@ def __init__(self, ctxf = None, ctxc = None, mp = None): self.smoothing = False # bool turn on color smoothing self.snapshot = False # Generate a single, high res shotb - self.precision = 17 # int decimal precision for calculations - self.duration = 0 # int duration of clip in seconds self.fps = 0 # int number of frames per second self.palette = None self.burn_in = False - self.use_high_precision = False - self.high_precision_type = 'flint' - self.cache_path = "cache" + # Shifting from the MandlContext being the oracle of frame information, to the Timeline being the oracle. + # Rather than keeping 'current frame' info in the context, we just keep the timeline, and + # query it for frame-specific parameters to render with. + self.timeline = None + + self.project_name = "default_project" + self.cache_path = u"%s_cache" % self.project_name self.build_cache = False self.invalidate_cache = False self.verbose = 0 # how much to print about progress def zoom_in(self, iterations=1): - if self.use_high_precision == True: - if self.high_precision_type == 'decimal': - oldPrecision = getcontext().prec - getcontext().prec = DECIMAL_HIGH_PRECISION_SIZE - while iterations: - self.cmplx_width_decimal *= Decimal(self.scaling_factor) - self.cmplx_height_decimal *= Decimal(self.scaling_factor) - self.num_epochs += 1 - iterations -= 1 - getcontext().prec = oldPrecision - else: # flint - while iterations: - self.cmplx_width_flint *= self.scaling_factor - self.cmplx_height_flint *= self.scaling_factor - self.num_epochs += 1 - iterations -= 1 - else: # Default native python, or maybe mpmath - while iterations: - self.cmplx_width *= self.scaling_factor - self.cmplx_height *= self.scaling_factor - self.num_epochs += 1 - iterations -= 1 + while iterations: + self.cmplx_width *= self.scaling_factor + self.cmplx_height *= self.scaling_factor + self.num_epochs += 1 + iterations -= 1 def mandelbrot(self, c): - z = self.ctxc(0) + z = self.math_support.createComplex(0, 0) n = 0 # fabs(z) returns the modulus of a complex number, which is the @@ -331,74 +580,11 @@ def mandelbrot(self, c): if self.smoothing: z = z*z + c; n+=1 # a couple extra iterations helps z = z*z + c; n+=1 # decrease the size of the error - mu = n + 1 - math.log(self.mp.log2(abs(z))) + mu = n + 1 - math.log(math.log(abs(z))) return mu else: return n - def mandelbrot_flint(self, c): - z = flint.acb(0.0) - n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - while ((z.real*z.real)+(z.imag*z.imag)) <= self.escape_squared and n < self.max_iter: - z = z*z + c - n += 1 - return n - - def mandelbrot_decimal(self, param_complex_array): - """Takes the 2 complex number components and returns the - number of iterations it took to become greater than the escape value. - - TODO: really need tests to make sure precision is being properly preserved""" - param_real = param_complex_array[0] - param_imag = param_complex_array[1] - - z_real = Decimal('0.0') - z_imag = Decimal('0.0') - n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - while((z_real*z_real) + (z_imag*z_imag)) <= self.escape_squared and n < self.max_iter: - prev_z_real = z_real - prev_z_imag = z_imag - - z_real = prev_z_real * prev_z_real - prev_z_imag * prev_z_imag + Decimal(param_real) - z_imag = prev_z_real * prev_z_imag * Decimal('2.0') + Decimal(param_imag) - n += 1 - - return n - - def linspace_decimal(self, paramFirst, paramLast, quantity): - first = Decimal(paramFirst) - last = Decimal(paramLast) - dataRange = last - first - answers = np.zeros((quantity, 1), dtype=object) - - for x in range(0, quantity): - answers[x] = first + (dataRange / (quantity - 1)) * x - - return answers - - def linspace(self, paramFirst, paramLast, quantity): - first = paramFirst - last = paramLast - dataRange = last - first - answers = np.zeros((quantity, 1), dtype=object) - - for x in range(0, quantity): - answers[x] = first + (dataRange / (quantity - 1)) * x - - return answers - def load_frame(self, frame_number, cache_path, build_cache = True, invalidate_cache = False): """Cache-aware data tuple loading or calculating""" @@ -410,17 +596,7 @@ def load_frame(self, frame_number, cache_path, build_cache = True, invalidate_ca frame_data = np.zeros((1,1), dtype=np.uint8) if os.path.exists(cache_file_name) == False: - #print("Calculating epoch data") - if self.use_high_precision == True: - if self.high_precision_type == 'decimal': - #print(" (decimal high precision)") - frame_data = self.calculate_epoch_data_decimal(frame_number) - else: - #print(" (flint high precision)") - frame_data = self.calculate_epoch_data_flint() - else: - #print(" (normal precision as-compiled, perhaps mpmath?)") - frame_data = self.calculate_epoch_data(frame_number) + frame_data = self.calculate_epoch_data(frame_number) if build_cache == True: #print("Writing cache file") @@ -439,11 +615,11 @@ def calculate_epoch_data(self, t): # Use center point to determines the box in the complex plane # we need to calculatee - re_start = self.ctxf(self.cmplx_center.real - (self.cmplx_width / 2.)) - re_end = self.ctxf(self.cmplx_center.real + (self.cmplx_width / 2.)) + re_start = self.math_support.createFloat(self.cmplx_center.real - (self.cmplx_width / 2.)) + re_end = self.math_support.createFloat(self.cmplx_center.real + (self.cmplx_width / 2.)) - im_start = self.ctxf(self.cmplx_center.imag - (self.cmplx_height / 2.)) - im_end = self.ctxf(self.cmplx_center.imag + (self.cmplx_height / 2.)) + im_start = self.math_support.createFloat(self.cmplx_center.imag - (self.cmplx_height / 2.)) + im_end = self.math_support.createFloat(self.cmplx_center.imag + (self.cmplx_height / 2.)) if self.verbose > 0: print("MandlContext starting epoch %d re range %f %f im range %f %f center %f + %f i .... " %\ @@ -454,168 +630,98 @@ def calculate_epoch_data(self, t): for x in range(0, self.img_width): for y in range(0, self.img_height): # map from pixels to complex coordinates - Re_x = self.ctxf(re_start) + (self.ctxf(x) / self.ctxf(self.img_width - 1)) * \ - self.ctxf(re_end - re_start) - Im_y = self.ctxf(im_start) + (self.ctxf(y) / self.ctxf(self.img_height - 1)) * \ - self.ctxf(im_end - im_start) - - c = self.ctxc(Re_x, Im_y) + Re_x = re_start + (x / (self.img_width - 1)) * (re_end - re_start) + Im_y = im_start + (y / (self.img_height - 1)) * (im_end - im_start) + c = self.math_support.createComplex(Re_x, Im_y) m = self.mandelbrot(c) values[x,y] = m return values - def calculate_epoch_data_flint(self): - # Use center point to determines the box in the complex plane - # we need to calculatee - re_start = self.cmplx_center_flint.real - (self.cmplx_width_flint / 2.0) - re_end = self.cmplx_center_flint.real + (self.cmplx_width_flint / 2.0) - - im_start = self.cmplx_center_flint.imag - (self.cmplx_height_flint / 2.0) - im_end = self.cmplx_center_flint.imag + (self.cmplx_height_flint / 2.0) - - if self.verbose > 0: - print("MandlContext starting epoch %d re range %s %s im range %s %s center %s + %s i .... " %\ - (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center_flint.real, self.cmplx_center_flint.imag), - end = " ") + def render_frame_number(self, frame_number, snapshot_filename=None): + """ + Load or calculate the frame data. - # Create 1D arrays of the real and complex ranges for each pixel of the current image - real_1d = self.linspace(re_start, re_end, self.img_width) - imag_1d = self.linspace(im_start, im_end, self.img_height) + Once we have frame data, can perform histogram and coloring + """ + (pixel_values_2d, frame_metadata) = self.timeline.loadResultsForFrameNumber(frame_number, build_cache=self.build_cache, cache_path=self.cache_path, invalidate_cache=self.invalidate_cache) - # Create two parallel 2D arrays, for the row+column combinations of real and imaginary values, - pixel_real_2d, pixel_imag_2d = np.meshgrid(imag_1d, real_1d) - - # Combine two parallel 2D arrays into a 3D array - # e.g. - # arr1 = [A, B, C, D] - # arr2 = [1, 2, 3] - # pixel_inputs_3 = [[[A,1],[A,2],[A,3]], [[B,1],[B,2],[B,3]], [[C,1],[C,2],[C,3]], [[D,1],[D,2],[D,3]]] - # - # Worth noting that vstack seems to need backwards params to get the ordering right - pixel_inputs_3d = np.vstack(([pixel_imag_2d.T], [pixel_real_2d.T])).T - #print("shape of pixel_inputs_3d: %s" % str(pixel_inputs_3d.shape)) + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + pixel_values_2d = pixel_values_2d.T - pixel_values_2d = np.zeros((self.img_width, self.img_height), dtype=np.uint8) - - show_row_progress = False - if show_row_progress == True: - for x in range(0, self.img_width): - for y in range(0, self.img_height): - pixel_values_2d[x,y] = self.mandelbrot_flint(flint.acb(pixel_inputs_3d[x,y,0], pixel_inputs_3d[x,y,1])) - print("%d-" % x, end="") - sys.stdout.flush() - else: - # Probalby not necessary, but lining up the 2-element subarray - pixel_inputs_1d = pixel_inputs_3d.reshape(pixel_inputs_3d.shape[0] * pixel_inputs_3d.shape[1], 2) - #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) - pixel_values_1d = np.array([self.mandelbrot_flint(flint.acb(complexAsArray[0], complexAsArray[1])) for complexAsArray in pixel_inputs_1d]) - - pixel_values_2d = pixel_values_1d.reshape(self.img_width, self.img_height, 1) - pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level - #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) - - # - # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here - # somewhere... - # - - # In search of efficient ways to apply the map, and getting stuck with various issues - # like pickling, which are keeping me from using multiprocessing.Pool - - #nope - #theFunction = np.vectorize(self.mandelbrot_flint) - #pixel_values_1d = theFunction(pixel_inputs_1d) - - #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) - # hmm, I see the problem# pixel_values_3d = self.mandelbrot_flint(pixel_inputs_3d) - #pixel_values_1d = self.mandelbrot_decimal(pixel_inputs_1d) + # Used to create a histogram of the frequency of iteration + # deppths retured by the mandelbrot calculation. Helpful for + # color selection since many iterations never come up so you + # loose fidelity by not focusing on those heavily used + hist = defaultdict(int) + values = np.zeros((self.img_width, self.img_height), dtype=np.uint8) - #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) - #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) - #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) + #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - # Can't pikcle... hmm - #mandelpool = multiprocessing.Pool(processes = 1) - #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) - #mandelpool.close() - #mandelpool.join() + for x in range(0, self.img_width): + for y in range(0, self.img_height): + if pixel_values_2d[x,y] < self.max_iter: + hist[math.floor(pixel_values_2d[x,y])] += 1 - return pixel_values_2d + total = sum(hist.values()) + hues = [] + h = 0 + # calculate percent of total for each iteration + for i in range(self.max_iter): + if total : + h += hist[i] / total + hues.append(h) + hues.append(h) - def calculate_epoch_data_decimal(self,t): - oldPrecision = getcontext().prec - getcontext().prec = DECIMAL_HIGH_PRECISION_SIZE + im = Image.new('RGB', (self.img_width, self.img_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, self.img_width): + for y in range(0, self.img_height): + m = pixel_values_2d[x,y] - # Use center point to determines the box in the complex plane - # we need to calculatee - re_start = self.cmplx_center_real_decimal - (self.cmplx_width_decimal / Decimal('2.0')) - re_end = self.cmplx_center_real_decimal + (self.cmplx_width_decimal / Decimal('2.0')) + if not self.palette: + c = 255 - int(255 * hues[math.floor(m)]) + color=(c, c, c) + elif self.smoothing: + c1 = self.palette[1024 - int(1024 * hues[math.floor(m)])] + c2 = self.palette[1024 - int(1024 * hues[math.ceil(m)])] + color = MandlPalette.linear_interpolate(c1,c2,.5) + else: + color = self.palette[1024 - int(1024 * hues[math.floor(m)])] - im_start = self.cmplx_center_imag_decimal - (self.cmplx_height_decimal / Decimal('2.0')) - im_end = self.cmplx_center_imag_decimal + (self.cmplx_height_decimal / Decimal('2.0')) + # Plot the point + draw.point([x, y], color) - if self.verbose > 0: - print("MandlContext starting epoch %d re range %s %s im range %s %s center %s + %s i .... " %\ - (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center_real_decimal, self.cmplx_center_imag_decimal), - end = " ") - # Create 1D arrays of the real and complex ranges for each pixel of the current image - real_1d = self.linspace_decimal(re_start, re_end, self.img_width) - imag_1d = self.linspace_decimal(im_start, im_end, self.img_height) - # Create two parallel 2D arrays, for the row+column combinations of real and imaginary values, - pixel_real_2d, pixel_imag_2d = np.meshgrid(imag_1d, real_1d) + #print("Finished iteration RErange %f:%f (re width: %f)"%(RE_START, RE_END, RE_END - RE_START)) + #print("Finished iteration IMrange %f:%f (im height: %f)"%(IM_START, IM_END, IM_END - IM_START)) - # Combine two parallel 2D arrays into a 3D array - # Worth noting that vstack seems to need backwards params to get the ordering right - # arr1 = [A, B, C, D] - # arr2 = [1, 2, 3] - # output_arr = [[[A,1],[A,2],[A,3]], [[B,1],[B,2],[B,3]], [[C,1],[C,2],[C,3]], [[D,1],[D,2],[D,3]]] - pixel_inputs_3d = np.vstack(([pixel_imag_2d.T], [pixel_real_2d.T])).T - #print("shape of pixel_inputs_3d: %s" % str(pixel_inputs_3d.shape)) + if self.burn_in == True: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (frame_number, frame_metadata['mesh_center'], frame_metadata['real_width'], frame_metadata['imag_width']) - pixel_values_2d = np.zeros((self.img_width, self.img_height), dtype=np.uint8) + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) - show_row_progress = False - if show_row_progress == True: - for x in range(0, self.img_width): - for y in range(0, self.img_height): - pixel_values_2d[x,y] = self.mandelbrot_decimal(pixel_inputs_3d[x,y]) - print("%d." % x, end="") - sys.stdout.flush() - else: - # Probalby not necessary, but lining up the 2-element subarray - pixel_inputs_1d = pixel_inputs_3d.reshape(pixel_inputs_3d.shape[0] * pixel_inputs_3d.shape[1], 2) - #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) - pixel_values_1d = np.array([self.mandelbrot_decimal(np.array(complexAsArray)) for complexAsArray in pixel_inputs_1d]) - - pixel_values_2d = pixel_values_1d.reshape(self.img_width, self.img_height, 1) - pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level - #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) + #if self.verbose > 0: + # print("Done]") + + if snapshot_filename: + return im.save(snapshot_filename,"gif") + else: + return np.array(im) - getcontext().prec = oldPrecision - - return pixel_values_2d - def next_epoch(self, t, snapshot_filename = None): """Called for each frame of the animation. Will calculate current view, and then zoom in""" - ## Use center point to determines the box in the complex plane - ## we need to calculatee - #re_start = self.ctxf(self.cmplx_center.real - (self.cmplx_width / 2.)) - #re_end = self.ctxf(self.cmplx_center.real + (self.cmplx_width / 2.)) - # - #im_start = self.ctxf(self.cmplx_center.imag - (self.cmplx_height / 2.)) - #im_end = self.ctxf(self.cmplx_center.imag + (self.cmplx_height / 2.)) - # - #if self.verbose > 0: - # print("MandlContext starting epoch %d re range %f %f im range %f %f center %f + %f i .... " %\ - # (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center.real, self.cmplx_center.imag), - # end = " ") - # Used to create a histogram of the frequency of iteration # deppths retured by the mandelbrot calculation. Helpful for # color selection since many iterations never come up so you @@ -633,11 +739,6 @@ def next_epoch(self, t, snapshot_filename = None): if pixel_values_2d[x,y] < self.max_iter: hist[math.floor(pixel_values_2d[x,y])] += 1 - #oldImage = Image.fromarray(values.astype('uint8')) - #oldImage.save("calc_plain_%d.gif" % self.num_epochs) - #newImage = Image.fromarray(pixel_values_2d.astype('uint8')) - #newImage.save("calc_precise_%d.gif" % self.num_epochs) - total = sum(hist.values()) hues = [] h = 0 @@ -677,13 +778,10 @@ def next_epoch(self, t, snapshot_filename = None): center_string = str(self.cmplx_center) range_string = "(%f,%f)" % (self.cmplx_width, self.cmplx_height) - if self.use_high_precision == True: - if self.high_precision_type == 'decimal': - center_string = "(%s, %si)" % (str(self.cmplx_center_real_decimal), str(self.cmplx_center_imag_decimal)) - range_string = "(%s,%s)" % (str(self.cmplx_width_decimal), str(self.cmplx_height_decimal)) - else: # flint - center_string = str(self.cmplx_center_flint) - range_string = "(%s,%s)" % (str(self.cmplx_width_flint), str(self.cmplx_height_flint)) +# if self.use_high_precision == True: +# if self.high_precision_type == 'flint': +# center_string = str(self.cmplx_center_flint) +# range_string = "(%s,%s)" % (str(self.cmplx_width_flint), str(self.cmplx_height_flint)) burn_in_text = u"%d center: %s\n size: %s" %\ (quick_frame_number, center_string, range_string) @@ -709,10 +807,10 @@ def next_epoch(self, t, snapshot_filename = None): def __repr__(self): return """\ -[MandlContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:.20f} -Cmplx H:{ch:.20f} Complx Center:{cc:s} Scaling:{s:f} Epochs:{e:d} Max iter:{mx:d}]\ +[MandlContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:s} +Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Epochs:{e:d} Max iter:{mx:d}]\ """.format( - w=self.img_width,h=self.img_height,cw=self.cmplx_width,ch=self.cmplx_height, + w=self.img_width,h=self.img_height,cw=str(self.cmplx_width),ch=str(self.cmplx_height), cc=str(self.cmplx_center),s=self.scaling_factor,e=self.num_epochs,mx=self.max_iter); class MediaView: @@ -721,7 +819,7 @@ class MediaView: """ def make_frame(self, t): - return self.ctx.next_epoch(t) + return self.ctx.render_frame_number(self.frame_number_from_time(t)) def __init__(self, duration, fps, ctx): self.duration = duration @@ -730,6 +828,8 @@ def __init__(self, duration, fps, ctx): self.banner = False self.vfilename = None + def frame_number_from_time(self, t): + return math.floor(self.fps * t) def intro_banner(self): # Generate a text clip @@ -758,19 +858,12 @@ def create_snapshot(self): def run(self): - # Check whether we need to zoom in prior to calculation - - if self.ctx.set_zoom_level > 0: - print("Zooming in by %d epochs" % (self.ctx.set_zoom_level)) - while self.ctx.set_zoom_level > 0: - self.ctx.zoom_in() - self.ctx.set_zoom_level -= 1 - - if self.ctx.snapshot == True: self.create_snapshot() return + self.ctx.timeline = self.construct_simple_timeline() + self.clip = mpy.VideoClip(self.make_frame, duration=self.duration) if self.banner: @@ -788,117 +881,1045 @@ def run(self): else: print("Error: file extension not supported, must be gif or mp4") sys.exit(0) + + def construct_simple_timeline(self): + """ + Transitional. + + Basically, let's construct a timeline from one set of start/end points, + as defined by the current context. + """ + frame_count = self.duration * self.fps + if math.floor(frame_count) != frame_count: + frame_count = math.floor(frame_count) + 1 + + overall_zoom_factor = self.ctx.scaling_factor + + start_width_real = self.ctx.cmplx_width + start_width_imag = self.ctx.cmplx_height + + # Check whether we need to zoom in prior to calculation + if self.ctx.set_zoom_level > 0: + print("Zooming in by %d epochs" % (self.ctx.set_zoom_level)) + start_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, self.ctx.set_zoom_level) + start_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, self.ctx.set_zoom_level) + + # Calculate the endpoint widths, at the last desired frame + end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(start_width_real, overall_zoom_factor, frame_count - 1) + end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(start_width_imag, overall_zoom_factor, frame_count - 1) + + timeline = DiveTimeline(projectFolderName=self.ctx.project_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support) + # Here, I have ctx, which should know escapeSquared, maxIter, and shouldSmooth... + #print("Trying to make span of %d frames" % frame_count) + span = timeline.addNewSpanAtEnd(frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_squared, self.ctx.max_iter, self.ctx.smoothing) + + #perspectiveFrame = math.floor(frame_count * .5) + #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) + + return timeline + + def __repr__(self): + return """\ +[MediaView duration {du:f} FPS:{f:s} Output:{vf:s}]\ +""".format(du=self.duration,f=str(self.fps),vf=str(self.vfilename)) + + +class DiveTimeline: + """ + Representation of an edit timeline. This maps parameters to specific frame numbers. + A timeline also is the keeper of framerate for a frame sequence. (I think?) + + For now, the timeline also maintains the calculation cache for every frame, though + this may eventually be the responsibility of a 'Project' + + Overview of the sequencing classes + ---------------------------------- + DiveTimeline + DiveTimelineSpan (Basis for setting keyframes) + DiveSpanKeyframe + - DiveSpanCenterKeyframe + - DiveSpanWindowKeyframe + - DiveSpanUniformKeyframe + - DiveSpanTiltKeyframe + DiveMesh + MeshGenerator + - MeshGeneratorUniform + - MeshGeneratorTilt + + There are currently only 3 'tracks' of keyframes (for complex + center, window base sizes, and perspective). Keyframes + currently all live on integer frame numbers. + """ + + def __init__(self, projectFolderName, framerate, frameWidth, frameHeight, mathSupport): + self.projectFolderName = projectFolderName + self.cachePath = os.path.join("%s_cache" % projectFolderName) + + self.framerate = float(framerate) + self.frameWidth = int(frameWidth) + self.frameHeight = int(frameHeight) + + self.mathSupport = mathSupport + + # No definition made yet for edit gaps, so let's just enforce adjacency of ranges for now. + self.timelineSpans = [] + + def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag, escapeSquared, maxEscapeIterations, shouldSmooth): + """ + Constructs a new span, and adds it to the end of the existing span list + + Also adds center keyframes (that is, keyframes at the end of the span, which + set the values for the complex center of the image), and window width keyframes + to the start and end of the new span. + + Apparently also adding perspective keyframes too. + """ + span = DiveTimelineSpan(self, frameCount, escapeSquared, maxEscapeIterations, shouldSmooth) + span.addNewCenterKeyframe(0, center, 'quadratic-to', 'quadratic-to') + span.addNewCenterKeyframe(frameCount - 1, center, 'quadratic-to', 'quadratic-to') + span.addNewWindowKeyframe(0, startWidthReal, startWidthImag) + span.addNewWindowKeyframe(frameCount - 1, endWidthReal, endWidthImag) + + # Setting uniform perspective this way kinda taped on as a solution, but not yet + # sure how to more gracefully set up perspective keyframes. + span.addNewUniformKeyframe(0) + span.addNewUniformKeyframe(frameCount-1) + + self.timelineSpans.append(span) + + return span + + def getSpanForFrameNumber(self, frameNumber): + """ + Seems like I should cache or memoize this, to keep from searching for every frame, + or at least binary search it, but I'm allergic to optimizing before profiling, + so 'slow' search it is for now. + + 10 frames -> {0,9} + 3 frames -> {10,12} + 10 frames -> {13,22} + """ + nextSpanFirstFrame = 0 + for currSpan in self.timelineSpans: + nextSpanFirstFrame += currSpan.frameCount + if frameNumber < nextSpanFirstFrame: + currSpan.lastObservedStartFrame = nextSpanFirstFrame - currSpan.frameCount + return currSpan + + return None # Went past the end without finding a valid span, so it's too high a frame number + + def loadResultsForFrameNumber(self, frame_number, build_cache=True, cache_path="cache", invalidate_cache=False): + """Cache-aware data tuple loading or calculating""" + + cache_file_name = os.path.join(cache_path, u"%d.npy" % frame_number) + cache_metadata_file_name = os.path.join(cache_path, u"%d.npy.meta" % frame_number) + #print("cache file %s" % cache_file_name) + + if invalidate_cache == True and os.path.exists(cache_file_name) == True and os.path.isfile(cache_file_name): + os.remove(cache_file_name) + os.remove(cache_metadata_file_name) + + frame_data = np.zeros((1,1), dtype=np.uint8) + frame_metadata = {} + if os.path.exists(cache_file_name) == False: + #print("Calculating epoch results") + (frame_data, frame_metadata) = self.calculateResultsForFrameNumber(frame_number) + + if build_cache == True: + #print("Writing cache file") + if not os.path.exists(cache_path): + os.makedirs(cache_path) + + # Write 2 separate files out, the numpy array, and a metadata sidecar + np.save(cache_file_name, frame_data) + with open(cache_metadata_file_name, 'wb') as metadataHandle: + pickle.dump(frame_metadata, metadataHandle) + else: + #print("Loading cache file") + # Load both the numpy array, and the metadata sidecar + frame_data = np.load(cache_file_name, allow_pickle=True) + with open(cache_metadata_file_name, 'rb') as metadataReadHandle: + frame_metadata = pickle.load(metadataReadHandle) + return (frame_data, frame_metadata) + + def calculateResultsForFrameNumber(self, frameNumber): + diveMesh = self.getMeshForFrame(frameNumber) + mesh = diveMesh.generateMesh() + + show_row_progress = False + + pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint8) + for x in range(0, mesh.shape[0]): + for y in range(0, mesh.shape[1]): + pixel_values_2d[x,y] = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeSquared, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) + if show_row_progress == True: + print("%d-" % x, end="") + sys.stdout.flush() + + frame_metadata = {'frame_number' : frameNumber, + 'mesh_center': str(diveMesh.center), + 'real_width' : str(diveMesh.realMeshGenerator.baseWidth), + 'imag_width' : str(diveMesh.imagMeshGenerator.baseWidth), + 'escape_squared' : str(diveMesh.escapeSquared), + 'max_escape_iterations' : str(diveMesh.maxEscapeIterations)} + + return (pixel_values_2d, frame_metadata) + + + #### + # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here + # somewhere... + #### + + # In search of efficient ways to apply the map, and getting stuck with various issues + # like pickling, which are keeping me from using multiprocessing.Pool + +# Seemed to go exponential run time for some bizarre reason +# # Probably not necessary, but lining up the 2-element subarray +# pixel_inputs_1d = pixel_values_2d.reshape((mesh.shape[0] * mesh.shape[1])) +# +# # Pretty sure this is mistakenly doing an n! pass, or something just as ridiculous. +# #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) +# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeSquared, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) +# +# pixel_values_2d = pixel_values_1d.reshape((mesh.shape[0], mesh.shape[1])) +# #pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level +# #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) + + #nope + #theFunction = np.vectorize(self.mandelbrot_flint) + #pixel_values_1d = theFunction(pixel_inputs_1d) + + #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) + + #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) + #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) + #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) + + # Can't pikcle... hmm + #mandelpool = multiprocessing.Pool(processes = 1) + #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) + #mandelpool.close() + #mandelpool.join() + + def getMeshForFrame(self, frameNumber): + """ + Calculate a discretized 2d plane of complex points based on spans and keyframes in this timeline. + + # Haven't revisited this logic since building the MeshGenerator objects... + Order of operations: + 1.) base_complex_center + 2.) distortions on base_complex_center (probably never really want this, but hey, it makes sense here) + 3.) MeshGenerator distortions? + 4.) overall distortions on the calculated 2D mesh + + Procedurally calculate the discretized 2D plane of complex points, based on all + the known keyframes and modifiers for a given frame number... + + # complexCenter, complexWidth, imaginaryWidth + + """ + # First step is to figure out which span the target frame belongs to + targetSpan = self.getSpanForFrameNumber(frameNumber) + if not targetSpan: + raise IndexError("Frame number '%d' is out of range for this timeline" % frameNumber) + + # Within this span, find the closest upstream and closest downstream keyframes + # Pretty sure we require least 2 keyframes defined for every span (start and end), so + # this should work out. + localFrameNumber = frameNumber - targetSpan.lastObservedStartFrame + (previousCenterKeyframe, nextCenterKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('center', localFrameNumber) + #print("centers %s -> %s" % (str(previousCenterKeyframe.center), str(nextCenterKeyframe.center))) + + meshCenterValue = targetSpan.interpolateCenterValueBetweenKeyframes(localFrameNumber, previousCenterKeyframe, nextCenterKeyframe) + #print(" interpolatedCenter: %s" % meshCenterValue) + + (previousWindowKeyframe, nextWindowKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('window', localFrameNumber) + #print("windows %s,%s -> %s,%s" % (str(previousWindowKeyframe.realWidth), str(previousWindowKeyframe.imagWidth), str(nextWindowKeyframe.realWidth), str(nextWindowKeyframe.imagWidth))) + + (baseWidthReal, baseWidthImag) = targetSpan.interpolateWindowValuesBetweenKeyframes(localFrameNumber, previousWindowKeyframe, nextWindowKeyframe) + + #print(" interpolatedWidths: %s, %s" % (str(baseWidthReal), str(baseWidthImag))) + + (previousPerspectiveKeyframe, nextPerspectiveKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('perspective', localFrameNumber) + + # More complicated with perspective keyframes, right? + # Which mesh generator do we use - Uniform or Tilt? + + # Might have to interpolate the (widthFactor,heightFactor) of a tilt keyframe... + # If both keyframes are uniform, then we don't actually interpolate + # Hacky isisntance, but whatcha gonna do? + previousIsUniform = isinstance(previousPerspectiveKeyframe, DiveSpanUniformKeyframe) + nextIsUniform = isinstance(nextPerspectiveKeyframe, DiveSpanUniformKeyframe) + if previousIsUniform and nextIsUniform: + # Might feel like "baseImagWidth" is a typo (because it's distributed vertically), but + # it's the 'imaginary width', even though we use it as the vertical element in the final mesh + realMeshGenerator = MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal) + imagMeshGenerator = MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag) + else: + (widthTiltFactor, heightTiltFactor) = self.interpolateTiltFactorsBetweenPerspectiveKeyframes(localFrameNumber, previousPerspectiveKeyframe, nextPerspectiveKeyframe) + + # Tilt factor is the multiplier applied to the range. + realMeshGenerator = MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal, tiltFactor=widthTiltFactor) + imagMeshGenerator = MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag, tiltFactor=heightTiltFactor) + + # Passing lots into the dive mesh. Notably, some info about the DiveTimelineSpan that was + # responsible for creating this mesh. Might want to store an actual reference to the object, but + # doesn't seem needed yet? + diveMesh = DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeSquared, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) + #print (diveMesh) + return diveMesh + + def interpolateTiltFactorsBetweenPerspectiveKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + """ + First part of this repeats some hacky isinstance stuff. + + But this time, we know they BOTH won't be uniform, else we never would try to + interpolate between them. + """ + if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: + raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + + # Hacky isisntance, but whatcha gonna do? + leftIsUniform = isinstance(leftKeyframe, DiveSpanUniformKeyframe) + rightIsUniform = isinstance(rightKeyframe, DiveSpanUniformKeyframe) + + leftFrameNumber = leftKeyframe.lastObservedFrameNumber + rightFrameNumber = rightKeyframe.lastObservedFrameNumber + + # Use 1.0 as default widthFactor and heightFactor for uniform keyframes. + if leftIsUniform: + transitionType = rightKeyframe.transitionIn + leftWidthValue = 1.0 + leftHeightValue = 1.0 + rightWidthValue = rightKeyframe.widthFactor + rightHeightValue = rightKeyframe.heightFactor + elif rightIsUniform: + transitionType = leftKeyframe.transitionOut + leftWidthValue = leftKeyframe.widthFactor + leftHeightValue = leftKeyframe.heightFactor + rightWidthValue = 1.0 + rightHeightValue = 1.0 + else: # both are not uniform + # TODO: probably should enforce same transition type or crash here, but + # I don't feel like it at the moment. + transitionType = leftKeyframe.transitionOut + leftWidthValue = leftKeyframe.widthFactor + leftHeightValue = leftKeyframe.heightFactor + rightWidthValue = rightKeyframe.widthFactor + rightHeightValue = rightKeyframe.heightFactor + + if transitionType == 'log-to': + widthTiltFactor = self.mathSupport.interpolateLogTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateLogTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + elif transitionType == 'root-to': + widthTiltFactor = self.mathSupport.interpolateRootTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateRootTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + elif transitionType == 'linear': + widthTiltFactor = self.mathSupport.interpolateLinearTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateLinearTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + elif transitionType == 'quadratic-to': + widthTiltFactor = self.mathSupport.interpolateQuadraticEaseOut(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateQuadraticEaseOut(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + elif transitionType == 'quadratic-from': + widthTiltFactor = self.mathSupport.interpolateQuadraticEaseIn(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateQuadraticEaseIn(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + else: # transitionType == 'quadratic-to-from' + widthTiltFactor = self.mathSupport.interpolateQuadraticEaseInOut(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolateQuadraticEaseInOut(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + + return(widthTiltFactor, heightTiltFactor) + +# Maybe these belong in timeline span? +# def getFrameNumberForTimecode(self, timecode): +# return math.floor( +# def getTimecodeForFrameNumber(self, frame_number): + + def __repr__(self): + return """\ +[DiveTimeline Project:{proj} framerate:{f}]\ +""".format(proj=self.title,f=self.framerate) + +class DiveTimelineSpan: + """ + # ?(Can be used to observe/calculate n-1 zoom factors.)? + """ + def __init__(self, timeline, frameCount, escapeSquared, maxEscapeIterations, shouldSmooth): + self.timeline = timeline + self.frameCount = int(frameCount) + + self.escapeSquared = escapeSquared + self.maxEscapeIterations = maxEscapeIterations + self.shouldSmooth = shouldSmooth + + # Only a single 'track' for each keyframe type to begin with, represented + # just as a keyframe lookup for each track. Being able to stack multiples of + # similar-typed keyframes will probably be helpful in the long run. + self.centerKeyframes = {} + self.windowKeyframes = {} + self.perspectiveKeyframes = {} + # Currently, not allowing keyframes to exist outside of the span, even though + # that is often helpful for defining pleasing transitions. + # Currently, also not allowing keyframes to exist at non-frame targets, which + # might lead to some alignment frustrations, because sub-frame calculations + # are probably kinda important. + + self.lastObservedStartFrame = 0 # To stash frame offset when extracted from Timeline + + #### + # TODO: All of these helper functions need to perform a modification of upstream and downsatream + # keyframes when forcing addition, to make sure the transition types are all in order. + # When creating a new keyframe, default is to inherit the lead-in and lead-out interpolators of the existing span. + # + # So, if it's: + # | (lin) | (default all linear) + # + # | +(unspec)K(unspec) | + # | (lin) K (lin) | + # + # | +(log-to)K(unspec) | + # | (log-to) K (lin) | + # + # | +(log-to)K(log-from) | + # | (log-to) K (log-from) | + # + # When trying to fill the range, both adjacent keyframes will agree (because it's a span property). + # I think this means when setting a keyframe, the upstream and downstream keyframes are ALWAYS SET + # to be consistent with the newly inserted keyframe. (either that, or create single-point-of-reference span structure?) + # + # BaseRangeSpan(startKey, endKey, type=lin) + # +Keyframe(log-from) => + # BaseRangeSpan(startKey, keyframe, type=lin) + BaseRangeSpan(keyframe, endkey, type=log-from) + # + # Now, some asshole wants to define a 'speed' for a section... + # This will drag some keyframes along, up to a point, where it can. + # It might even just warn if reverse is observed? + # + # Looks like we need special handling for when setting a keyframe on top of an existig keyframe, right? + # If existing keyframe at the new keyframe frane number, and new keyframe is unspecified, then + # keep the interpolators as-is. + + def addNewCenterKeyframe(self, frameNumber, centerValue, transitionIn='quadratic-to', transitionOut='quadratic-to'): + # TODO: should probably gracefully handle stomping an existing keyframe, right? + newKeyframe = DiveSpanCenterKeyframe(self, centerValue, transitionIn, transitionOut) + self.centerKeyframes[frameNumber] = newKeyframe + return newKeyframe + + def addNewWindowKeyframe(self, frameNumber, realWidth, imagWidth, transitionIn='root-to', transitionOut='root-to'): + newKeyframe = DiveSpanWindowKeyframe(self, realWidth, imagWidth, transitionIn, transitionOut) + self.windowKeyframes[frameNumber] = newKeyframe + return newKeyframe + + def addNewUniformKeyframe(self, frameNumber, transitionIn='quadratic-to', transitionOut='quadratic-to'): + newKeyframe = DiveSpanUniformKeyframe(self, transitionIn, transitionOut) + self.perspectiveKeyframes[frameNumber] = newKeyframe + return newKeyframe + + def addNewTiltKeyframe(self, frameNumber, widthFactor, heightFactor, transitionIn='quadratic-to-from', transitionOut='quadratic-to-from'): + newKeyframe = DiveSpanTiltKeyframe(self, widthFactor, heightFactor, transitionIn, transitionOut) + self.perspectiveKeyframes[frameNumber] = newKeyframe + return newKeyframe + + def getKeyframesClosestToFrameNumber(self, keyframeType, frameNumber): + """ + Returns a tuple of keyframes, which are the nearest left and right keyframes for the frameNumber. + When the frameNumber directly has a keyframe, the same keyframe is returned for both values. + + Important to remember to bless the 'lastObservedFrameNumber' into the keyframe. + """ + typeOptions = ['center', 'window', 'perspective'] + if keyframeType not in typeOptions: + raise ValueError("keyframeType must be one of (%s)" % ", ".join(typeOptions)) + + if frameNumber >= self.frameCount: + raise IndexError("Requested %s keyframe frame number '%d' is out of range for a span that's '%d' frames long" % (keyframeType, frameNumber, self.frameCount)) + + keyframeHash = None + if keyframeType == 'perspective': + keyframeHash = self.perspectiveKeyframes + elif keyframeType == 'window': + keyframeHash = self.windowKeyframes + else: # keyframeType == 'center': + keyframeHash = self.centerKeyframes + + # Direct hit + # (really should have non-integer locations for keyframes, shouldn't we?) + if frameNumber in keyframeHash: + targetKeyframe = keyframeHash[frameNumber] + targetKeyframe.lastObservedFrameNumber = frameNumber + return (targetKeyframe, targetKeyframe) + + previousKeyframe = None + nextKeyframe = None + for currFrameNumber in sorted(keyframeHash.keys()): + if currFrameNumber <= frameNumber: + previousKeyframe = keyframeHash[currFrameNumber] + previousKeyframe.lastObservedFrameNumber = currFrameNumber + if currFrameNumber > frameNumber: + nextKeyframe = keyframeHash[currFrameNumber] + nextKeyframe.lastObservedFrameNumber = currFrameNumber + break # Past the sorted range, so done looking + + return (previousKeyframe, nextKeyframe) + + def interpolateCenterValueBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + """ + Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe + """ + #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) + + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return leftKeyframe.center + + if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: + raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + + # May want to consider 'close' rather than equal for the center equivalence check. + if leftKeyframe.center == rightKeyframe.center: + return leftKeyframe.center + + # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn + if leftKeyframe.transitionOut != rightKeyframe.transitionIn: + raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) + transitionType = leftKeyframe.transitionOut + + # Python scopes seep like this, right? Just use the value later? + # + # And I kept all these separate, because I'm prety sure there will be more + # interpolation-specific parameters needed when all's said and done. + if transitionType == 'log-to': + interpolatedReal = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + elif transitionType == 'root-to': + interpolatedReal = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + elif transitionType == 'linear': + interpolatedReal = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + elif transitionType == 'quadratic-to': + interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + elif transitionType == 'quadratic-from': + interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + else: # transitionType == 'quadratic-to-from': + interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + interpolatedCenter = self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) + return interpolatedCenter + + def interpolateWindowValuesBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + """ + Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe + """ + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return (leftKeyframe.realWidth, leftKeyframe.imagWidth) + + # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn + if leftKeyframe.transitionOut != rightKeyframe.transitionIn: + raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) + + transitionType = leftKeyframe.transitionOut + if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: + raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + + interpolatedRealWidth = leftKeyframe.realWidth + interpolatedImagWidth = leftKeyframe.imagWidth + + # And I kept all these separate, because I'm prety sure there will be more + # interpolation-specific parameters needed when all's said and done. + if transitionType == 'log-to': + interpolatedRealWidth = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + elif transitionType == 'root-to': + interpolatedRealWidth = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + elif transitionType == 'linear': + interpolatedRealWidth = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + elif transitionType == 'quadratic-to': + interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + elif transitionType == 'quadratic-from': + interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + else: # transitionType == 'quadratic-to-from': + interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + + return (interpolatedRealWidth, interpolatedImagWidth) + + def __repr__(self): + return """\ +[DiveTimelineSpan {framecount} frames]\ +""".format(framecount=self.frameCount) + +class DiveSpanKeyframe: + def __init__(self, span, transitionIn='quadratic-to', transitionOut='quadratic-from'): + self.span = span + + transitionOptions = ['quadratic-to', 'quadratic-from', 'quadratic-to-from', 'log-to', 'root-to', 'linear'] + if transitionIn not in transitionOptions: + raise ValueError("transitionIn must be one of (%s)" % ", ".join(transitionOptions)) + if transitionOut not in transitionOptions: + raise ValueError("transitionOut must be one of (%s)" % ", ".join(transitionOptions)) + + self.transitionIn = transitionIn + self.transitionOut = transitionOut + + self.lastObservedFrameNumber = 0 # For stashing frame numbers in def __repr__(self): return """\ -[MediaView duration {du:f} FPS:{f:d} Output:{vf:s}]\ -""".format(du=self.duration,f=self.fps,vf=str(self.vfilename)) +[DiveSpanKeyframe, {inType} -> frame {frame} -> {outType}]\ +""".format(inType=self.transitionIn, frame=self.lastObservedFrameNumber, outType=self.transitionOut) + +class DiveSpanCenterKeyframe(DiveSpanKeyframe): + def __init__(self, span, center, transitionIn='quadratic-to', transitionOut='quadratic-from'): + super().__init__(span, transitionIn, transitionOut) + self.center = center + +class DiveSpanWindowKeyframe(DiveSpanKeyframe): + def __init__(self, span, realWidth, imagWidth, transitionIn='root-to', transitionOut='root-to'): + super().__init__(span, transitionIn, transitionOut) + self.realWidth = realWidth + self.imagWidth = imagWidth + +class DiveSpanUniformKeyframe(DiveSpanKeyframe): + def __init__(self, span, transitionIn='quadratic-to', transitionOut='quadratic-from'): + super().__init__(span, transitionIn, transitionOut) + +class DiveSpanTiltKeyframe(DiveSpanKeyframe): + def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', transitionOut='quadratic-from'): + super().__init__(span, transitionIn, transitionOut) + self.widthFactor = widthFactor + self.heightFactor = heightFactor + +#### +# Big bunch of 'still thinking about' comments here. Probably should have kept them on my own branch. +### +# +# Conceptual priority of items in a timeline goes something like... +# +# Frame Numbers +# Frames are the base grid, so nothing can 'shift' a frame number to a higher or lower value. +# +# DiveSpan +# Spans define the start and end base values (pre-modifications) of a dive animation +# +# DiveSpanCenterKeyframe and DiveSpanWindowKeyframe +# Within spans, target values to hit for a frame +# +# I think that's the end of the highest-order items. Modifications up to this point have to +# all be complete and calculated before the next items can be calculated/analyzed. +# +# For window rages, whether set as keyframes, or interpolated between, you can observe +# the effective zoom factor between any two frames, based on the transition from frame's +# values to the next -# For now, use global context for a single dive per run +# +# Idea behind ZoomFactorKeyframe +# +# Attaches to the timeline based on width ranges. Still haven't solved several +# aspects of this half-baked idea yet. +# Main goal is to be able to set a speed factor for a range of frames. +# +# If a keyframe sets an axis +# range for a specific frame number, then the two surrounding frame-spans up to the nearest adjacent +# range keyframes need to be stretched or squished so that the product of all the zoom factors +# between keyframes achieves the ranges set in the adjacent range keyframes. Or more simply, +# if you add a range keyframe, then the surrounding zoom factors are adjusted so existing +# range keyframe targets aren't changed. +# The default ramp type for zoom factor interpolation is linear. The logic behind this is that +# if the factor is linearly monotonically increasing, then the feel is acceleration. So linear +# interpolation of zoom factors results in a ramp of speed between keyframes. I imagine that more +# explicit curve control for zoom factor interpolation will be desirable. +# +# [[Linear interpolation does seem to make it harder to set a constant speed though, doesn't it?]] +# [[Does it work to add a keyframe as "set a constant zoom factor between all these frames"?]] +# + +## +# A - attempt at constructing a 'move this image to this frame' kind of behavior +## +# Take the calculated range values at frame + 4, and shift them to the target frame. +# Other stuff has to follow along... and attempting to write this is showing me what's left dangling... +# +# Quick observation: +# We'll come at this edit by looking at an existing render, and wanting a modification. +# The existing render has specific base information at a frame we're observing. +# This construction should be resilient to upstream base information modifications +# (i.e. if something 'before' is changed, this edit should still work about the same way) + +# And this really has to be range-dependent, despite my trying to dodge that over and over and over. +# I keep trying to say "the base is rigidly calculated" +# And keep realizing that speed manipulations for a visible range MUST be dependent on some +# flexible attachment of the effect to a range of base values. + +# Maybe the deal is, that range manipulations are all calculated in advance, and in the specific order of +# their declaration? Like, there's no complicated layering of range manipulations, only +# a procedural description of all the range manipulations, which is realized into the baked-in +# base information for every frame, before axes are generated or meshes are manipulated. + +# Adding set values non-monotonically, will basically play segments in reverse, right? +# start = 1.0, end = .0001, 100 frames long +# +keyframe in place at frame 10 (so frames 1-10 are equivalent to the default base ranges) +# +keyframe at frame 15, back to 1.0 (so plays start->key->start->end) +# (also, frames 15-100 are equivalent to what used to happen in frames 1-100) +# +# So now, want to set a 'speed' for 15->20 +# Or better, want to set the reverse speed to faster... +# But I shouldn't be allowed to say "play a frame range faster", should I? +# Because it's dependent on the interpolated values that it has to hit. +# But I need to be able to say for this UNINTERRUPTED frame span (could verify on application?!), +# Set my zoom factors to BLAH. +# A speed manipulation is a localized effect, that will change the (?upstream and?) downstream interpolations. +# The probem is, we maybe need to figure out what those ranges are iteratively, before allowing +# it to 'lock' into place? + +#targetFrameNumber = 2 +#sampleFromFrameNumber = targetFrameNumber + 4 +# +# Looks like that when we're sampling the 'current' shape for a future frame number, +# we need to force that number into a keyframe, if it's part of an existing manipulation? +#if mainSpan.frameNumberHasParametricEffects(sampleFromFrameNumber): +#if mainSpan.frameNumberHasBaseModifiers(sampleFromFrameNumber): +# # Creates a 'current values' keyframe, unrolls parametric changes to get to concrete values +# mainSpan.addKeyframeInPlace(sampleFromFrameNumber) +# +#addBaseRangeKeyframeInPlace +# +#focusRangeWidth, focusRangeHeight = mainSpan.getRangeShapeForFrameNumber(sampleFromFrameNumber) + +# When we set a window keyframe, we've changed the 'effective zoom factor' of all upstream and downstream frames. +# The range of the 'effective speed change' is bounded by the next closest keyframes. + +## +# B - attempt at constructing a "speed up these 4 frames" kinda behavior +## +# +#targetFrameNumber = 3 +#targetFrameCount = 4 +#startingRangeWidth, startingRangeHeight = mainSpan.getRangeShapeForFrameNumber(targetFrameNumber) +#endingRangeWidth, endingRangeHeight = mainSpan.getRangeShapeForFrameNumber(targetFrameNumber + targetFrameCount - 1) -flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) -mp.mp.prec = MPMATH_HIGH_PRECISION_SIZE # Sets mpmath's precision (in bits) +# No speed assignments that span across existing base keyframes allowed. -# The extra parameter to instantiation is the way to run with mpmath -#mandl_ctx = MandlContext(ctxc=mp.mpc) +# Seems like this is getting close. +# The thing is, for a given range (whether forward or backward), I can't manipulate the speed in a way that +# breaks the base keyframe endpoints. +# +# Maybe that's it... +# No 'speed' assignments possible, only equivalent 'base keyframe' assignments. +# So I can't say 'play from this base range to that base range faster, +# +# Really want speed to ramp as a default, and make it so sudden jumps in speed +# are more difficult to achieve by default. -# mpmath isn't as complete an alternate implementation as the others, because params -# can't change instantiation types while running set_default_params with the -# current setup, and second because I didn't check all instantiations to -# be correctly typed. -mandl_ctx = MandlContext() -view_ctx = MediaView(16, 16, mandl_ctx) +# THAT'S RIGHT - I forgot, that speed modifiers don't hang on frame numbers, but they're applied +# on regions within the visible ranges... +# BUT THAT WON'T WORK as a timeline thing, because if we reverse, then we don't want the +# speed modifier to have an effect for all the matching ranges. + +# Maybe it's that at a given keyframe, BOTH range targets, and a zoom factor can be applied? + +## Maybe "speed change" is a function that does a bunch of math for you? +#mainSpan.addRangeSpeedKeyframe(startFrameNumber, leadingTransitionFrames=2, trailingTransitionFrames=2, startingZoomFactor * 2, +# +#mainSpan.addRangeSpeedModifier(startFrameNumber, leadingTransitionFrames=2, trailingTransitionFrames=2, startingZoomFactor * 2) +# +#internally, it says: +#firstFrameZoomFactor = mainSpan.getZoomFactorBetweenFrameNumbers(startFrameNumber, startFrameNumber + 1) +#?rangeKeyframeAxisGenerator()? +#...generate keyframes for target ranges, based on this... somehow... + +class DiveMesh: + """ + A DiveMesh is a 2D array of complex numbers that serves as a basis for + calculation, with the x-axis (across the width) based on real component + distribution, and y-axis (across the height) based on imaginary + component distribution. Conceptually, this is the mapping of a portion + of complex space into a 2D "imaging"(?) plane. + + Two separate 2D (real-valued) meshes are generated first, one for the real component, + and one for the imaginary component. This means range and distribution types of the + component meshes are the highest priority parameters. + + # Not Implemented: + # Next, distortions are applied separately to the real mesh, and to the imaginary mesh. + # Then, the separate 2D meshes are combined to become the overall mesh values. + # Finally, overall distortions are applied to the overall mesh values. + """ + def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeSquared, maxEscapeIterations, shouldSmooth): + # Trying not to apply castings to these types, to keep them the same as + # the original parameters, which could make swapping out different + # precision libraries simpler? + self.meshWidth = width + self.meshHeight = height + self.center = center + + self.realMeshGenerator = realMeshGenerator + self.imagMeshGenerator = imagMeshGenerator + + #self.realMeshDistortions = [] + #self.imagMeshDistortions = [] + + #self.meshDistortions = [] + + # Technically, it seems redundant to have mathSupport specified separately for MeshGenerators + # and for DiveMesh, but it's enough of a chicken-and-egg problem that I'll just + # pass an extra parameter here and there. + self.mathSupport = mathSupport + + # These properties are attached to the mesh, provided by the DiveTimelineSpan that created them + self.escapeSquared = escapeSquared + self.maxEscapeIterations = maxEscapeIterations + self.shouldSmooth = shouldSmooth + + def generateMesh(self): + realMesh = self.realMeshGenerator.generateForDiveMesh(self) + imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) + + if realMesh.shape != imagMesh.shape: + raise ValueError("Real sub-mesh (%s) and Imaginary sub-mesh (%s) shapes don't match." % (realMesh.shape, imagMesh.shape)) + + meshShape = realMesh.shape + combinedMesh = np.zeros(meshShape, dtype=object) + # The native python 'complex' type assigns into "object" type arrays without problems, + # but not vice-versa, so use object type for everything. + + for x in range(0, meshShape[0]): + for y in range(0, meshShape[1]): + combinedMesh[x,y] = self.mathSupport.createComplex(realMesh[x,y], imagMesh[x,y]) + + return combinedMesh + + def __repr__(self): + return """\ +[DiveMesh {{{mwidth},{mheight}}} realGenerator:{rgen} imagGenerator:{igen} ]\ +""".format(mwidth=self.meshWidth, mheight=self.meshHeight, rgen=str(self.realMeshGenerator), igen=str(self.imagMeshGenerator)) + +class MeshGenerator: + def __init__(self, mathSupport, varyingAxis): + self.mathSupport = mathSupport + + axisOptions = ['width', 'height'] + if varyingAxis not in axisOptions: + raise ValueError("varyingAxis must be one of (%s)" % ", ".join(axisOptions)) + self.varyingAxis = varyingAxis + + def generateForDiveMesh(self): + raise NotImplementedError("generateForDiveMesh() must be overridden in a MeshGenerator subclass") + +class MeshGeneratorUniform(MeshGenerator): + """ + Generates a 2D mesh of values distributed across with width, along the axis specified. + + DiveMesh is used for calculations of ranges, so the DiveMesh is responsible for + instantiation of correct types. In other words, we try to avoid doing instantiations here. + + Technically, should probably make the DiveMesh responsible for valuesCenter, so there's a single + point of reference. It just seemed less clear that way at the moment. + """ + def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth): + super().__init__(mathSupport, varyingAxis) + self.valuesCenter = valuesCenter + self.baseWidth = baseWidth + + def generateForDiveMesh(self, diveMesh): + """ + e.g. + diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='width', self.valuesCenter=1.0, baseWidth=.2 returns: + [[0.9, 1.0, 1.1], + [0.9, 1.0, 1.1]] + + diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='height', self.valuesCenter=1.0, baseWidth=.2 returns: + [[0.9, 0.9, 0.9], + [1.1, 1.1, 1.1]] + """ + mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + + if self.varyingAxis == 'width': + #calculate start/end... Probably need to be subtype aware for this... + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshWidth) + mesh[0:] = discretizedValues # Assign the one-row discretization to every row of the mesh + else: # self.varyingAxis == 'height' + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshHeight) + # Assign the one-row discretization (as a column) to every column of the mesh + mesh[0:] = discretizedValues[:,np.newaxis] + + return mesh + + def __repr__(self): + return """\ +[MeshGeneratorUniform center:{vCenter} baseWidth:{vWidth} along axis:{vAxis}]\ +""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, vAxis=self.varyingAxis) + +# Not implemented yet: +# MeshGeneratorLogTilt, which uses a log scaling anchored at the middle +# Maybe also: +# DiveMeshGeneratorSqueeze (base-width->different-middle-width->base-width) +# Mostly, looking for effects that give me control over something that feels +# like camera behavior, such as lens barrel distortion. + +class MeshGeneratorTilt(MeshGenerator): + def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth, tiltFactor): + """ + Tilt is symmetric about the baseWidth. + Negative values aren't treated as negative factors, but instead as reversal + of the scaling direction (e.g. -2 means {.5,2.0}, and 2 => {2.0,.5}) + This means tilt values should not be between {-1.0,1.0}. + + Originally thought this would need 2 axis parameters, but for now, requiring + the tilt axis to be the same as varying axis seems to make sense. + + Because the tilt factor is spread across the mesh rows, there might be + some strong aliasing (across dive frames) if there's an even number + of rows or columns? I could imagine a back-and-forth wiggle developing if the + scales and factors line up the right way. + + Axis discretization happens for every frame, on 2 axes (across complex range, and across real range). + Mesh generation is the combination of these 2 axes (perhaps plus further post-processing modifications). + + A stretched axis (wider range), compared to the current frame's baseline, is akin to + calculating previous steps. For example, if you happen to stretch the axis as much as the previous + frame transition's zoom factor, then you're sorta recalculating the previous frame's axis again. + Similarly, a squished axis (narrower range), is akin to calculating future steps. + """ + super().__init__(mathSupport, varyingAxis) + self.valuesCenter = valuesCenter + self.baseWidth = baseWidth + self.tiltFactor = tiltFactor + + def generateForDiveMesh(self, diveMesh): + mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + + # Calculating only one side from the tiltFactor, then using the delta from + # the original as the other side's size. This keeps our center in the linear + # center of the ranges, instead of shifting it some amount dependent on the factor. + + # TODO: pretty sure this star/tend calculation needs to have its math done by + # the MathSupport too, to keep type requirements localized there? + startWidth = 1.0 / self.tiltFactor * self.baseWidth + startDelta = self.baseWidth - startWidth + endWidth = self.baseWidth + startDelta + if self.tiltFactor < 0.0: + endWidth = 1.0 / self.tiltFactor * -1 * self.baseWidth + endDelta = self.baseWidth - endWidth + startWidth = self.baseWidth + endDelta + + if self.varyingAxis == 'width': + # Values vary along the width axis, and the tiltFactor is applied to the + # range of each row, which effectively treats the width axis as the + # rotation point + meshRowBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshHeight) + for y in range(0, diveMesh.meshHeight): + #calculate start/end... Probably need to be subtype aware for this... + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshRowBaseWidths[y], diveMesh.meshWidth) + mesh[y] = discretizedValues # Assign the dscretization to this row + else: # self.varyingAxis == 'height' + meshColBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshWidth) + for x in range(0, diveMesh.meshWidth): + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshColBaseWidths[x], diveMesh.meshHeight) + mesh[:,x] = discretizedValues # Assign the discretization (as a column) + + return mesh + + def __repr__(self): + return """\ +[MeshGeneratorTilt center:{vCenter} baseWidth:{vWidth} tiltFactor:{tilt} along axis:'{vAxis}']\ +""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, tilt=self.tiltFactor, vAxis=self.varyingAxis) # -- # Default settings for the dive. All of these can be overridden from the # command line # -- -def set_default_params(): - global mandl_ctx - +def set_demo1_params(mandl_ctx, view_ctx): + print("+ Running in demo mode - loading default params") mandl_ctx.img_width = 1024 mandl_ctx.img_height = 768 cmplx_width_str = '5.0' cmplx_height_str = '3.5' - mandl_ctx.cmplx_width = mandl_ctx.ctxf(float(cmplx_width_str)) - mandl_ctx.cmplx_height = mandl_ctx.ctxf(float(cmplx_height_str)) - mandl_ctx.cmplx_width_decimal = Decimal(cmplx_width_str) - mandl_ctx.cmplx_height_decimal = Decimal(cmplx_height_str) - mandl_ctx.cmplx_width_flint = flint.arb(cmplx_width_str) - mandl_ctx.cmplx_height_flint = flint.arb(cmplx_height_str) + mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(cmplx_width_str) + mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(cmplx_height_str) # This is close t Misiurewicz point M32,2 # mandl_ctx.cmplx_center = mandl_ctx.ctxc(-.77568377, .13646737) center_real_str = '-1.769383179195515018213' center_imag_str = '0.00423684791873677221' - mandl_ctx.cmplx_center = mandl_ctx.ctxc(float(center_real_str),float(center_imag_str)) - mandl_ctx.cmplx_center_real_decimal = Decimal(center_real_str) - mandl_ctx.cmplx_center_imag_decimal = Decimal(center_imag_str) - mandl_ctx.cmplx_center_flint = flint.acb(center_real_str, center_imag_str) + mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_real_str, center_imag_str) - mandl_ctx.scaling_factor = .97 - mandl_ctx.num_epochs = 0 + mandl_ctx.project_name = 'demo1' + mandl_ctx.cache_path = u"%s_cache" % mandl_ctx.project_name + + mandl_ctx.scaling_factor = .90 mandl_ctx.max_iter = 255 mandl_ctx.escape_rad = 4. mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad - mandl_ctx.precision = 100 + mandl_ctx.verbose = 3 + mandl_ctx.burn_in = True + mandl_ctx.build_cache=True - view_ctx.duration = 16 - view_ctx.fps = 16 + view_ctx.duration = 2.0 + #view_ctx.duration = 10.0 + # FPS still isn't set quite right, but we'll get it there eventually. + view_ctx.fps = 23.976 / 2.0 + #view_ctx.fps = 29.97 / 2.0 -def set_preview_mode(): - global mandl_ctx +def set_preview_mode(mandl_ctx, view_ctx): print("+ Running in preview mode ") mandl_ctx.img_width = 300 mandl_ctx.img_height = 200 - mandl_ctx.cmplx_width = 3. - mandl_ctx.cmplx_height = 2.5 + mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(3.0) + mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(2.5) mandl_ctx.scaling_factor = .75 mandl_ctx.escape_rad = 4. + mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad view_ctx.duration = 4 view_ctx.fps = 4 -def set_snapshot_mode(): - global mandl_ctx +def set_snapshot_mode(mandl_ctx, view_ctx, snapshot_filename='snapshot.gif'): print("+ Running in snapshot mode ") mandl_ctx.snapshot = True + view_ctx.vfilename = snapshot_filename mandl_ctx.img_width = 3000 mandl_ctx.img_height = 2000 mandl_ctx.max_iter = 2000 - mandl_ctx.cmplx_width = 3. - mandl_ctx.cmplx_height = 2.5 + mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(3.0) + mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(2.5) mandl_ctx.scaling_factor = .99 # set so we can zoom in more accurately mandl_ctx.escape_rad = 4. + mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad view_ctx.duration = 0 view_ctx.fps = 0 -def parse_options(): - global mandl_ctx - +def parse_options(mandl_ctx, view_ctx): argv = sys.argv[1:] - opts, args = getopt.getopt(argv, "pd:m:s:f:z:w:h:c:", ["preview", + "demo", "duration=", "max-iter=", "img-w=", @@ -915,18 +1936,25 @@ def parse_options(): "color=", "burn", "flint", - "decimal", - "cache-path=", + "project-name=", "build-cache", "invalidate-cache", "banner", "smooth"]) + # Math support as to be handled first, so other parameter + # instantiations are properly typed + for opt, arg in opts: + if opt in ['--flint']: + mandl_ctx.math_support = DiveMathSupportFlint() + for opt,arg in opts: if opt in ['-p', '--preview']: - set_preview_mode() - if opt in ['-s', '--snapshot']: - set_snapshot_mode() + set_preview_mode(mandl_ctx, view_ctx) + elif opt in ['-s', '--snapshot']: + set_snapshot_mode(mandl_ctx, view_ctx, arg) + elif opt in ['--demo']: + set_demo1_params(mandl_ctx, view_ctx) for opt, arg in opts: if opt in ['-d', '--duration']: @@ -938,9 +1966,7 @@ def parse_options(): elif opt in ['-h', '--img-h']: mandl_ctx.img_height = int(arg) elif opt in ['-c', '--center']: - mandl_ctx.cmplx_center= complex(arg) - elif opt in ['-h', '--img-h']: - mandl_ctx.img_height = int(arg) + mandl_ctx.cmplx_center= mandl_ctx.math_support.createComplex(arg) elif opt in ['--scaling-factor']: mandl_ctx.scaling_factor = float(arg) elif opt in ['-z', '--zoom']: @@ -980,14 +2006,9 @@ def parse_options(): mandl_ctx.palette = m elif opt in ['--burn']: mandl_ctx.burn_in = True - elif opt in ['--decimal']: - mandl_ctx.use_high_precision = True - mandl_ctx.high_precision_type = "decimal" - elif opt in ['--flint']: - mandl_ctx.use_high_precision = True - mandl_ctx.high_precision_type = "flint" - elif opt in ['--cache-path']: - mandl_ctx.cache_path = arg + elif opt in ['--project-name']: + mandl_ctx.project_name = arg + mandl_ctx.cache_path = u"%s_cache" % mandl_ctx.project_name elif opt in ['--build-cache']: mandl_ctx.build_cache = True elif opt in ['--invalidate-cache']: @@ -1017,8 +2038,11 @@ def parse_options(): if __name__ == "__main__": print("++ mandlebort.py version %s" % (MANDL_VER)) - - set_default_params() - parse_options() + mandl_ctx = MandlContext() + view_ctx = MediaView(16, 16, mandl_ctx) + + parse_options(mandl_ctx, view_ctx) + view_ctx.run() + From 682c84dab22cd06c58efafcbfc82dbbcba3d315d Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 21 Jul 2021 00:21:43 -0700 Subject: [PATCH 06/44] Split DiveMathSupport (and the flint subclass) into its own file. Split DiveMesh (and subclasses) into its own file too. --- divemesh.py | 215 +++++++++++++++++++++ fractalmath.py | 298 +++++++++++++++++++++++++++++ mandelbrot.py | 504 +------------------------------------------------ 3 files changed, 523 insertions(+), 494 deletions(-) create mode 100644 divemesh.py create mode 100644 fractalmath.py diff --git a/divemesh.py b/divemesh.py new file mode 100644 index 0000000..e2ca5d1 --- /dev/null +++ b/divemesh.py @@ -0,0 +1,215 @@ +# -- +# File: divemesh.py +# +# For a specific frame/epoch, the mesh is the plane across complex +# space (real+imaginary) that we're computing fractals on. +# +# Overview of mesh classes +# ------------------------ +# DiveMesh +# MeshGenerator +# - MeshGeneratorUniform +# - MeshGeneratorTilt +# +# -- + +import numpy as np + +import fractalmath as fm # Technically not required, but definitely needed + +class DiveMesh: + """ + A DiveMesh is a 2D array of complex numbers that serves as a basis for + calculation, with the x-axis (across the width) based on real component + distribution, and y-axis (across the height) based on imaginary + component distribution. Conceptually, this is the mapping of a portion + of complex space into a 2D "imaging"(?) plane. + + Two separate 2D (real-valued) meshes are generated first, one for the real component, + and one for the imaginary component. This means range and distribution types of the + component meshes are the highest priority parameters. + + # Not Implemented: + # Next, distortions are applied separately to the real mesh, and to the imaginary mesh. + # Then, the separate 2D meshes are combined to become the overall mesh values. + # Finally, overall distortions are applied to the overall mesh values. + """ + def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeSquared, maxEscapeIterations, shouldSmooth): + # Trying not to apply castings to these types, to keep them the same as + # the original parameters, which could make swapping out different + # precision libraries simpler? + self.meshWidth = width + self.meshHeight = height + self.center = center + + self.realMeshGenerator = realMeshGenerator + self.imagMeshGenerator = imagMeshGenerator + + #self.realMeshDistortions = [] + #self.imagMeshDistortions = [] + + #self.meshDistortions = [] + + # Technically, it seems redundant to have mathSupport specified separately for MeshGenerators + # and for DiveMesh, but it's enough of a chicken-and-egg problem that I'll just + # pass an extra parameter here and there. + self.mathSupport = mathSupport + + # These properties are attached to the mesh, provided by the DiveTimelineSpan that created them + self.escapeSquared = escapeSquared + self.maxEscapeIterations = maxEscapeIterations + self.shouldSmooth = shouldSmooth + + def generateMesh(self): + realMesh = self.realMeshGenerator.generateForDiveMesh(self) + imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) + + if realMesh.shape != imagMesh.shape: + raise ValueError("Real sub-mesh (%s) and Imaginary sub-mesh (%s) shapes don't match." % (realMesh.shape, imagMesh.shape)) + + meshShape = realMesh.shape + combinedMesh = np.zeros(meshShape, dtype=object) + # The native python 'complex' type assigns into "object" type arrays without problems, + # but not vice-versa, so use object type for everything. + + for x in range(0, meshShape[0]): + for y in range(0, meshShape[1]): + combinedMesh[x,y] = self.mathSupport.createComplex(realMesh[x,y], imagMesh[x,y]) + + return combinedMesh + + def __repr__(self): + return """\ +[DiveMesh {{{mwidth},{mheight}}} realGenerator:{rgen} imagGenerator:{igen} ]\ +""".format(mwidth=self.meshWidth, mheight=self.meshHeight, rgen=str(self.realMeshGenerator), igen=str(self.imagMeshGenerator)) + +class MeshGenerator: + def __init__(self, mathSupport, varyingAxis): + self.mathSupport = mathSupport + + axisOptions = ['width', 'height'] + if varyingAxis not in axisOptions: + raise ValueError("varyingAxis must be one of (%s)" % ", ".join(axisOptions)) + self.varyingAxis = varyingAxis + + def generateForDiveMesh(self): + raise NotImplementedError("generateForDiveMesh() must be overridden in a MeshGenerator subclass") + +class MeshGeneratorUniform(MeshGenerator): + """ + Generates a 2D mesh of values distributed across with width, along the axis specified. + + DiveMesh is used for calculations of ranges, so the DiveMesh is responsible for + instantiation of correct types. In other words, we try to avoid doing instantiations here. + + Technically, should probably make the DiveMesh responsible for valuesCenter, so there's a single + point of reference. It just seemed less clear that way at the moment. + """ + def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth): + super().__init__(mathSupport, varyingAxis) + self.valuesCenter = valuesCenter + self.baseWidth = baseWidth + + def generateForDiveMesh(self, diveMesh): + """ + e.g. + diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='width', self.valuesCenter=1.0, baseWidth=.2 returns: + [[0.9, 1.0, 1.1], + [0.9, 1.0, 1.1]] + + diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='height', self.valuesCenter=1.0, baseWidth=.2 returns: + [[0.9, 0.9, 0.9], + [1.1, 1.1, 1.1]] + """ + mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + + if self.varyingAxis == 'width': + #calculate start/end... Probably need to be subtype aware for this... + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshWidth) + mesh[0:] = discretizedValues # Assign the one-row discretization to every row of the mesh + else: # self.varyingAxis == 'height' + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshHeight) + # Assign the one-row discretization (as a column) to every column of the mesh + mesh[0:] = discretizedValues[:,np.newaxis] + + return mesh + + def __repr__(self): + return """\ +[MeshGeneratorUniform center:{vCenter} baseWidth:{vWidth} along axis:{vAxis}]\ +""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, vAxis=self.varyingAxis) + +# Not implemented yet: +# MeshGeneratorLogTilt, which uses a log scaling anchored at the middle +# Maybe also: +# DiveMeshGeneratorSqueeze (base-width->different-middle-width->base-width) +# Mostly, looking for effects that give me control over something that feels +# like camera behavior, such as lens barrel distortion. + +class MeshGeneratorTilt(MeshGenerator): + def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth, tiltFactor): + """ + Tilt is symmetric about the baseWidth. + Negative values aren't treated as negative factors, but instead as reversal + of the scaling direction (e.g. -2 means {.5,2.0}, and 2 => {2.0,.5}) + This means tilt values should not be between {-1.0,1.0}. + + Originally thought this would need 2 axis parameters, but for now, requiring + the tilt axis to be the same as varying axis seems to make sense. + + Because the tilt factor is spread across the mesh rows, there might be + some strong aliasing (across dive frames) if there's an even number + of rows or columns? I could imagine a back-and-forth wiggle developing if the + scales and factors line up the right way. + + Axis discretization happens for every frame, on 2 axes (across complex range, and across real range). + Mesh generation is the combination of these 2 axes (perhaps plus further post-processing modifications). + + A stretched axis (wider range), compared to the current frame's baseline, is akin to + calculating previous steps. For example, if you happen to stretch the axis as much as the previous + frame transition's zoom factor, then you're sorta recalculating the previous frame's axis again. + Similarly, a squished axis (narrower range), is akin to calculating future steps. + """ + super().__init__(mathSupport, varyingAxis) + self.valuesCenter = valuesCenter + self.baseWidth = baseWidth + self.tiltFactor = tiltFactor + + def generateForDiveMesh(self, diveMesh): + mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + + # Calculating only one side from the tiltFactor, then using the delta from + # the original as the other side's size. This keeps our center in the linear + # center of the ranges, instead of shifting it some amount dependent on the factor. + + # TODO: pretty sure this star/tend calculation needs to have its math done by + # the MathSupport too, to keep type requirements localized there? + startWidth = 1.0 / self.tiltFactor * self.baseWidth + startDelta = self.baseWidth - startWidth + endWidth = self.baseWidth + startDelta + if self.tiltFactor < 0.0: + endWidth = 1.0 / self.tiltFactor * -1 * self.baseWidth + endDelta = self.baseWidth - endWidth + startWidth = self.baseWidth + endDelta + + if self.varyingAxis == 'width': + # Values vary along the width axis, and the tiltFactor is applied to the + # range of each row, which effectively treats the width axis as the + # rotation point + meshRowBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshHeight) + for y in range(0, diveMesh.meshHeight): + #calculate start/end... Probably need to be subtype aware for this... + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshRowBaseWidths[y], diveMesh.meshWidth) + mesh[y] = discretizedValues # Assign the dscretization to this row + else: # self.varyingAxis == 'height' + meshColBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshWidth) + for x in range(0, diveMesh.meshWidth): + discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshColBaseWidths[x], diveMesh.meshHeight) + mesh[:,x] = discretizedValues # Assign the discretization (as a column) + + return mesh + + def __repr__(self): + return """\ +[MeshGeneratorTilt center:{vCenter} baseWidth:{vWidth} tiltFactor:{tilt} along axis:'{vAxis}']\ +""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, tilt=self.tiltFactor, vAxis=self.varyingAxis) diff --git a/fractalmath.py b/fractalmath.py new file mode 100644 index 0000000..8e9507f --- /dev/null +++ b/fractalmath.py @@ -0,0 +1,298 @@ +# -- +# File: fractalmath.py +# +# Home for math operations that should be high-precision aware +# +# -- + +import math + +import numpy as np + +#FLINT_HIGH_PRECISION_SIZE = 16 # 53 is how many bits are in float64 +FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 +#FLINT_HIGH_PRECISION_SIZE = 200 + +class DiveMathSupport: + """ + Toolbox for math functions that need to be type-aware, so we + can support different back-end math libraries. + + This base class is for native python calculations. + + This exists because globally swapping out types doesn't do enough + to keep numeric types all in line. Some calculations also need additional + steps to make the math behave right, such as if we were using + the Decimal library, and had to keep separate real and imaginary + components of a complex number ourselves. + + Trying to preserve types where possible, without forcing casting, because sometimes + all the math operations will already work for custom numeric types. + """ + + def createComplex(self, realComponent, imagComponent): + """Compatible complex types will return values for .real() and .imag()""" + return complex(float(realComponent), float(imagComponent)) + + def createFloat(self, floatValue): + return float(floatValue) + + def floor(self, value): + return math.floor(value) + + def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): + """ + Shortcut calculate the starting point for the last frame's properties, + which we'll use for instantiation with specific widths. This is + because if any keyframes are added, the best we can do is measure an + effective zoom factor between any 2 frames. + + Somehow, this seems to be ok for flint too?! + """ + return startValue * (overallZoomFactor ** iterations) + + def createLinspace(self, paramFirst, paramLast, quantity): + """Attempt at a mostly type-agnostic linspace(), seems to work with flint types too""" + dataRange = paramLast - paramFirst + answers = np.zeros((quantity), dtype=object) + + for x in range(0, quantity): + answers[x] = paramFirst + dataRange * (x / (quantity - 1)) + + return answers + + def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): + """ + Attempt at a mostly type-agnostic linspace-from-center-and-width + + Turns out, we didn't *really* need this for using flint, though it + was an important for a Python/Decimal version to be able to have a + type-specific implementation in what was a DiveMeshDecimal function + """ + return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): + """ + Probably want additional log-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + # y1 = a * ln(b * x1) + # y2 = a * ln(b * x2) + # a = (y1-y2) / ln(x1/x2) + # b = exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) + # + # y = a * ln(b * x) + # answer = a * ln(b * targetX) + # answer = ((y1-y2)/ln(x1/x2)) * ln(exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) * targetX) + # This kinda worked, but keeps the ln function so you just see part of + # some curve through the query points. Really, want to make sure the asymptote + # is at or near the start (or end). + # aVal = ((startY-endY) / math.log(startX / endX)) + # bVal = math.exp(startY / aVal) / startX + # return aVal * math.log(bVal * targetX) + + # This version seems like it worked fine, but ended up with a pretty wrong + # transition + # + # But, if we set the crossover point to the first point (add 1 to xVal), then all we + # need to do is solve for a, right? Also, using the 0-crossover as the + # first point means the asymptote can't be hit, because it's 1 less than + # the start. + # + # endY = a * ln(endX + 1 - startX) + startY + # a = (endY - startY) / ln(endX - startX + 1) + + aVal = (endY - startY) / math.log(endX - startX + 1) + return aVal * math.log(targetX - startX + 1) + startY + + def interpolateRootTo(self, startX, startY, endX, endY, targetX): + """ + Iterative multiplications of window sizes for zooming means we want to be able to + interpolate between two points using the root if the frame count between them as + the scale factor + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + root = endX - startX + scaleFactor = (endY / startY) ** (1 / root) + return startY * (scaleFactor ** targetX) + + def interpolateQuadraticEaseIn(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseIn puts the majority of changes in the start of the X range. + + I *think* this is just the backwards solution to QuadraticEaseOut? + So, just swap in and out points? + + Probably want additional quadratic-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + return self.interpolateQuadraticEaseOut(endX, endY, startX, startY, targetX) + + def interpolateQuadraticEaseOut(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseOut leaves the majority of changes to the end of the X range. + + Probably want additional quadratic params, but for now, let's just bake in one equation + which uses the first point as the vertex, and passes through the second point. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + # Find a, given that the start point is the vertex, and the parabola passes + # through the other point + # y = a * (x - h)**2 + k + # y = a * (x - startX)**2 + startY + # endY = a * (endX - startX)**2 + startY + # a = ((endY-startY)/((endX-startX)**2) + # + # answer = a * (targetX - startX)**2 + startY + # answer = ((endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY + return (endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY + + def interpolateQuadraticEaseInOut(self, startX, startY, endX, endY, targetX): + """ + QuadraticEaseInOut finds a (linear) midpoint of the range, then leaves the + majority of the change to the middle of the range, easing both out of + the start, and into the end along separate parabolas. + + CAUTION: This forces the 'middle' X value to floor(). The idea is that X + will be frame numbers, so it should gracefully handle the inflection + around an integer-valued frame, instead of doing a (maybe more + complicated?) rounding inference. + The problem is that this means it isn't a universal solver. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + midpointFrame = self.floor(((endX-startX) * .5)) + startX + midpointY = self.interpolateLinear(startX, startY, endX, endY, midpointFrame) + if targetX <= midpointFrame: + return self.interpolateQuadraticEaseOut(startX, startY, midpointFrame, midpointY, targetX) + else: + return self.interpolateQuadraticEaseIn(midpointFrame, midpointY, endX, endY, targetX) + + def interpolateLinear(self, startX, startY, endX, endY, targetX): + # x1=2, y1=1, x2=12, y2=2, targetX = 7 + # valuesRange = endX - startX = 10 + # targetPercent = (targetX - startX) / valuesRange = .5 + # valuesDomain = endY - startY = 1 + # answer = targetPercent * valuesDomain + startY = 1.5 + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + return (((targetX - startX)/ (endX - startX)) * (endY - startY)) + startY + + def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): + z = self.createComplex(0, 0) + n = 0 + + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + while ((z.real*z.real)+(z.imag*z.imag)) <= escapeSquared and n < maxIter: + z = z*z + c + n += 1 +# + if n == maxIter: + return maxIter + + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + if shouldSmooth == True: + z = z*z + c; n+=1 # a couple extra iterations helps + z = z*z + c; n+=1 # decrease the size of the error + mu = n + 1 - math.log(math.log(abs(z))) + return mu + else: + return n + +class DiveMathSupportFlint(DiveMathSupport): + """ + Overrides to instantiate flint-specific complex types + + Looks like flint types are safe to use in base's createLinspace() + """ + def __init__(self): + super().__init__() + + self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. + self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) + + def createComplex(self, realComponent, imagComponent): + return self.flint.acb(realComponent, imagComponent) + + def createFloat(self, floatValue): + return self.flint.arb(floatValue) + + def floor(self, value): + return self.flint.arb(value).floor() + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): + """ + Probably want additional log-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + aVal = (endY - startY) / (self.flint.arb(endX - startX + 1).log()) + return aVal * (self.flint.arb(targetX - startX + 1).log()) + startY + + def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): + """ + NOTE: Smoothing maybe should be only a post-processing step? Maybe not? + + This flint-specific implementation only really does flint-y logs in the smoothing. + The Decimal-specific implementation needed some extra steps, but I've ditched that one. + """ + z = self.createComplex(0, 0) + n = 0 + + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane) + # + # However, instead we just square both sides of the inequality to + # avoid the sqrt + # + # Important to cast the escape answer back to float, or the arb gets sheared bizarrely + while (float((z.real*z.real)+(z.imag*z.imag))) <= escapeSquared and n < maxIter: + z = z*z + c + n += 1 + + if n == maxIter: + return maxIter + + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + if shouldSmooth == True: + z = z*z + c; n+=1 # a couple extra iterations helps + z = z*z + c; n+=1 # decrease the size of the error + + #mu = n + 1 - math.log(self.mp.log2(abs(z))) # (and, there IS NO log2 in mpmath... hrm) + # Maybe this is the right way to do the same thing?! + mu = n + 1 - z.abs_lower().log().log() + return mu + else: + return n + diff --git a/mandelbrot.py b/mandelbrot.py index 5d2e47e..5776d96 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -43,301 +43,18 @@ # -- our files import fractalcache as fc import fractalpalette as fp +import fractalmath as fm -MANDL_VER = "0.1" - -#FLINT_HIGH_PRECISION_SIZE = 16 # 53 is how many bits are in float64 -FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 -#FLINT_HIGH_PRECISION_SIZE = 200 - -class DiveMathSupport: - """ - Toolbox for math functions that need to be type-aware, so we - can support different back-end math libraries. - - This exists because globally swapping out types doesn't do enough - to keep numeric types all in line. Some calculations also need additional - steps to make the math behave right, such as if we were using - the Decimal library, and had to keep separate real and imaginary - components of a complex number ourselves. - - Trying to preserve types where possible, without forcing casting, because sometimes - all the math operations will already work for custom numeric types. - """ - - def createComplex(self, realComponent, imagComponent): - """Compatible complex types will return values for .real() and .imag()""" - return complex(float(realComponent), float(imagComponent)) - - def createFloat(self, floatValue): - return float(floatValue) - - def floor(self, value): - return math.floor(value) - - def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): - """ - Shortcut calculate the starting point for the last frame's properties, - which we'll use for instantiation with specific widths. This is - because if any keyframes are added, the best we can do is measure an - effective zoom factor between any 2 frames. - - Somehow, this seems to be ok for flint too?! - """ - return startValue * (overallZoomFactor ** iterations) - - def createLinspace(self, paramFirst, paramLast, quantity): - """Attempt at a mostly type-agnostic linspace(), seems to work with flint types too""" - dataRange = paramLast - paramFirst - answers = np.zeros((quantity), dtype=object) - - for x in range(0, quantity): - answers[x] = paramFirst + dataRange * (x / (quantity - 1)) - - return answers - - def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): - """ - Attempt at a mostly type-agnostic linspace-from-center-and-width - - Turns out, we didn't *really* need this for using flint, though it - was an important for a Python/Decimal version to be able to have a - type-specific implementation in what was a DiveMeshDecimal function - """ - return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) - - def interpolateLogTo(self, startX, startY, endX, endY, targetX): - """ - Probably want additional log-defining params, but for now, let's just bake in one equation - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - # y1 = a * ln(b * x1) - # y2 = a * ln(b * x2) - # a = (y1-y2) / ln(x1/x2) - # b = exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) - # - # y = a * ln(b * x) - # answer = a * ln(b * targetX) - # answer = ((y1-y2)/ln(x1/x2)) * ln(exp(((y2 * ln(x1)) - (y1 * ln(x2))) / (y1-y2)) * targetX) - # This kinda worked, but keeps the ln function so you just see part of - # some curve through the query points. Really, want to make sure the asymptote - # is at or near the start (or end). - # aVal = ((startY-endY) / math.log(startX / endX)) - # bVal = math.exp(startY / aVal) / startX - # return aVal * math.log(bVal * targetX) - - # This version seems like it worked fine, but ended up with a pretty wrong - # transition - # - # But, if we set the crossover point to the first point (add 1 to xVal), then all we - # need to do is solve for a, right? Also, using the 0-crossover as the - # first point means the asymptote can't be hit, because it's 1 less than - # the start. - # - # endY = a * ln(endX + 1 - startX) + startY - # a = (endY - startY) / ln(endX - startX + 1) - - aVal = (endY - startY) / math.log(endX - startX + 1) - return aVal * math.log(targetX - startX + 1) + startY - - def interpolateRootTo(self, startX, startY, endX, endY, targetX): - """ - Iterative multiplications of window sizes for zooming means we want to be able to - interpolate between two points using the root if the frame count between them as - the scale factor - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - root = endX - startX - scaleFactor = (endY / startY) ** (1 / root) - return startY * (scaleFactor ** targetX) - - def interpolateQuadraticEaseIn(self, startX, startY, endX, endY, targetX): - """ - QuadraticEaseIn puts the majority of changes in the start of the X range. - - I *think* this is just the backwards solution to QuadraticEaseOut? - So, just swap in and out points? - - Probably want additional quadratic-defining params, but for now, let's just bake in one equation - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - return self.interpolateQuadraticEaseOut(endX, endY, startX, startY, targetX) - - def interpolateQuadraticEaseOut(self, startX, startY, endX, endY, targetX): - """ - QuadraticEaseOut leaves the majority of changes to the end of the X range. - - Probably want additional quadratic params, but for now, let's just bake in one equation - which uses the first point as the vertex, and passes through the second point. - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - # Find a, given that the start point is the vertex, and the parabola passes - # through the other point - # y = a * (x - h)**2 + k - # y = a * (x - startX)**2 + startY - # endY = a * (endX - startX)**2 + startY - # a = ((endY-startY)/((endX-startX)**2) - # - # answer = a * (targetX - startX)**2 + startY - # answer = ((endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY - return (endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY - - def interpolateQuadraticEaseInOut(self, startX, startY, endX, endY, targetX): - """ - QuadraticEaseInOut finds a (linear) midpoint of the range, then leaves the - majority of the change to the middle of the range, easing both out of - the start, and into the end along separate parabolas. - - CAUTION: This forces the 'middle' X value to floor(). The idea is that X - will be frame numbers, so it should gracefully handle the inflection - around an integer-valued frame, instead of doing a (maybe more - complicated?) rounding inference. - The problem is that this means it isn't a universal solver. - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - midpointFrame = self.floor(((endX-startX) * .5)) + startX - midpointY = self.interpolateLinear(startX, startY, endX, endY, midpointFrame) - if targetX <= midpointFrame: - return self.interpolateQuadraticEaseOut(startX, startY, midpointFrame, midpointY, targetX) - else: - return self.interpolateQuadraticEaseIn(midpointFrame, midpointY, endX, endY, targetX) - - def interpolateLinear(self, startX, startY, endX, endY, targetX): - # x1=2, y1=1, x2=12, y2=2, targetX = 7 - # valuesRange = endX - startX = 10 - # targetPercent = (targetX - startX) / valuesRange = .5 - # valuesDomain = endY - startY = 1 - # answer = targetPercent * valuesDomain + startY = 1.5 - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - return (((targetX - startX)/ (endX - startX)) * (endY - startY)) + startY - - def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): - z = self.createComplex(0, 0) - n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - while ((z.real*z.real)+(z.imag*z.imag)) <= escapeSquared and n < maxIter: - z = z*z + c - n += 1 -# - if n == maxIter: - return maxIter - - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if shouldSmooth == True: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - mu = n + 1 - math.log(math.log(abs(z))) - return mu - else: - return n - -class DiveMathSupportFlint(DiveMathSupport): - """ - Overrides to instantiate flint-specific complex types - - Looks like flint types are safe to use in base's createLinspace() - """ - def __init__(self): - super().__init__() - - self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. - self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) - - def createComplex(self, realComponent, imagComponent): - return self.flint.acb(realComponent, imagComponent) - - def createFloat(self, floatValue): - return self.flint.arb(floatValue) - - def floor(self, value): - return self.flint.arb(value).floor() - - def interpolateLogTo(self, startX, startY, endX, endY, targetX): - """ - Probably want additional log-defining params, but for now, let's just bake in one equation - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - aVal = (endY - startY) / (self.flint.arb(endX - startX + 1).log()) - return aVal * (self.flint.arb(targetX - startX + 1).log()) + startY - - def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): - """ - NOTE: Smoothing maybe should be only a post-processing step? Maybe not? - - This flint-specific implementation only really does flint-y logs in the smoothing. - The Decimal-specific implementation needed some extra steps, but I've ditched that one. - """ - z = self.createComplex(0, 0) - n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - # - # Important to cast the escape answer back to float, or the arb gets sheared bizarrely - while (float((z.real*z.real)+(z.imag*z.imag))) <= escapeSquared and n < maxIter: - z = z*z + c - n += 1 - - if n == maxIter: - return maxIter - - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if shouldSmooth == True: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - - #mu = n + 1 - math.log(self.mp.log2(abs(z))) # (and, there IS NO log2 in mpmath... hrm) - # Maybe this is the right way to do the same thing?! - mu = n + 1 - z.abs_lower().log().log() - return mu - else: - return n +import divemesh as mesh +MANDL_VER = "0.1" class MandlContext: """ The context for a single dive """ - def __init__(self, math_support=DiveMathSupport()): + def __init__(self, math_support=fm.DiveMathSupport()): self.math_support = math_support self.img_width = 0 # int : Wide of Image in pixels @@ -848,10 +565,6 @@ class DiveTimeline: - DiveSpanWindowKeyframe - DiveSpanUniformKeyframe - DiveSpanTiltKeyframe - DiveMesh - MeshGenerator - - MeshGeneratorUniform - - MeshGeneratorTilt There are currently only 3 'tracks' of keyframes (for complex center, window base sizes, and perspective). Keyframes @@ -1061,19 +774,19 @@ def getMeshForFrame(self, frameNumber): if previousIsUniform and nextIsUniform: # Might feel like "baseImagWidth" is a typo (because it's distributed vertically), but # it's the 'imaginary width', even though we use it as the vertical element in the final mesh - realMeshGenerator = MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal) - imagMeshGenerator = MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag) + realMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal) + imagMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag) else: (widthTiltFactor, heightTiltFactor) = self.interpolateTiltFactorsBetweenPerspectiveKeyframes(localFrameNumber, previousPerspectiveKeyframe, nextPerspectiveKeyframe) # Tilt factor is the multiplier applied to the range. - realMeshGenerator = MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal, tiltFactor=widthTiltFactor) - imagMeshGenerator = MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag, tiltFactor=heightTiltFactor) + realMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal, tiltFactor=widthTiltFactor) + imagMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag, tiltFactor=heightTiltFactor) # Passing lots into the dive mesh. Notably, some info about the DiveTimelineSpan that was # responsible for creating this mesh. Might want to store an actual reference to the object, but # doesn't seem needed yet? - diveMesh = DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeSquared, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) + diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeSquared, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) #print (diveMesh) return diveMesh @@ -1543,202 +1256,6 @@ def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', #?rangeKeyframeAxisGenerator()? #...generate keyframes for target ranges, based on this... somehow... -class DiveMesh: - """ - A DiveMesh is a 2D array of complex numbers that serves as a basis for - calculation, with the x-axis (across the width) based on real component - distribution, and y-axis (across the height) based on imaginary - component distribution. Conceptually, this is the mapping of a portion - of complex space into a 2D "imaging"(?) plane. - - Two separate 2D (real-valued) meshes are generated first, one for the real component, - and one for the imaginary component. This means range and distribution types of the - component meshes are the highest priority parameters. - - # Not Implemented: - # Next, distortions are applied separately to the real mesh, and to the imaginary mesh. - # Then, the separate 2D meshes are combined to become the overall mesh values. - # Finally, overall distortions are applied to the overall mesh values. - """ - def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeSquared, maxEscapeIterations, shouldSmooth): - # Trying not to apply castings to these types, to keep them the same as - # the original parameters, which could make swapping out different - # precision libraries simpler? - self.meshWidth = width - self.meshHeight = height - self.center = center - - self.realMeshGenerator = realMeshGenerator - self.imagMeshGenerator = imagMeshGenerator - - #self.realMeshDistortions = [] - #self.imagMeshDistortions = [] - - #self.meshDistortions = [] - - # Technically, it seems redundant to have mathSupport specified separately for MeshGenerators - # and for DiveMesh, but it's enough of a chicken-and-egg problem that I'll just - # pass an extra parameter here and there. - self.mathSupport = mathSupport - - # These properties are attached to the mesh, provided by the DiveTimelineSpan that created them - self.escapeSquared = escapeSquared - self.maxEscapeIterations = maxEscapeIterations - self.shouldSmooth = shouldSmooth - - def generateMesh(self): - realMesh = self.realMeshGenerator.generateForDiveMesh(self) - imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) - - if realMesh.shape != imagMesh.shape: - raise ValueError("Real sub-mesh (%s) and Imaginary sub-mesh (%s) shapes don't match." % (realMesh.shape, imagMesh.shape)) - - meshShape = realMesh.shape - combinedMesh = np.zeros(meshShape, dtype=object) - # The native python 'complex' type assigns into "object" type arrays without problems, - # but not vice-versa, so use object type for everything. - - for x in range(0, meshShape[0]): - for y in range(0, meshShape[1]): - combinedMesh[x,y] = self.mathSupport.createComplex(realMesh[x,y], imagMesh[x,y]) - - return combinedMesh - - def __repr__(self): - return """\ -[DiveMesh {{{mwidth},{mheight}}} realGenerator:{rgen} imagGenerator:{igen} ]\ -""".format(mwidth=self.meshWidth, mheight=self.meshHeight, rgen=str(self.realMeshGenerator), igen=str(self.imagMeshGenerator)) - -class MeshGenerator: - def __init__(self, mathSupport, varyingAxis): - self.mathSupport = mathSupport - - axisOptions = ['width', 'height'] - if varyingAxis not in axisOptions: - raise ValueError("varyingAxis must be one of (%s)" % ", ".join(axisOptions)) - self.varyingAxis = varyingAxis - - def generateForDiveMesh(self): - raise NotImplementedError("generateForDiveMesh() must be overridden in a MeshGenerator subclass") - -class MeshGeneratorUniform(MeshGenerator): - """ - Generates a 2D mesh of values distributed across with width, along the axis specified. - - DiveMesh is used for calculations of ranges, so the DiveMesh is responsible for - instantiation of correct types. In other words, we try to avoid doing instantiations here. - - Technically, should probably make the DiveMesh responsible for valuesCenter, so there's a single - point of reference. It just seemed less clear that way at the moment. - """ - def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth): - super().__init__(mathSupport, varyingAxis) - self.valuesCenter = valuesCenter - self.baseWidth = baseWidth - - def generateForDiveMesh(self, diveMesh): - """ - e.g. - diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='width', self.valuesCenter=1.0, baseWidth=.2 returns: - [[0.9, 1.0, 1.1], - [0.9, 1.0, 1.1]] - - diveMesh.meshWidth=3, diveMesh.meshHeight=2, self.varyingAxis='height', self.valuesCenter=1.0, baseWidth=.2 returns: - [[0.9, 0.9, 0.9], - [1.1, 1.1, 1.1]] - """ - mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) - - if self.varyingAxis == 'width': - #calculate start/end... Probably need to be subtype aware for this... - discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshWidth) - mesh[0:] = discretizedValues # Assign the one-row discretization to every row of the mesh - else: # self.varyingAxis == 'height' - discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshHeight) - # Assign the one-row discretization (as a column) to every column of the mesh - mesh[0:] = discretizedValues[:,np.newaxis] - - return mesh - - def __repr__(self): - return """\ -[MeshGeneratorUniform center:{vCenter} baseWidth:{vWidth} along axis:{vAxis}]\ -""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, vAxis=self.varyingAxis) - -# Not implemented yet: -# MeshGeneratorLogTilt, which uses a log scaling anchored at the middle -# Maybe also: -# DiveMeshGeneratorSqueeze (base-width->different-middle-width->base-width) -# Mostly, looking for effects that give me control over something that feels -# like camera behavior, such as lens barrel distortion. - -class MeshGeneratorTilt(MeshGenerator): - def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth, tiltFactor): - """ - Tilt is symmetric about the baseWidth. - Negative values aren't treated as negative factors, but instead as reversal - of the scaling direction (e.g. -2 means {.5,2.0}, and 2 => {2.0,.5}) - This means tilt values should not be between {-1.0,1.0}. - - Originally thought this would need 2 axis parameters, but for now, requiring - the tilt axis to be the same as varying axis seems to make sense. - - Because the tilt factor is spread across the mesh rows, there might be - some strong aliasing (across dive frames) if there's an even number - of rows or columns? I could imagine a back-and-forth wiggle developing if the - scales and factors line up the right way. - - Axis discretization happens for every frame, on 2 axes (across complex range, and across real range). - Mesh generation is the combination of these 2 axes (perhaps plus further post-processing modifications). - - A stretched axis (wider range), compared to the current frame's baseline, is akin to - calculating previous steps. For example, if you happen to stretch the axis as much as the previous - frame transition's zoom factor, then you're sorta recalculating the previous frame's axis again. - Similarly, a squished axis (narrower range), is akin to calculating future steps. - """ - super().__init__(mathSupport, varyingAxis) - self.valuesCenter = valuesCenter - self.baseWidth = baseWidth - self.tiltFactor = tiltFactor - - def generateForDiveMesh(self, diveMesh): - mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) - - # Calculating only one side from the tiltFactor, then using the delta from - # the original as the other side's size. This keeps our center in the linear - # center of the ranges, instead of shifting it some amount dependent on the factor. - - # TODO: pretty sure this star/tend calculation needs to have its math done by - # the MathSupport too, to keep type requirements localized there? - startWidth = 1.0 / self.tiltFactor * self.baseWidth - startDelta = self.baseWidth - startWidth - endWidth = self.baseWidth + startDelta - if self.tiltFactor < 0.0: - endWidth = 1.0 / self.tiltFactor * -1 * self.baseWidth - endDelta = self.baseWidth - endWidth - startWidth = self.baseWidth + endDelta - - if self.varyingAxis == 'width': - # Values vary along the width axis, and the tiltFactor is applied to the - # range of each row, which effectively treats the width axis as the - # rotation point - meshRowBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshHeight) - for y in range(0, diveMesh.meshHeight): - #calculate start/end... Probably need to be subtype aware for this... - discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshRowBaseWidths[y], diveMesh.meshWidth) - mesh[y] = discretizedValues # Assign the dscretization to this row - else: # self.varyingAxis == 'height' - meshColBaseWidths = self.mathSupport.createLinspace(startWidth, endWidth, diveMesh.meshWidth) - for x in range(0, diveMesh.meshWidth): - discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, meshColBaseWidths[x], diveMesh.meshHeight) - mesh[:,x] = discretizedValues # Assign the discretization (as a column) - - return mesh - - def __repr__(self): - return """\ -[MeshGeneratorTilt center:{vCenter} baseWidth:{vWidth} tiltFactor:{tilt} along axis:'{vAxis}']\ -""".format(vCenter=self.valuesCenter, vWidth=self.baseWidth, tilt=self.tiltFactor, vAxis=self.varyingAxis) # -- @@ -1777,7 +1294,6 @@ def set_demo1_params(mandl_ctx, view_ctx): mandl_ctx.build_cache=True view_ctx.duration = 2.0 - #view_ctx.duration = 10.0 # FPS still isn't set quite right, but we'll get it there eventually. view_ctx.fps = 23.976 / 2.0 @@ -1867,7 +1383,7 @@ def parse_options(mandl_ctx, view_ctx): # instantiations are properly typed for opt, arg in opts: if opt in ['--flint']: - mandl_ctx.math_support = DiveMathSupportFlint() + mandl_ctx.math_support = fm.DiveMathSupportFlint() for opt,arg in opts: if opt in ['-p', '--preview']: From c3004ab7f6017b064a80de3eb614315bdf61716e Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 21 Jul 2021 00:24:53 -0700 Subject: [PATCH 07/44] forgot to fix an instantiation --- mandelbrot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mandelbrot.py b/mandelbrot.py index 5776d96..6544da5 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -278,8 +278,8 @@ def calc_cur_frame(self, snapshot_filename = None): # Calculate box in complex plane from center point # -- - re_start = self.ctxf(self.cmplx_center.real - (self.cmplx_width / 2.)) - re_end = self.ctxf(self.cmplx_center.real + (self.cmplx_width / 2.)) + re_start = self.math_support.createFloat(self.cmplx_center.real - (self.cmplx_width / 2.)) + re_end = self.math_support.createFloat(self.cmplx_center.real + (self.cmplx_width / 2.)) im_start = self.math_support.createFloat(self.cmplx_center.imag - (self.cmplx_height / 2.)) im_end = self.math_support.createFloat(self.cmplx_center.imag + (self.cmplx_height / 2.)) From 1ab4aef5b808d999520a11f2af8479ddcb34c39a Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 21 Jul 2021 10:13:59 -0700 Subject: [PATCH 08/44] squaring optimization spotted on Stack Overflow, to shave a fraction of a second --- fractalmath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fractalmath.py b/fractalmath.py index 8e9507f..3ba8854 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -209,7 +209,7 @@ def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): # # However, instead we just square both sides of the inequality to # avoid the sqrt - while ((z.real*z.real)+(z.imag*z.imag)) <= escapeSquared and n < maxIter: + while ((z.real**2)+(z.imag**2)) <= escapeSquared and n < maxIter: z = z*z + c n += 1 # @@ -276,7 +276,7 @@ def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): # avoid the sqrt # # Important to cast the escape answer back to float, or the arb gets sheared bizarrely - while (float((z.real*z.real)+(z.imag*z.imag))) <= escapeSquared and n < maxIter: + while (float((z.real**2)+(z.imag**2))) <= escapeSquared and n < maxIter: z = z*z + c n += 1 From 40575c5b2ddbfee05189efdabf19d2c470c7efa2 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Thu, 22 Jul 2021 16:06:38 -0700 Subject: [PATCH 09/44] Cache now might go to two different places. Another step towards removing epoch calculations, replacing them with timeline interpolations --- clint_runscript.sh | 6 +- divemesh.py | 7 +- fractalcache.py | 214 ++++++++++--------- fractalmath.py | 126 ++++++----- mandelbrot.py | 516 +++++++++++++++------------------------------ 5 files changed, 368 insertions(+), 501 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 108a1b2..4662c53 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,7 +1,9 @@ -#python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo -python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache +python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo +#python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache #python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache +#python3 mandelbrot.py --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk +#python3 mandelbrot.py --gif=native_julia_demo_fresh.gif --color=exp2 --demo-julia-walk --invalidate-cache diff --git a/divemesh.py b/divemesh.py index e2ca5d1..d37d58d 100644 --- a/divemesh.py +++ b/divemesh.py @@ -34,7 +34,7 @@ class DiveMesh: # Then, the separate 2D meshes are combined to become the overall mesh values. # Finally, overall distortions are applied to the overall mesh values. """ - def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeSquared, maxEscapeIterations, shouldSmooth): + def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeRadius, maxEscapeIterations, shouldSmooth): # Trying not to apply castings to these types, to keep them the same as # the original parameters, which could make swapping out different # precision libraries simpler? @@ -56,7 +56,7 @@ def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, self.mathSupport = mathSupport # These properties are attached to the mesh, provided by the DiveTimelineSpan that created them - self.escapeSquared = escapeSquared + self.escapeRadius = escapeRadius self.maxEscapeIterations = maxEscapeIterations self.shouldSmooth = shouldSmooth @@ -78,6 +78,9 @@ def generateMesh(self): return combinedMesh + def isUniform(self): + return self.realMeshGenerator and isinstance(self.realMeshGenerator, MeshGeneratorUniform) and self.imagMeshGenerator and isinstance(self.imagMeshGenerator, MeshGeneratorUniform) + def __repr__(self): return """\ [DiveMesh {{{mwidth},{mheight}}} realGenerator:{rgen} imagGenerator:{igen} ]\ diff --git a/fractalcache.py b/fractalcache.py index 8769d17..f876e89 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -14,116 +14,124 @@ from decimal import Decimal - CACHE_VER = 0.11 -FRACTL_CACHE_DIR = "./.fractal_cache/" - -class Frame: - def __init__(self, ver, cw, ch, center, julia_c, escape_r, m_iter, values, histogram): - self.ver = ver - self.cw = cw - self.ch = ch - self.center = center - self.julia_c = julia_c - self.escape_r = escape_r - self.m_iter = m_iter - self.values = values - self.histogram = histogram - - -class FractalCache: - - def __init__(self, mandl_ctx, root_dir = FRACTL_CACHE_DIR): - self.ctx = mandl_ctx - self.root_dir = FRACTL_CACHE_DIR - - self.subdir = None - - def create_subdir_name(self): - filename = self.root_dir + "/" - if not self.ctx.julia_c: - filename = filename + "mandelbrot/" - else: - filename = filename + "juliaset/" - - return filename + "/" + str(self.ctx.img_width)+"x"+str(self.ctx.img_height) - - def create_file_name(self): - ba = None - - ba = pickle.dumps(Frame(CACHE_VER, - self.ctx.cmplx_width, - self.ctx.cmplx_height, - self.ctx.cmplx_center, - self.ctx.julia_c, - self.ctx.escape_rad, - self.ctx.max_iter, - None, None - )) - - h = hmac.new(ba, digestmod=hashlib.sha1) - return h.hexdigest() - - def setup(self): - print("+ using cache %s" % (self.root_dir)) - - if not os.path.isdir(self.root_dir): - print(" -> directory not found ... creating ") - os.makedirs(self.root_dir,exist_ok=True) - - self.subdir = self.create_subdir_name() - if not os.path.isdir(self.subdir): - print(" -> subdir %s not found ... creating "%(self.subdir)) - os.makedirs(self.subdir,exist_ok=True) - - def check_cache(self): - - filename = self.subdir + "/" + self.create_file_name() - if not os.path.isfile(filename): - print(" -> cache file %s not found ... "%(filename)) - return False - return True - - def write_cache(self, values, histogram): - - filename = self.subdir + "/" + self.create_file_name() - print("+ writing frame to cache file %s ... "%(filename)) - frame = Frame(CACHE_VER, - self.ctx.cmplx_width, - self.ctx.cmplx_height, - self.ctx.cmplx_center, - self.ctx.julia_c, - self.ctx.escape_rad, - self.ctx.max_iter, - values, - histogram) +class FrameInfo: + def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + self.mesh_width = mesh_width + self.mesh_height = mesh_height + self.center = center + self.complex_real_width = complex_real_width + self.complex_imag_width = complex_imag_width + self.escape_r = escape_r + self.max_escape_iter = max_escape_iter - with open(filename, 'wb') as fd: - pickle.dump(frame,fd) + self.raw_values = raw_values + self.raw_histogram = raw_histogram + self.smooth_values = smooth_values + self.smooth_histogram = smooth_histogram - def read_cache(self): - - if not self.check_cache(): - return None, None + def emptyCopy(self): + return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter)) - filename = self.subdir + "/" + self.create_file_name() +class Frame: + """ + Two different results caches available, one for uniform meshes + at "", and a project-specific one for + non-uniform meshes at "_cache". + + The project-specific cache uses just frame numbers for cache file names. + The shared cache uses a hash of this frame's FrameInfo for cache file names. + + A shared results cache called 'shared_cache' would have directories like: + shared_cache/v_0.11/native/mandelbrot/deadbeef + shared_cache/v_0.11/flint/mandelbrot/deadbeef + shared_cache/v_0.11/native/julia/feedbabe + shared_cache/v_0.11/flint/julia/feedbabe + + And the project called 'demo1' would have non-uniform meshes cached at: + demo1/v_0.11/native/mandelbrot/123.pik + demo1/v_0.11/flint/mandelbrot/123.pik + demo1/v_0.11/native/julia/12.pik + demo1/v_0.11/flint/julia/13.pik + """ + def __init__(self, timeline, dive_mesh, frame_number=-1, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + self.cache_version = CACHE_VER + self.timeline = timeline + self.dive_mesh = dive_mesh + self.frame_number = frame_number + + self.frame_info = FrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.dive_mesh.escapeRadius, self.dive_mesh.maxEscapeIterations, raw_values, raw_histogram, smooth_values, smooth_histogram) + + def create_results_file_name(self): + return os.path.join(self.create_results_subpath(), self.create_results_file_identifier()) + + def create_results_file_identifier(self): + if self.dive_mesh.isUniform(): + ba = None + ba = pickle.dumps(self.frame_info.emptyCopy()) + h = hmac.new(ba, digestmod=hashlib.sha1) + return h.hexdigest() + else: + return u"%d.pik" % self.frame_number + + def create_results_subpath(self, mkdir_if_needed=False): + if self.dive_mesh.isUniform(): + root_cache_path = self.timeline.sharedCachePath + else: + root_cache_path = u"%s_cache" % self.timeline.projectFolderName + + version_subdir = u"v_%s" % str(self.cache_version) + results_subpath = os.path.join(root_cache_path, version_subdir, self.timeline.mathSupport.precisionType, self.timeline.fractalType) + if mkdir_if_needed and not os.path.exists(results_subpath): + os.makedirs(results_subpath) + return results_subpath + + def write_results_cache(self): + self.create_results_subpath(mkdir_if_needed=True) # Just for side-effect folder creation + + filename = self.create_results_file_name() + #print("+ writing frame to cache file %s ... "%(filename)) + + # Probably a mistake to write a no-data cache file, so panic. + if self.frame_info.raw_values is None or self.frame_info.smooth_values is None: + raise ValueError("Aborting cache file write of missing data to \"%s\"" % filename) - print("+ frame from cache file %s ... "%(filename)) - frame = None + with open(filename, 'wb') as fd: + pickle.dump(self.frame_info,fd) - with open(filename, 'rb') as fd: - frame = pickle.load(fd) + def read_results_cache(self): + filename = self.create_results_file_name() + if not os.path.exists(filename) or not os.path.isfile(filename): + return - assert frame.ver == CACHE_VER - assert frame.cw == self.ctx.cmplx_width - assert frame.ch == self.ctx.cmplx_height - assert frame.center == self.ctx.cmplx_center - assert frame.escape_r == self.ctx.escape_rad - assert frame.m_iter == self.ctx.max_iter + #print("+ frame from cache file %s ... "%(filename)) + frame_data = None - if frame.julia_c != self.ctx.julia_c : - print("** error: inconsistent cache %s:%s"%(str(frame.julia_c),str(self.ctx.julia_c))) - return None, None + with open(filename, 'rb') as fd: + frame_data = pickle.load(fd) + + assert frame_data.mesh_width == self.frame_info.mesh_width + assert frame_data.mesh_height == self.frame_info.mesh_height + assert frame_data.center == self.frame_info.center + assert frame_data.complex_real_width == self.frame_info.complex_real_width + assert frame_data.complex_imag_width == self.frame_info.complex_imag_width + assert frame_data.escape_r == self.frame_info.escape_r + assert frame_data.max_escape_iter == self.frame_info.max_escape_iter + + self.frame_info.raw_values = frame_data.raw_values + self.frame_info.raw_histogram = frame_data.raw_histogram + self.frame_info.smooth_values = frame_data.smooth_values + self.frame_info.smooth_histogram = frame_data.smooth_histogram + + return + + def remove_from_results_cache(self): + cache_file_name = self.create_results_file_name() + if os.path.exists(cache_file_name) and os.path.isfile(cache_file_name): + os.remove(cache_file_name) + self.raw_values = None + self.raw_histogram = None + self.smooth_values = None + self.smooth_histogram = None - return frame.values,frame.histogram diff --git a/fractalmath.py b/fractalmath.py index 3ba8854..b16e895 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -29,6 +29,8 @@ class DiveMathSupport: Trying to preserve types where possible, without forcing casting, because sometimes all the math operations will already work for custom numeric types. """ + def __init__(self): + self.precisionType = 'native' def createComplex(self, realComponent, imagComponent): """Compatible complex types will return values for .real() and .imag()""" @@ -200,31 +202,59 @@ def interpolateLinear(self, startX, startY, endX, endY, targetX): else: return (((targetX - startX)/ (endX - startX)) * (endY - startY)) + startY - def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): - z = self.createComplex(0, 0) + def mandelbrot(self, c, escapeRadius, maxIter): + """ + Now that smoothing is handled separately, the native python implementation + COULD work for flint as well, except it uses 'complex(0,0)' instead + of self.createComplex(0,0). + The reason for this is just as below - to reduce one extra function call in + the core calculation. + + Could just call julia with a zero start here, but seems wiser to + not have one extra function call in the core of the calculation? + """ + z = complex(0,0) n = 0 + while abs(z) <= escapeRadius and n < maxIter: + z = z*z + c + n += 1 + + return (n, z) + + def julia(self, c, z0, escapeRadius, maxIter): + """ + Some interesting c values + c = complex(-0.8, 0.156) + c = complex(-0.4, 0.6) + c = complex(-0.7269, 0.1889) - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - while ((z.real**2)+(z.imag**2)) <= escapeSquared and n < maxIter: + Looks like this implementation is able to handle flint types, now that smoothing + is handled separately. + + fabs(z) returns the modulus of a complex number, which is the + distance to 0 (on a 2x2 cartesian plane) + However, instead we just square both sides of the inequality to + avoid the sqrt + e.g.: while (float((z.real**2)+(z.imag**2))) <= escapeSquared and n < maxIter: + + However, for python, it looks like it takes almost 1/2 of the time to do abs(Z) + """ + z = z0 + n = 0 + while abs(z) <= escapeRadius and n < maxIter: z = z*z + c n += 1 -# - if n == maxIter: + + return (n, z) + + def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + if endingIter == maxIter: return maxIter - - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if shouldSmooth == True: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - mu = n + 1 - math.log(math.log(abs(z))) - return mu - else: - return n + else: + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + # Note: Results in a float. We think. + return endingIter + 1 - math.log(math.log2(abs(endingZ))) class DiveMathSupportFlint(DiveMathSupport): """ @@ -237,6 +267,7 @@ def __init__(self): self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) + self.precisionType = 'flint' def createComplex(self, realComponent, imagComponent): return self.flint.acb(realComponent, imagComponent) @@ -259,40 +290,37 @@ def interpolateLogTo(self, startX, startY, endX, endY, targetX): aVal = (endY - startY) / (self.flint.arb(endX - startX + 1).log()) return aVal * (self.flint.arb(targetX - startX + 1).log()) + startY - def mandelbrot(self, c, escapeSquared, maxIter, shouldSmooth=False): + def mandelbrot(self, c, escapeRadius, maxIter): """ - NOTE: Smoothing maybe should be only a post-processing step? Maybe not? - - This flint-specific implementation only really does flint-y logs in the smoothing. - The Decimal-specific implementation needed some extra steps, but I've ditched that one. + Now that smoothing is handled separately, the native python implementation + COULD work for flint as well, except it uses 'complex(0,0)' instead + of self.createComplex(0,0). + The reason for this is just as below - to reduce one extra function call in + the core calculation. + + Could just call julia with a zero start here, but seems wiser to + not have one extra function call in the core of the calculation? """ - z = self.createComplex(0, 0) + z = self.flint.acb(0,0) n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - # - # Important to cast the escape answer back to float, or the arb gets sheared bizarrely - while (float((z.real**2)+(z.imag**2))) <= escapeSquared and n < maxIter: + while abs(z) <= escapeRadius and n < maxIter: z = z*z + c n += 1 - if n == maxIter: + return (n, z) + + + def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + """ + This flint-specific implementation only really does flint-y logs in the smoothing. + The Decimal-specific implementation needed some extra steps, but I've ditched that one. + """ + if endingIter == maxIter: return maxIter - - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if shouldSmooth == True: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - - #mu = n + 1 - math.log(self.mp.log2(abs(z))) # (and, there IS NO log2 in mpmath... hrm) - # Maybe this is the right way to do the same thing?! - mu = n + 1 - z.abs_lower().log().log() - return mu - else: - return n + else: + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + # Note: Results in a float. We think. + return endingIter + 1 - endingZ.abs_lower().const_log2().const_log10() + diff --git a/mandelbrot.py b/mandelbrot.py index 6544da5..1aca6de 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -62,29 +62,19 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.cmplx_width = self.math_support.createFloat(0.0) self.cmplx_height = self.math_support.createFloat(0.0) - - # point we're going to dive into self.cmplx_center = self.math_support.createComplex(0.0, 0.0) # center of image in complex plane self.max_iter = 0 # int max iterations before bailing self.escape_rad = 0. # float radius mod Z hits before it "escapes" - self.escape_squared = 0.0 # float squared radius, (redundant, but helpful) self.scaling_factor = 0.0 # float amount to zoom each epoch - self.num_epochs = 0 # int, nuber of epochs into the dive self.set_zoom_level = 0 # Zoom in prior to the dive self.smoothing = False # bool turn on color smoothing self.snapshot = False # Generate a single, high res shotb - self.duration = 0 # int duration of clip in seconds - self.fps = 0 # int number of frames per second - - self.julia_c = None - self.julia_orig = None - self.julia_walk_c = None - + self.fractal = 'mandelbrot' # or 'julia' self.julia_list = None self.palette = None @@ -95,155 +85,32 @@ def __init__(self, math_support=fm.DiveMathSupport()): # query it for frame-specific parameters to render with. self.timeline = None - self.project_name = "default_project" - self.cache_path = u"%s_cache" % self.project_name + self.project_name = 'default_project' + self.shared_cache_path = 'shared_cache' self.build_cache = False self.invalidate_cache = False - self.ver = MANDL_VER # used to version cash - - self.cache = None self.verbose = 0 # how much to print about progress - def zoom_in(self, iterations=1): - while iterations: - self.cmplx_width *= self.scaling_factor - self.cmplx_height *= self.scaling_factor - self.num_epochs += 1 - iterations -= 1 - - # Use Bresenham's line drawing algo for a simple walk between two - # complex points - def julia_walk(self, t): - - # duration of a leg - leg_d = float(self.duration) / float(len(self.julia_list) - 1) - # which leg are we walking? - leg = math.floor(float(t) / leg_d) - # how far along are we on that leg? - timeslice = float(self.duration) / (float(self.duration) * float(self.fps)) - fraction = (float(t) - (float(leg) * leg_d)) / (leg_d - timeslice) - - #print("T %f Leg %d leg_d %d Fraction %f"%(t,leg,leg_d,fraction)) - - cp1 = self.julia_list[leg] - cp2 = self.julia_list[leg + 1] - - - if self.julia_orig != cp1: - self.julia_orig = cp1 - - x0 = self.julia_orig.real - x1 = cp2.real - y0 = self.julia_orig.imag - y1 = cp2.imag - - new_x = ((x1 - x0)*fraction) + x0 - new_y = ((y1 - y0)*fraction) + y0 - self.julia_c = complex(new_x, new_y) - - - # Some interesting c values - # c = complex(-0.8, 0.156) - # c = complex(-0.4, 0.6) - # c = complex(-0.7269, 0.1889) - - def julia(self, c, z0): - z = z0 - n = 0 - while abs(z) <= 2 and n < self.max_iter: - z = z*z + c - n += 1 - - if n == self.max_iter: - return self.max_iter - - return n + 1 - math.log(math.log2(abs(z))) - - def mandelbrot(self, c): - z = self.math_support.createComplex(0, 0) - n = 0 - - # fabs(z) returns the modulus of a complex number, which is the - # distance to 0 (on a 2x2 cartesian plane) - # - # However, instead we just square both sides of the inequality to - # avoid the sqrt - while ((z.real*z.real)+(z.imag*z.imag)) <= self.escape_squared and n < self.max_iter: - z = z*z + c - n += 1 - - if n >= self.max_iter: - return self.max_iter - - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - if self.smoothing: - z = z*z + c; n+=1 # a couple extra iterations helps - z = z*z + c; n+=1 # decrease the size of the error - mu = n + 1 - math.log(math.log(abs(z))) - return mu - else: - return n - - def load_frame(self, frame_number, cache_path, build_cache = True, invalidate_cache = False): - """Cache-aware data tuple loading or calculating""" - - cache_file_name = os.path.join(cache_path, u"%d.npy" % frame_number) - #print("cache file %s" % cache_file_name) - - if invalidate_cache == True and os.path.exists(cache_file_name) == True and os.path.isfile(cache_file_name): - os.remove(cache_file_name) - - frame_data = np.zeros((1,1), dtype=np.uint8) - if os.path.exists(cache_file_name) == False: - frame_data = self.calculate_epoch_data(frame_number) - - if build_cache == True: - #print("Writing cache file") - if not os.path.exists(cache_path): - os.makedirs(cache_path) - - np.save(cache_file_name, frame_data) - else: - #print("Loading cache file") - frame_data = np.load(cache_file_name, allow_pickle=True) - - return frame_data - def render_frame_number(self, frame_number, snapshot_filename=None): """ Load or calculate the frame data. Once we have frame data, can perform histogram and coloring """ - (pixel_values_2d, frame_metadata) = self.timeline.loadResultsForFrameNumber(frame_number, build_cache=self.build_cache, cache_path=self.cache_path, invalidate_cache=self.invalidate_cache) + (pixel_values_2d_raw, hist_raw, pixel_values_2d_smooth, hist_smooth, frame_metadata) = self.timeline.loadResultsForFrameNumber(frame_number, buildCache=self.build_cache, invalidateCache=self.invalidate_cache) # Capturing the transpose of our array, because it looks like I mixed # up rows and cols somewhere along the way. - pixel_values_2d = pixel_values_2d.T - - # Used to create a histogram of the frequency of iteration - # depths retured by the mandelbrot calculation. Helpful for - # color selection since many iterations never come up so you - # loose fidelity by not focusing on those heavily used - - hist = defaultdict(int) - values = np.zeros((self.img_width, self.img_height), dtype=np.uint8) + if self.smoothing == True: + pixel_values_2d = pixel_values_2d_smooth.T + hist = hist_smooth + else: + pixel_values_2d = pixel_values_2d_raw.T + hist = hist_raw #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - # -- - # Iterate over every point in the complex plane (1:1 mapping per - # pixel) and run the fractacl calculation. We save the output in - # a 2x2 array, and also create the histogram of values - # -- - - for x in range(0, self.img_width): - for y in range(0, self.img_height): - if pixel_values_2d[x,y] < self.max_iter: - hist[math.floor(pixel_values_2d[x,y])] += 1 - total = sum(hist.values()) hues = [] h = 0 @@ -271,47 +138,6 @@ def render_frame_number(self, frame_number, snapshot_filename=None): return im.save(snapshot_filename,"gif") else: return np.array(im) - - def calc_cur_frame(self, snapshot_filename = None): - - # -- - # Calculate box in complex plane from center point - # -- - - re_start = self.math_support.createFloat(self.cmplx_center.real - (self.cmplx_width / 2.)) - re_end = self.math_support.createFloat(self.cmplx_center.real + (self.cmplx_width / 2.)) - - im_start = self.math_support.createFloat(self.cmplx_center.imag - (self.cmplx_height / 2.)) - im_end = self.math_support.createFloat(self.cmplx_center.imag + (self.cmplx_height / 2.)) - - if self.verbose > 0: - print("MandlContext starting epoch %d re range %f %f im range %f %f center %f + %f i .... " %\ - (self.num_epochs, re_start, re_end, im_start, im_end, self.cmplx_center.real, self.cmplx_center.imag), - end = " ") - - values = np.zeros((self.img_width, self.img_height), dtype=np.uint8) - hist = [] - for x in range(0, self.img_width): - for y in range(0, self.img_height): - # map from pixels to complex coordinates - Re_x = re_start + (x / (self.img_width - 1)) * (re_end - re_start) - Im_y = im_start + (y / (self.img_height - 1)) * (im_end - im_start) - - c = self.math_support.createComplex(Re_x, Im_y) - - if not self.julia_c: - m = self.mandelbrot(c) - else: - z0 = c - m = self.julia(self.julia_c, z0) - - values[x,y] = m - - if m < self.max_iter: - hist[math.floor(m)] += 1 - - return values, hist - def draw_image_PIL(self, values, hues, metadata=None): @@ -339,7 +165,7 @@ def draw_image_PIL(self, values, hues, metadata=None): #print("Finished iteration IMrange %f:%f (im height: %f)"%(IM_START, IM_END, IM_END - IM_START)) if self.burn_in == True and metadata != None: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (metadata['frame_number'], metadata['mesh_center'], metadata['real_width'], metadata['imag_width']) + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (metadata['frame_number'], metadata['mesh_center'], metadata['complex_real_width'], metadata['complex_imag_width']) burn_in_location = (10,10) burn_in_margin = 5 @@ -350,82 +176,21 @@ def draw_image_PIL(self, values, hues, metadata=None): return im - def next_epoch(self, t, snapshot_filename = None): - """Called for each frame of the animation. Will calculate - current view, and then zoom in""" - - values, hist = None, None - if self.cache: - values, hist = self.cache.read_cache() - - if not values or not hist: - # call primary calculation function - values, hist = self.calc_cur_frame(snapshot_filename) - - if self.cache: - self.cache.write_cache(values, hist) - - - #- - # From histogram normalize to percent-of-total. This is - # effectively a probability distribution of escape values - # - # Note that this is not effecitly a probability distribution for - # a given escape value. We can use this to calculate the Shannon - - total = sum(hist.values()) - hues = [] - h = 0 - - for i in range(self.max_iter): - if total : - h += hist[i] / total - hues.append(h) - hues.append(h) - - - # -- - # Create image for this frame - # -- - - im = self.draw_image_PIL(values, hues) - - # -- - # Do next step in animation - # -- - - if self.julia_list: - self.julia_walk(t) - else: - self.zoom_in() - - if self.verbose > 0: - print("Done]") - - if snapshot_filename: - return im.save(snapshot_filename,"gif") - else: - return np.array(im) - def __repr__(self): return """\ [MandlContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:s} -Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Smoothing:{sm:b} Epochs:{e:d} Max iter:{mx:d}]\ +Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Smoothing:{sm:b} Max iter:{mx:d}]\ """.format( w=self.img_width,h=self.img_height,cw=str(self.cmplx_width),ch=str(self.cmplx_height), - cc=str(self.cmplx_center),s=self.scaling_factor,e=self.num_epochs,mx=self.max_iter,sm=self.smoothing); + cc=str(self.cmplx_center),s=self.scaling_factor,mx=self.max_iter,sm=self.smoothing); class MediaView: """ Handle displaying to gif / mp4 / screen etc. """ - def make_frame(self, t): - if self.use_epochs == True: - return self.ctx.next_epoch(t) - else: - return self.ctx.render_frame_number(self.frame_number_from_time(t)) + return self.ctx.render_frame_number(self.frame_number_from_time(t)) def __init__(self, duration, fps, ctx): self.duration = duration @@ -434,11 +199,6 @@ def __init__(self, duration, fps, ctx): self.banner = False self.vfilename = None - self.ctx.duration = duration - self.ctx.fps = fps - - self.use_epochs = True - def frame_number_from_time(self, t): return math.floor(self.fps * t) @@ -476,9 +236,6 @@ def setup(self): print(self) print(self.ctx) - if self.ctx.cache: - self.ctx.cache.setup() - def run(self): @@ -532,13 +289,40 @@ def construct_simple_timeline(self): end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(start_width_real, overall_zoom_factor, frame_count - 1) end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(start_width_imag, overall_zoom_factor, frame_count - 1) - timeline = DiveTimeline(projectFolderName=self.ctx.project_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support) - # Here, I have ctx, which should know escapeSquared, maxIter, and shouldSmooth... - #print("Trying to make span of %d frames" % frame_count) - span = timeline.addNewSpanAtEnd(frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_squared, self.ctx.max_iter, self.ctx.smoothing) + if self.ctx.fractal == 'julia': + timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='julia', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) + # Just evenly divide the waypoints across the time for a simple timeline + keyframeCount = len(self.ctx.julia_list) + # 2 keyframes over 10 frames = 10 frames per keyframe + keyframeSpacing = math.floor(frame_count / (keyframeCount - 1)) + if keyframeSpacing < 1: + raise ValueError("Can't construct julia walk with more waypoints than animation frames") + + span = DiveTimelineSpan(timeline, frame_count, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + span.addNewWindowKeyframe(0, start_width_real, start_width_imag) + span.addNewWindowKeyframe(frame_count - 1, end_width_real, end_width_imag) + span.addNewUniformKeyframe(0) + span.addNewUniformKeyframe(frame_count - 1) + + currKeyframeFrameNumber = 0 + for currJuliaCenter in self.ctx.julia_list: + # Recognize when we're at the last item, and jump that keyframe to the final frame + if currKeyframeFrameNumber + keyframeSpacing > frame_count - 1: + currKeyframeNumber = frame_count - 1 + + span.addNewCenterKeyframe(currKeyframeFrameNumber, currJuliaCenter, transitionIn='linear', transitionOut='linear') + currKeyframeFrameNumber += keyframeSpacing + + timeline.timelineSpans.append(span) - #perspectiveFrame = math.floor(frame_count * .5) - #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) + else: + timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='mandelbrot', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) + # Here, I have ctx, which should know escapeRadius, maxIter, and shouldSmooth... + #print("Trying to make span of %d frames" % frame_count) + span = timeline.addNewSpanAtEnd(frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + + #perspectiveFrame = math.floor(frame_count * .5) + #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) return timeline @@ -571,9 +355,15 @@ class DiveTimeline: currently all live on integer frame numbers. """ - def __init__(self, projectFolderName, framerate, frameWidth, frameHeight, mathSupport): + def __init__(self, projectFolderName, fractal, framerate, frameWidth, frameHeight, mathSupport, sharedCachePath): + self.projectFolderName = projectFolderName - self.cachePath = os.path.join("%s_cache" % projectFolderName) + self.sharedCachePath = sharedCachePath + + fractalOptions = ['mandelbrot', 'julia'] + if fractal not in fractalOptions: + raise ValueError("fractal must be one of (%s)" % ", ".join(fractalOptions)) + self.fractalType = fractal self.framerate = float(framerate) self.frameWidth = int(frameWidth) @@ -584,7 +374,7 @@ def __init__(self, projectFolderName, framerate, frameWidth, frameHeight, mathSu # No definition made yet for edit gaps, so let's just enforce adjacency of ranges for now. self.timelineSpans = [] - def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag, escapeSquared, maxEscapeIterations, shouldSmooth): + def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag, escapeRadius, maxEscapeIterations, shouldSmooth): """ Constructs a new span, and adds it to the end of the existing span list @@ -594,7 +384,7 @@ def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, en Apparently also adding perspective keyframes too. """ - span = DiveTimelineSpan(self, frameCount, escapeSquared, maxEscapeIterations, shouldSmooth) + span = DiveTimelineSpan(self, frameCount, escapeRadius, maxEscapeIterations, shouldSmooth) span.addNewCenterKeyframe(0, center, 'quadratic-to', 'quadratic-to') span.addNewCenterKeyframe(frameCount - 1, center, 'quadratic-to', 'quadratic-to') span.addNewWindowKeyframe(0, startWidthReal, startWidthImag) @@ -628,63 +418,70 @@ def getSpanForFrameNumber(self, frameNumber): return None # Went past the end without finding a valid span, so it's too high a frame number - def loadResultsForFrameNumber(self, frame_number, build_cache=True, cache_path="cache", invalidate_cache=False): - """Cache-aware data tuple loading or calculating""" - - cache_file_name = os.path.join(cache_path, u"%d.npy" % frame_number) - cache_metadata_file_name = os.path.join(cache_path, u"%d.npy.meta" % frame_number) - #print("cache file %s" % cache_file_name) - - if invalidate_cache == True and os.path.exists(cache_file_name) == True and os.path.isfile(cache_file_name): - os.remove(cache_file_name) - os.remove(cache_metadata_file_name) - - frame_data = np.zeros((1,1), dtype=np.uint8) - frame_metadata = {} - if os.path.exists(cache_file_name) == False: - #print("Calculating epoch results") - (frame_data, frame_metadata) = self.calculateResultsForFrameNumber(frame_number) - - if build_cache == True: - #print("Writing cache file") - if not os.path.exists(cache_path): - os.makedirs(cache_path) - - # Write 2 separate files out, the numpy array, and a metadata sidecar - np.save(cache_file_name, frame_data) - with open(cache_metadata_file_name, 'wb') as metadataHandle: - pickle.dump(frame_metadata, metadataHandle) - else: - #print("Loading cache file") - # Load both the numpy array, and the metadata sidecar - frame_data = np.load(cache_file_name, allow_pickle=True) - with open(cache_metadata_file_name, 'rb') as metadataReadHandle: - frame_metadata = pickle.load(metadataReadHandle) - return (frame_data, frame_metadata) - - def calculateResultsForFrameNumber(self, frameNumber): + def loadResultsForFrameNumber(self, frameNumber, buildCache=True, invalidateCache=False): + """ Multi-cache-aware data loading or calculating """ diveMesh = self.getMeshForFrame(frameNumber) + cacheFrame = fc.Frame(self, diveMesh, frameNumber) + + if invalidateCache == True: + cacheFrame.remove_from_results_cache() + + cacheFrame.read_results_cache() + + if cacheFrame.frame_info.raw_values is None or cacheFrame.frame_info.raw_histogram is None or cacheFrame.frame_info.smooth_values is None or cacheFrame.frame_info.smooth_histogram is None: + #print("+ calculating epoch results") + (rawValues, rawHistogram, smoothValues, smoothHistogram) = self.calculateResultsForDiveMesh(diveMesh) + cacheFrame.frame_info.raw_values = rawValues + cacheFrame.frame_info.raw_histogram = rawHistogram + cacheFrame.frame_info.smooth_values = smoothValues + cacheFrame.frame_info.smooth_histogram = smoothHistogram + + # Fresly calculated results get saved if we're building the cache + if buildCache == True: + cacheFrame.write_results_cache() + + frame_metadata = {'frame_number' : frameNumber, + 'fractal_type': self.fractalType, + 'precision_type': self.mathSupport.precisionType, + 'mesh_center': str(diveMesh.center), + 'complex_real_width' : str(diveMesh.realMeshGenerator.baseWidth), + 'complex_imag_width' : str(diveMesh.imagMeshGenerator.baseWidth), + 'escape_radius' : str(diveMesh.escapeRadius), + 'mesh_is_uniform' : str(diveMesh.isUniform()), + 'max_escape_iterations' : str(diveMesh.maxEscapeIterations)} + + return (cacheFrame.frame_info.raw_values, cacheFrame.frame_info.raw_histogram, cacheFrame.frame_info.smooth_values, cacheFrame.frame_info.smooth_histogram, frame_metadata) + + def calculateResultsForDiveMesh(self, diveMesh): mesh = diveMesh.generateMesh() show_row_progress = False - pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint8) + pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=object) + pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=object) + hist = defaultdict(int) + hist_smoothed = defaultdict(int) for x in range(0, mesh.shape[0]): for y in range(0, mesh.shape[1]): - pixel_values_2d[x,y] = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeSquared, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) + if self.fractalType == 'julia': + (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) + else: # self.FractalType == 'mandelbrot' + (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) + + pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) + + # Extra casts to make flint types behave + if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) + hist[int(float(self.mathSupport.floor(pixel_values_2d[x,y])))] += 1 + if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: + hist_smoothed[int(float(self.mathSupport.floor(pixel_values_2d_smoothed[x,y])))] += 1 + if show_row_progress == True: print("%d-" % x, end="") sys.stdout.flush() - frame_metadata = {'frame_number' : frameNumber, - 'mesh_center': str(diveMesh.center), - 'real_width' : str(diveMesh.realMeshGenerator.baseWidth), - 'imag_width' : str(diveMesh.imagMeshGenerator.baseWidth), - 'escape_squared' : str(diveMesh.escapeSquared), - 'max_escape_iterations' : str(diveMesh.maxEscapeIterations)} - - return (pixel_values_2d, frame_metadata) - + return (pixel_values_2d, hist, pixel_values_2d_smoothed, hist_smoothed) #### # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here @@ -700,7 +497,7 @@ def calculateResultsForFrameNumber(self, frameNumber): # # # Pretty sure this is mistakenly doing an n! pass, or something just as ridiculous. # #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) -# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeSquared, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) +# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeRadius, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) # # pixel_values_2d = pixel_values_1d.reshape((mesh.shape[0], mesh.shape[1])) # #pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level @@ -786,7 +583,7 @@ def getMeshForFrame(self, frameNumber): # Passing lots into the dive mesh. Notably, some info about the DiveTimelineSpan that was # responsible for creating this mesh. Might want to store an actual reference to the object, but # doesn't seem needed yet? - diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeSquared, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) + diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeRadius, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) #print (diveMesh) return diveMesh @@ -864,11 +661,11 @@ class DiveTimelineSpan: """ # ?(Can be used to observe/calculate n-1 zoom factors.)? """ - def __init__(self, timeline, frameCount, escapeSquared, maxEscapeIterations, shouldSmooth): + def __init__(self, timeline, frameCount, escapeRadius, maxEscapeIterations, shouldSmooth): self.timeline = timeline self.frameCount = int(frameCount) - self.escapeSquared = escapeSquared + self.escapeRadius = escapeRadius self.maxEscapeIterations = maxEscapeIterations self.shouldSmooth = shouldSmooth @@ -1263,7 +1060,7 @@ def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', # command line # -- def set_demo1_params(mandl_ctx, view_ctx): - print("+ Running in demo mode - loading default params") + print("+ Running in demo mode - loading default mandelbrot dive params") mandl_ctx.img_width = 1024 mandl_ctx.img_height = 768 @@ -1279,18 +1076,16 @@ def set_demo1_params(mandl_ctx, view_ctx): mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_real_str, center_imag_str) mandl_ctx.project_name = 'demo1' - mandl_ctx.cache_path = u"%s_cache" % mandl_ctx.project_name mandl_ctx.scaling_factor = .90 mandl_ctx.max_iter = 255 - #mandl_ctx.escape_rad = 4. - mandl_ctx.escape_rad = 32768. - mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad + mandl_ctx.escape_rad = 2. + #mandl_ctx.escape_rad = 32768. mandl_ctx.verbose = 3 - mandl_ctx.burn_in = True + mandl_ctx.burn_in = False mandl_ctx.build_cache=True view_ctx.duration = 2.0 @@ -1299,8 +1094,40 @@ def set_demo1_params(mandl_ctx, view_ctx): view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 29.97 / 2.0 - # Separate execution paths for now - view_ctx.use_epochs = False +def set_julia_walk_demo1_params(mandl_ctx, view_ctx): + print("+ Running in demo mode - loading default julia walk params") + mandl_ctx.img_width = 1024 + mandl_ctx.img_height = 768 + + cmplx_width_str = '3.2' + cmplx_height_str = '2.5' + mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(cmplx_width_str) + mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(cmplx_height_str) + + mandl_ctx.fractal = 'julia' + mandl_ctx.julia_list = [mandl_ctx.math_support.createComplex(0.355,0.355), mandl_ctx.math_support.createComplex(0.0,0.8), mandl_ctx.math_support.createComplex(0.3355,0.355)] + + mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(0,0) + + mandl_ctx.project_name = 'julia_demo1' + + mandl_ctx.scaling_factor = 1.0 + + mandl_ctx.max_iter = 255 + + mandl_ctx.escape_rad = 2. + #mandl_ctx.escape_rad = 32768. + + mandl_ctx.verbose = 3 + mandl_ctx.burn_in = True + mandl_ctx.build_cache = True + + view_ctx.duration = 2.0 + #view_ctx.duration = 0.25 + + # FPS still isn't set quite right, but we'll get it there eventually. + view_ctx.fps = 23.976 / 2.0 + #view_ctx.fps = 29.97 / 2.0 def set_preview_mode(mandl_ctx, view_ctx): print("+ Running in preview mode ") @@ -1315,7 +1142,6 @@ def set_preview_mode(mandl_ctx, view_ctx): #mandl_ctx.escape_rad = 4. mandl_ctx.escape_rad = 32768. - mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad view_ctx.duration = 4 view_ctx.fps = 4 @@ -1339,7 +1165,6 @@ def set_snapshot_mode(mandl_ctx, view_ctx, snapshot_filename='snapshot.gif'): #mandl_ctx.escape_rad = 4. mandl_ctx.escape_rad = 32768. - mandl_ctx.escape_squared = mandl_ctx.escape_rad * mandl_ctx.escape_rad view_ctx.duration = 0 view_ctx.fps = 0 @@ -1351,7 +1176,9 @@ def parse_options(mandl_ctx, view_ctx): opts, args = getopt.getopt(argv, "pd:m:s:f:z:w:h:c:", ["preview", "demo", + "demo-julia-walk", "duration=", + "fps=", "max-iter=", "img-w=", "img-h=", @@ -1361,11 +1188,9 @@ def parse_options(mandl_ctx, view_ctx): "scaling-factor=", "snapshot=", "zoom=", - "fps=", "gif=", "mpeg=", "verbose=", - "julia=", "julia-walk=", "center=", "palette-test=", @@ -1373,10 +1198,10 @@ def parse_options(mandl_ctx, view_ctx): "burn", "flint", "project-name=", + "shared-cache-path=", "build-cache", "invalidate-cache", "banner", - "cache", "smooth"]) # Math support as to be handled first, so other parameter @@ -1392,11 +1217,14 @@ def parse_options(mandl_ctx, view_ctx): set_snapshot_mode(mandl_ctx, view_ctx, arg) elif opt in ['--demo']: set_demo1_params(mandl_ctx, view_ctx) + elif opt in ['--demo-julia-walk']: + set_julia_walk_demo1_params(mandl_ctx, view_ctx) for opt, arg in opts: if opt in ['-d', '--duration']: view_ctx.duration = float(arg) - mandl_ctx.duration = float(arg) + elif opt in ['-f', '--fps']: + view_ctx.fps = float(arg) elif opt in ['-m', '--max-iter']: mandl_ctx.max_iter = int(arg) elif opt in ['-w', '--img-w']: @@ -1404,30 +1232,27 @@ def parse_options(mandl_ctx, view_ctx): elif opt in ['-h', '--img-h']: mandl_ctx.img_height = int(arg) elif opt in ['--cmplx-w']: - mandl_ctx.cmplx_width = float(arg) + mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(arg) elif opt in ['--cmplx-h']: - mandl_ctx.cmplx_height = float(arg) + mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(arg) elif opt in ['-c', '--center']: mandl_ctx.cmplx_center= mandl_ctx.math_support.createComplex(arg) elif opt in ['--scaling-factor']: mandl_ctx.scaling_factor = float(arg) elif opt in ['-z', '--zoom']: mandl_ctx.set_zoom_level = int(arg) - elif opt in ['-f', '--fps']: - view_ctx.fps = int(arg) - mandl_ctx.fps = int(arg) elif opt in ['--smooth']: mandl_ctx.smoothing = True - elif opt in ['--julia']: - mandl_ctx.julia_c = complex(arg) - elif opt in ['--cache']: - mandl_ctx.cache = fc.FractalCache(mandl_ctx) - elif opt in ['--julia-walk']: - mandl_ctx.julia_list = eval(arg) # expects a list of complex numbers - if len(mandl_ctx.julia_list) <= 1: + elif opt in ['--julia-list']: + mandl_ctx.fractal = 'julia' + raw_julia_list = eval(arg) # expects a list of complex numbers + if len(raw_julia_list) <= 1: print("Error: List of complex numbers for Julia walk must be at least two points") sys.exit(0) - mandl_ctx.julia_c = mandl_ctx.julia_list[0] + julia_list = [] + for currCenter in raw_julia_list: + julia_list.append(mandl_ctx.math_support.create_complex(currCenter)) + mandl_ctx.julia_list = julia_list elif opt in ['--center']: mandl_ctx.cmplx_center = complex(arg) elif opt in ['--palette-test']: @@ -1463,7 +1288,8 @@ def parse_options(mandl_ctx, view_ctx): mandl_ctx.burn_in = True elif opt in ['--project-name']: mandl_ctx.project_name = arg - mandl_ctx.cache_path = u"%s_cache" % mandl_ctx.project_name + elif opt in ['--shared-cache-path']: + mandl_ctx.shared_cache_path = arg elif opt in ['--build-cache']: mandl_ctx.build_cache = True elif opt in ['--invalidate-cache']: From 63410b80d8a1fdb28546e8c132664697a7f5460c Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Fri, 23 Jul 2021 01:09:48 -0700 Subject: [PATCH 10/44] Trying to make types behave enough that caching for flint types works acceptably. --- clint_runscript.sh | 2 +- fractalcache.py | 21 +++++++++++++-------- fractalmath.py | 8 ++++---- mandelbrot.py | 13 ++++++------- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 4662c53..7ec0a80 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -2,7 +2,7 @@ python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo #python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache -#python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint +#$python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache #python3 mandelbrot.py --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk diff --git a/fractalcache.py b/fractalcache.py index f876e89..82531cb 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -32,7 +32,12 @@ def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_ self.smooth_histogram = smooth_histogram def emptyCopy(self): + """ Looks like storing strings of everything makes us pickle-proof? """ return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter)) + #return FrameInfo(self.mesh_width, self.mesh_height, self.center, self.complex_real_width, self.complex_imag_width, self.escape_r, self.max_escape_iter) + + def pickleCopy(self): + return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) class Frame: """ @@ -98,7 +103,7 @@ def write_results_cache(self): raise ValueError("Aborting cache file write of missing data to \"%s\"" % filename) with open(filename, 'wb') as fd: - pickle.dump(self.frame_info,fd) + pickle.dump(self.frame_info.pickleCopy(),fd) def read_results_cache(self): filename = self.create_results_file_name() @@ -111,13 +116,13 @@ def read_results_cache(self): with open(filename, 'rb') as fd: frame_data = pickle.load(fd) - assert frame_data.mesh_width == self.frame_info.mesh_width - assert frame_data.mesh_height == self.frame_info.mesh_height - assert frame_data.center == self.frame_info.center - assert frame_data.complex_real_width == self.frame_info.complex_real_width - assert frame_data.complex_imag_width == self.frame_info.complex_imag_width - assert frame_data.escape_r == self.frame_info.escape_r - assert frame_data.max_escape_iter == self.frame_info.max_escape_iter +# assert frame_data.mesh_width == self.frame_info.mesh_width +# assert frame_data.mesh_height == self.frame_info.mesh_height +# assert frame_data.center == self.frame_info.center +# assert frame_data.complex_real_width == self.frame_info.complex_real_width +# assert frame_data.complex_imag_width == self.frame_info.complex_imag_width +# assert frame_data.escape_r == self.frame_info.escape_r +# assert frame_data.max_escape_iter == self.frame_info.max_escape_iter self.frame_info.raw_values = frame_data.raw_values self.frame_info.raw_histogram = frame_data.raw_histogram diff --git a/fractalmath.py b/fractalmath.py index b16e895..c584971 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -215,7 +215,7 @@ def mandelbrot(self, c, escapeRadius, maxIter): """ z = complex(0,0) n = 0 - while abs(z) <= escapeRadius and n < maxIter: + while abs(z) <= escapeRadius and n < maxIter: z = z*z + c n += 1 @@ -241,7 +241,7 @@ def julia(self, c, z0, escapeRadius, maxIter): """ z = z0 n = 0 - while abs(z) <= escapeRadius and n < maxIter: + while abs(z) <= escapeRadius and n < maxIter: z = z*z + c n += 1 @@ -303,7 +303,7 @@ def mandelbrot(self, c, escapeRadius, maxIter): """ z = self.flint.acb(0,0) n = 0 - while abs(z) <= escapeRadius and n < maxIter: + while float(z.abs_lower()) <= escapeRadius and n < maxIter: z = z*z + c n += 1 @@ -321,6 +321,6 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. - return endingIter + 1 - endingZ.abs_lower().const_log2().const_log10() + return float(endingIter + 1 - endingZ.abs_lower().const_log2().const_log10()) diff --git a/mandelbrot.py b/mandelbrot.py index 1aca6de..53dbbda 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -457,8 +457,8 @@ def calculateResultsForDiveMesh(self, diveMesh): show_row_progress = False - pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=object) - pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=object) + pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) + pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) hist = defaultdict(int) hist_smoothed = defaultdict(int) for x in range(0, mesh.shape[0]): @@ -470,12 +470,12 @@ def calculateResultsForDiveMesh(self, diveMesh): pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) - # Extra casts to make flint types behave + # Not using mathSupport's floor() here, because it should just be a normal-scale float if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) - hist[int(float(self.mathSupport.floor(pixel_values_2d[x,y])))] += 1 + hist[math.floor(pixel_values_2d[x,y])] += 1 if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: - hist_smoothed[int(float(self.mathSupport.floor(pixel_values_2d_smoothed[x,y])))] += 1 + hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 if show_row_progress == True: print("%d-" % x, end="") @@ -1085,7 +1085,7 @@ def set_demo1_params(mandl_ctx, view_ctx): #mandl_ctx.escape_rad = 32768. mandl_ctx.verbose = 3 - mandl_ctx.burn_in = False + mandl_ctx.burn_in = True mandl_ctx.build_cache=True view_ctx.duration = 2.0 @@ -1123,7 +1123,6 @@ def set_julia_walk_demo1_params(mandl_ctx, view_ctx): mandl_ctx.build_cache = True view_ctx.duration = 2.0 - #view_ctx.duration = 0.25 # FPS still isn't set quite right, but we'll get it there eventually. view_ctx.fps = 23.976 / 2.0 From 6775a4632454b7d832f0b68f876a7e5644d16744 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Fri, 23 Jul 2021 15:46:02 -0700 Subject: [PATCH 11/44] Was stomping center param a second time. Still not all instantiation for flint though. --- clint_runscript.sh | 9 +++++++++ mandelbrot.py | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 7ec0a80..a88323c 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -7,3 +7,12 @@ python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo #python3 mandelbrot.py --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk #python3 mandelbrot.py --gif=native_julia_demo_fresh.gif --color=exp2 --demo-julia-walk --invalidate-cache + +# Let's test out range overlaps for separate invocations. +# If the floats are reliable enough, then the middle (overlapping) frames shouldn't have +# to render on the second invocation. + + +#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='0.5' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.9 --zoom=5 --build-cache +#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='0.5' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.9 --zoom=0 --build-cache + diff --git a/mandelbrot.py b/mandelbrot.py index 53dbbda..af48874 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -1252,8 +1252,6 @@ def parse_options(mandl_ctx, view_ctx): for currCenter in raw_julia_list: julia_list.append(mandl_ctx.math_support.create_complex(currCenter)) mandl_ctx.julia_list = julia_list - elif opt in ['--center']: - mandl_ctx.cmplx_center = complex(arg) elif opt in ['--palette-test']: m = fp.MandlPalette() if str(arg) == "gauss": From 2d1428a0776e55ce64bdc29b2979e736a5386398 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 24 Jul 2021 18:57:19 -0700 Subject: [PATCH 12/44] added start and end frame parameters, added a likely-short-lived gmp math implementation, tried to fix some string-based complex parse and load issues. --- clint_runscript.sh | 15 +++- fractalcache.py | 8 ++ fractalmath.py | 198 +++++++++++++++++++++++++++++++++++++++++++-- mandelbrot.py | 99 ++++++++++++++++------- 4 files changed, 280 insertions(+), 40 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index a88323c..7e91625 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -2,9 +2,12 @@ python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo #python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache -#$python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint +#python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache +#python3 mandelbrot.py --gif=gmp_demo.gif --color=exp2 --demo --gmp +#python3 mandelbrot.py --gif=gmp_demo_fresh.gif --color=exp2 --demo --gmp --invalidate-cache + #python3 mandelbrot.py --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk #python3 mandelbrot.py --gif=native_julia_demo_fresh.gif --color=exp2 --demo-julia-walk --invalidate-cache @@ -12,7 +15,11 @@ python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo # If the floats are reliable enough, then the middle (overlapping) frames shouldn't have # to render on the second invocation. - -#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='0.5' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.9 --zoom=5 --build-cache -#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='0.5' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.9 --zoom=0 --build-cache +## Put some effort into trying to make multiple invocations around the same end-point values +## actually end up with the same values for sub-clips. Hopefully this means overlapping frames +## look up as identical in the cache. +##python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=1 --build-cache +#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=5 --clip-total-frames=6 --build-cache +##python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=1 --clip-total-frames=1 --build-cache +#python3 mandelbrot.py --gif=phase_02.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=11 --build-cache diff --git a/fractalcache.py b/fractalcache.py index 82531cb..7f5c3d8 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -39,6 +39,11 @@ def emptyCopy(self): def pickleCopy(self): return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + def __repr__(self): + return """\ +[FrameInfo complex_range {{{cw},{ch}}} center ({cc})]\ +""".format(cw=str(self.complex_real_width),ch=str(self.complex_imag_width),cc=str(self.center)); + class Frame: """ Two different results caches available, one for uniform meshes @@ -97,6 +102,7 @@ def write_results_cache(self): filename = self.create_results_file_name() #print("+ writing frame to cache file %s ... "%(filename)) + #print("Writing: %s" % str(self.frame_info)) # Probably a mistake to write a no-data cache file, so panic. if self.frame_info.raw_values is None or self.frame_info.smooth_values is None: @@ -111,6 +117,8 @@ def read_results_cache(self): return #print("+ frame from cache file %s ... "%(filename)) + #print("Reading: %s" % str(self.frame_info)) + frame_data = None with open(filename, 'rb') as fd: diff --git a/fractalmath.py b/fractalmath.py index c584971..7ea5e21 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -13,6 +13,8 @@ FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 #FLINT_HIGH_PRECISION_SIZE = 200 +GMP_HIGH_PRECISION_SIZE=53 + class DiveMathSupport: """ Toolbox for math functions that need to be type-aware, so we @@ -32,9 +34,13 @@ class DiveMathSupport: def __init__(self): self.precisionType = 'native' - def createComplex(self, realComponent, imagComponent): - """Compatible complex types will return values for .real() and .imag()""" - return complex(float(realComponent), float(imagComponent)) + def createComplex(self, *args): + """ + Compatible complex types will return values for .real() and .imag() + + Native complex type just passes on all params to complex() + """ + return complex(*args) def createFloat(self, floatValue): return float(floatValue) @@ -249,11 +255,15 @@ def julia(self, c, z0, escapeRadius, maxIter): def smoothAfterCalculation(self, endingZ, endingIter, maxIter): if endingIter == maxIter: - return maxIter + return float(maxIter) else: # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. + #if endingIter != 1: + # print("iter was %d" % endingIter) + #print("z: \"%s\" max_iter: %d iter: %d" % (endingZ, maxIter, endingIter)) + #return endingIter + 1 - math.log(math.log2(abs(endingZ))) return endingIter + 1 - math.log(math.log2(abs(endingZ))) class DiveMathSupportFlint(DiveMathSupport): @@ -269,8 +279,89 @@ def __init__(self): self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) self.precisionType = 'flint' - def createComplex(self, realComponent, imagComponent): - return self.flint.acb(realComponent, imagComponent) + def createComplex(self, *args): + """ + Flint complex type doesn't accept a string for instantiation. + + So, we do a string conversion here, to be flexible + """ + if len(args) == 2: + realPart = self.flint.arb(args[0]) + imagPart = self.flint.arb(args[1]) + return self.flint.acb(realPart, imagPart) + elif len(args) == 1: + if isinstance(args[0], str): + partsString = args[0] + + # Trim off the leading sign + firstIsPositive = True + if partsString.startswith('+'): + partsString = partsString[1:] + elif partsString.startswith('-'): + firstIsPositive = False + partsString = partsString[1:] + + # Trim off the trailing letter + lastIsImag = False + if partsString.endswith('j'): + lastIsImag = True + partsString = partsString[:-1] + + # Remaining string might have an internal sign. + # If there's no internal sign, then the whole remaining + # string is either the real or the complex + positiveParts = partsString.split('+') + negativeParts = partsString.split('-') + + realIsPositive = True + imagIsPositive = True + realPart = "" + imagPart = "" + if len(positiveParts) == 2: + realIsPositive = firstIsPositive + realPart = positiveParts[0] + imagIsPositive = True + imagPart = positiveParts[1] + elif len(negativeParts) == 2: + realIsPositive = firstIsPositive + realPart = negativeParts[0] + imagIsPositive = False + imagPart = negativeParts[1] + elif len(positiveParts) == 1 and len(negativeParts) == 1: + # No internal + or -, so it should just be a number + if lastIsImag == True: + imagPart = partsString + imagIsPositive = firstIsPositive + else: + realPart = partsString + realIsPositive = firstIsPositive + else: + raise ValueError("String parameter not identifiably a complex number, in createComplex()") + + preparedReal = '0.0' + preparedImag = '0.0' + if realPart != "": + if realIsPositive == True: + preparedReal = realPart + else: + preparedReal = "-%s" % realPart + + if imagPart != "": + if imagIsPositive == True: + preparedImag = imagPart + else: + preparedImag = "-%s" % imagPart + + return self.flint.acb(preparedReal, preparedImag) + else: + if isinstance(args[0], complex): + return self.flint.acb(args[0].real, args[0].imag) + else: + return self.flint.acb(args[0]) # Just a constant, so make a 0-imaginary value + elif len(args) == 0: + return self.flint.acb(0,0) + else: + raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") def createFloat(self, floatValue): return self.flint.arb(floatValue) @@ -309,18 +400,109 @@ def mandelbrot(self, c, escapeRadius, maxIter): return (n, z) - def smoothAfterCalculation(self, endingZ, endingIter, maxIter): """ This flint-specific implementation only really does flint-y logs in the smoothing. The Decimal-specific implementation needed some extra steps, but I've ditched that one. """ if endingIter == maxIter: - return maxIter + return float(maxIter) else: # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. return float(endingIter + 1 - endingZ.abs_lower().const_log2().const_log10()) +class DiveMathSupportGmp(DiveMathSupport): + """ + Overrides to instantiate gmpy2-specific complex types + + Note from the GMP docs - contexts and context managers are not thread-safe! + Modifying the context in one thread will impact all other threads. + """ + def __init__(self): + super().__init__() + + self.gmp = __import__('gmpy2') # Only imports if you instantiate this DiveMathSupport subclass. + self.gmp.get_context().precision = GMP_HIGH_PRECISION_SIZE + self.precisionType = 'gmp' + + def createComplex(self, *args): + """ + gmpy2.mpc() accepts one of: + 1 string (a native python complex string, with real and/or imag components) + 1 complex object (python native) + 1 float (no imaginary component) + 2 floats + 2 mpfr (gmp's float type) + """ + if len(args) == 2: + realPart = self.gmp.mpfr(args[0]) + imagPart = self.gmp.mpfr(args[1]) + return self.gmp.mpc(realPart, imagPart) + elif len(args) == 1: + return self.gmp.mpc(args[0]) + elif len(args) == 0: + return self.gmp.mpc(0.0) + else: + raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") + + def createFloat(self, floatValue): + return self.gmp.mpfr(floatValue) + + def floor(self, value): + return self.gmp.floor(value) + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): + """ + Probably want additional log-defining params, but for now, let's just bake in one equation + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + aVal = (endY - startY) / (self.gmp.log(endX - startX + 1)) + return aVal * (self.gmp.log(targetX - startX + 1)) + startY + + def mandelbrot(self, c, escapeRadius, maxIter): + """ + Now that smoothing is handled separately, the native python implementation + COULD work for flint as well, except it uses 'complex(0,0)' instead + of self.createComplex(0,0). + The reason for this is just as below - to reduce one extra function call in + the core calculation. + + Could just call julia with a zero start here, but seems wiser to + not have one extra function call in the core of the calculation? + + Really super didn't help to try locally caching function names + and using paren syntax - it doubled the runtime. Trying that was + probably just old advice for pre-python3? + """ + z = self.gmp.mpc(0.0) + n = 0 + + # Because norm() doesn't work for python3-gmp2 v 2.1.0a4 + #while float(self.gmp.sqrt(self.gmp.square(z.real) + self.gmp.square(z.imag))) <= escapeRadius and n < maxIter: + # looks like abs() gets to the right place, even though there's no explicit abs_lower() in libgmp? + while abs(z) <= escapeRadius and n < maxIter: + z = z*z + c + n += 1 + return (n, z) + + def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + """ + This flint-specific implementation only really does flint-y logs in the smoothing. + The Decimal-specific implementation needed some extra steps, but I've ditched that one. + """ + if endingIter == maxIter: + return float(maxIter) + else: + # The following code smooths out the colors so there aren't bands + # Algorithm taken from http://linas.org/art-gallery/escape/escape.html + # Note: Results in a float. We think. + return float(endingIter + 1 - self.gmp.log10(self.gmp.log2(self.gmp.norm(endingZ)))) + #return float(endingIter + 1 - self.gmp.log10(self.gmp.log10(self.gmp.sqrt(self.gmp.square(endingZ.real) + self.gmp.square(endingZ.imag))))) + diff --git a/mandelbrot.py b/mandelbrot.py index af48874..8cbb848 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -62,14 +62,16 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.cmplx_width = self.math_support.createFloat(0.0) self.cmplx_height = self.math_support.createFloat(0.0) - self.cmplx_center = self.math_support.createComplex(0.0, 0.0) # center of image in complex plane + self.cmplx_center = self.math_support.createComplex(0.0) # center of image in complex plane self.max_iter = 0 # int max iterations before bailing - self.escape_rad = 0. # float radius mod Z hits before it "escapes" + self.escape_rad = 2. # float radius mod Z hits before it "escapes" self.scaling_factor = 0.0 # float amount to zoom each epoch self.set_zoom_level = 0 # Zoom in prior to the dive + self.clip_start_frame = -1 + self.clip_total_frames = 1 self.smoothing = False # bool turn on color smoothing self.snapshot = False # Generate a single, high res shotb @@ -202,6 +204,9 @@ def __init__(self, duration, fps, ctx): def frame_number_from_time(self, t): return math.floor(self.fps * t) + def time_from_frame_number(self, frame_number): + return frame_number / self.fps + def intro_banner(self): # Generate a text clip w,h = self.ctx.img_width, self.ctx.img_height @@ -244,8 +249,16 @@ def run(self): return self.ctx.timeline = self.construct_simple_timeline() + # Duration may be less than overall, if this is a sub-clip, so + # figure out what our REAL duration is. + timeline_frame_count = self.ctx.timeline.getTotalSpanFrameCount() + timeline_duration = self.time_from_frame_number(timeline_frame_count) + + #for currFrameNumber in range(timeline_frame_count): + # print(self.ctx.render_frame_number(currFrameNumber)) + #exit(0) - self.clip = mpy.VideoClip(self.make_frame, duration=self.duration) + self.clip = mpy.VideoClip(self.make_frame, duration=timeline_duration) if self.banner: self.clip = self.intro_banner() @@ -270,44 +283,56 @@ def construct_simple_timeline(self): Basically, let's construct a timeline from one set of start/end points, as defined by the current context. """ - frame_count = self.duration * self.fps - if math.floor(frame_count) != frame_count: - frame_count = math.floor(frame_count) + 1 + overall_frame_count = self.duration * self.fps + if math.floor(overall_frame_count) != overall_frame_count: + overall_frame_count = math.floor(overall_frame_count) + 1 + + clip_start_frame = 0 + rendered_frame_count = overall_frame_count + last_frame_number = overall_frame_count overall_zoom_factor = self.ctx.scaling_factor + + if self.ctx.clip_start_frame == -1: # Whole clip is the span + start_width_real = self.ctx.cmplx_width + start_width_imag = self.ctx.cmplx_height + else: + if self.ctx.clip_start_frame + self.ctx.clip_total_frames > overall_frame_count: + raise ValueError("Can't construct timeline of %d frames for a sequence of only %d frames (%f seconds at %f fps)" % (self.ctx.clip_total_frames, overall_frame_count, self.duration, self.fps)) - start_width_real = self.ctx.cmplx_width - start_width_imag = self.ctx.cmplx_height - - # Check whether we need to zoom in prior to calculation - if self.ctx.set_zoom_level > 0: - print("Zooming in by %d epochs" % (self.ctx.set_zoom_level)) - start_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, self.ctx.set_zoom_level) - start_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, self.ctx.set_zoom_level) - - # Calculate the endpoint widths, at the last desired frame - end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(start_width_real, overall_zoom_factor, frame_count - 1) - end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(start_width_imag, overall_zoom_factor, frame_count - 1) + clip_start_frame = self.ctx.clip_start_frame + last_frame_number = clip_start_frame + self.ctx.clip_total_frames - 1 + rendered_frame_count = last_frame_number - clip_start_frame + 1 + + # Start frame is 1 or greater (hopefully), so subtracting 1 from the exponent here should be okay. + start_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, clip_start_frame) + start_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, clip_start_frame) + + # Sub-section the frames, if needed + end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) + end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) + + print("{%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) if self.ctx.fractal == 'julia': timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='julia', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) # Just evenly divide the waypoints across the time for a simple timeline keyframeCount = len(self.ctx.julia_list) # 2 keyframes over 10 frames = 10 frames per keyframe - keyframeSpacing = math.floor(frame_count / (keyframeCount - 1)) + keyframeSpacing = math.floor(rendered_frame_count / (keyframeCount - 1)) if keyframeSpacing < 1: raise ValueError("Can't construct julia walk with more waypoints than animation frames") - span = DiveTimelineSpan(timeline, frame_count, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + span = DiveTimelineSpan(timeline, rendered_frame_count, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) span.addNewWindowKeyframe(0, start_width_real, start_width_imag) - span.addNewWindowKeyframe(frame_count - 1, end_width_real, end_width_imag) + span.addNewWindowKeyframe(rendered_frame_count - 1, end_width_real, end_width_imag) span.addNewUniformKeyframe(0) - span.addNewUniformKeyframe(frame_count - 1) + span.addNewUniformKeyframe(rendered_frame_count - 1) currKeyframeFrameNumber = 0 for currJuliaCenter in self.ctx.julia_list: # Recognize when we're at the last item, and jump that keyframe to the final frame - if currKeyframeFrameNumber + keyframeSpacing > frame_count - 1: + if currKeyframeFrameNumber + keyframeSpacing > rendered_frame_count - 1: currKeyframeNumber = frame_count - 1 span.addNewCenterKeyframe(currKeyframeFrameNumber, currJuliaCenter, transitionIn='linear', transitionOut='linear') @@ -319,7 +344,7 @@ def construct_simple_timeline(self): timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='mandelbrot', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) # Here, I have ctx, which should know escapeRadius, maxIter, and shouldSmooth... #print("Trying to make span of %d frames" % frame_count) - span = timeline.addNewSpanAtEnd(frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + span = timeline.addNewSpanAtEnd(rendered_frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) #perspectiveFrame = math.floor(frame_count * .5) #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) @@ -374,6 +399,12 @@ def __init__(self, projectFolderName, fractal, framerate, frameWidth, frameHeigh # No definition made yet for edit gaps, so let's just enforce adjacency of ranges for now. self.timelineSpans = [] + def getTotalSpanFrameCount(self): + seenFrameCount = 0 + for currSpan in self.timelineSpans: + seenFrameCount += currSpan.frameCount + return seenFrameCount + def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag, escapeRadius, maxEscapeIterations, shouldSmooth): """ Constructs a new span, and adds it to the end of the existing span list @@ -1071,9 +1102,11 @@ def set_demo1_params(mandl_ctx, view_ctx): # This is close t Misiurewicz point M32,2 # mandl_ctx.cmplx_center = mandl_ctx.ctxc(-.77568377, .13646737) - center_real_str = '-1.769383179195515018213' - center_imag_str = '0.00423684791873677221' - mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_real_str, center_imag_str) + #center_real_str = '-1.769383179195515018213' + #center_imag_str = '0.00423684791873677221' + #mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_real_str, center_imag_str) + center_str = '-1.769383179195515018213+0.00423684791873677221j' + mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_str) mandl_ctx.project_name = 'demo1' @@ -1089,6 +1122,7 @@ def set_demo1_params(mandl_ctx, view_ctx): mandl_ctx.build_cache=True view_ctx.duration = 2.0 + #view_ctx.duration = 0.25 # FPS still isn't set quite right, but we'll get it there eventually. view_ctx.fps = 23.976 / 2.0 @@ -1178,6 +1212,8 @@ def parse_options(mandl_ctx, view_ctx): "demo-julia-walk", "duration=", "fps=", + "clip-start-frame=", + "clip-total-frames=", "max-iter=", "img-w=", "img-h=", @@ -1196,6 +1232,7 @@ def parse_options(mandl_ctx, view_ctx): "color=", "burn", "flint", + "gmp", "project-name=", "shared-cache-path=", "build-cache", @@ -1206,7 +1243,9 @@ def parse_options(mandl_ctx, view_ctx): # Math support as to be handled first, so other parameter # instantiations are properly typed for opt, arg in opts: - if opt in ['--flint']: + if opt in ['--gmp']: + mandl_ctx.math_support = fm.DiveMathSupportGmp() + elif opt in ['--flint']: mandl_ctx.math_support = fm.DiveMathSupportFlint() for opt,arg in opts: @@ -1224,6 +1263,10 @@ def parse_options(mandl_ctx, view_ctx): view_ctx.duration = float(arg) elif opt in ['-f', '--fps']: view_ctx.fps = float(arg) + elif opt in ['--clip-start-frame']: + mandl_ctx.clip_start_frame = int(arg) + elif opt in ['--clip-total-frames']: + mandl_ctx.clip_total_frames = int(arg) elif opt in ['-m', '--max-iter']: mandl_ctx.max_iter = int(arg) elif opt in ['-w', '--img-w']: From 5b1dcd37ae341a63e24d69d8590c829079d53ecd Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sun, 25 Jul 2021 12:00:46 -0700 Subject: [PATCH 13/44] Seems like vectorized numpy function is behaving. --- clint_runscript.sh | 4 ++-- mandelbrot.py | 56 +++++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 7e91625..2921f06 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,6 +1,6 @@ -python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo -#python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache +#python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo +python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache #python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache diff --git a/mandelbrot.py b/mandelbrot.py index 8cbb848..c606091 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -140,7 +140,7 @@ def render_frame_number(self, frame_number, snapshot_filename=None): return im.save(snapshot_filename,"gif") else: return np.array(im) - + def draw_image_PIL(self, values, hues, metadata=None): im = Image.new('RGB', (self.img_width, self.img_height), (0, 0, 0)) @@ -254,10 +254,6 @@ def run(self): timeline_frame_count = self.ctx.timeline.getTotalSpanFrameCount() timeline_duration = self.time_from_frame_number(timeline_frame_count) - #for currFrameNumber in range(timeline_frame_count): - # print(self.ctx.render_frame_number(currFrameNumber)) - #exit(0) - self.clip = mpy.VideoClip(self.make_frame, duration=timeline_duration) if self.banner: @@ -486,21 +482,22 @@ def loadResultsForFrameNumber(self, frameNumber, buildCache=True, invalidateCach def calculateResultsForDiveMesh(self, diveMesh): mesh = diveMesh.generateMesh() - show_row_progress = False + if self.fractalType == 'julia': + juliaFunction = np.vectorize(self.mathSupport.julia) + (pixel_values_2d, lastZees) = juliaFunction(diveMesh.center, mesh, diveMesh.escapeRadius, diveMesh.maxEscapeIterations) + else: # self.FractalType == 'mandelbrot' + mandelbrotFunction = np.vectorize(self.mathSupport.mandelbrot) + (pixel_values_2d, lastZees) = mandelbrotFunction(mesh, diveMesh.escapeRadius, diveMesh.maxEscapeIterations) + + smoothingFunction = np.vectorize(self.mathSupport.smoothAfterCalculation) + pixel_values_2d_smoothed = smoothingFunction(lastZees, pixel_values_2d, diveMesh.maxEscapeIterations) - pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) - pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) hist = defaultdict(int) hist_smoothed = defaultdict(int) + + show_row_progress = False for x in range(0, mesh.shape[0]): for y in range(0, mesh.shape[1]): - if self.fractalType == 'julia': - (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) - else: # self.FractalType == 'mandelbrot' - (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) - - pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) - # Not using mathSupport's floor() here, because it should just be a normal-scale float if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) @@ -508,9 +505,32 @@ def calculateResultsForDiveMesh(self, diveMesh): if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 - if show_row_progress == True: - print("%d-" % x, end="") - sys.stdout.flush() + +# pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) +# pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) +# hist = defaultdict(int) +# hist_smoothed = defaultdict(int) +# +# show_row_progress = True +# for x in range(0, mesh.shape[0]): +# for y in range(0, mesh.shape[1]): +# if self.fractalType == 'julia': +# (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) +# else: # self.FractalType == 'mandelbrot' +# (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) +# +# pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) +# +# # Not using mathSupport's floor() here, because it should just be a normal-scale float +# if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: +# #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) +# hist[math.floor(pixel_values_2d[x,y])] += 1 +# if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: +# hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 +# +# if show_row_progress == True: +# print("%d-" % x, end="") +# sys.stdout.flush() return (pixel_values_2d, hist, pixel_values_2d_smoothed, hist_smoothed) From 39193bb862831f3aed328dfa4f957119a1c98454 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 28 Jul 2021 12:29:45 -0700 Subject: [PATCH 14/44] Attempting a merge in of new Algo process from main, while also pushing core calculations into MathSupport classes --- README.md | 32 ++ clint_runscript.sh | 24 +- divemesh.py | 7 +- mandelbrot.py => fractal.py | 752 +++++++++++++++++------------------- fractalcache.py | 66 ++-- fractalmath.py | 23 +- fractalpalette.py | 78 +++- 7 files changed, 513 insertions(+), 469 deletions(-) rename mandelbrot.py => fractal.py (61%) diff --git a/README.md b/README.md index 8144f0a..6d1c02a 100644 --- a/README.md +++ b/README.md @@ -1 +1,33 @@ # mandl + +FractalContext + - Keeps and manages overall parameters. + - Instantiates algorithm and precision-aware subclasses. + - Holds the Timeline for the animation. + - Responsible for generating images for frame numbers. + + +MediaView + - Configures encoding parameters for output rendering. + + +DiveTimeline + - Keeps framerate and image/mesh size. + - Keeps a sequential list of DiveTimelineSpans, which define animation parameters through their keyframes. + - Instantiates an appropriate Algo for every frame requested. + + +Algo +EscapeAlgo(Algo) and EscapeFrameInfo(FrameInfo) +JuliaAlgo(EscapeAlgo) and JuliaFrameInfo(FrameInfo) + - Chaperone for algorithm-specific intermediates. + - Holds both algorithm invocations, and algorithm-specific pre and post processing hooks. + - Responsible for intermediates caching. + + +MathSupport + - Implementations of library-specific calculations. + - Interpolators and multipliers for different numeric types. + - Native Python implementation in the base class. + + diff --git a/clint_runscript.sh b/clint_runscript.sh index 2921f06..f73df0e 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,15 +1,15 @@ -#python3 mandelbrot.py --gif=native_demo.gif --color=exp2 --demo -python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache +#python3 fractal.py --gif=native_demo.gif --color=exp2 --demo +python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache -#python3 mandelbrot.py --gif=flint_demo.gif --color=exp2 --demo --flint -#python3 mandelbrot.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache +#python3 fractal.py --gif=flint_demo.gif --color=exp2 --demo --flint +#python3 fractal.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache -#python3 mandelbrot.py --gif=gmp_demo.gif --color=exp2 --demo --gmp -#python3 mandelbrot.py --gif=gmp_demo_fresh.gif --color=exp2 --demo --gmp --invalidate-cache +#python3 fractal.py --gif=gmp_demo.gif --color=exp2 --demo --gmp +#python3 fractal.py --gif=gmp_demo_fresh.gif --color=exp2 --demo --gmp --invalidate-cache -#python3 mandelbrot.py --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk -#python3 mandelbrot.py --gif=native_julia_demo_fresh.gif --color=exp2 --demo-julia-walk --invalidate-cache +#python3 fractal.py --algo='julia' --gif=native_julia_demo.gif --color=exp2 --demo-julia-walk +#python3 fractal.py --algo='julia' --gif=native_julia_demo_fresh.gif --color=exp2 --demo-julia-walk --invalidate-cache # Let's test out range overlaps for separate invocations. # If the floats are reliable enough, then the middle (overlapping) frames shouldn't have @@ -18,8 +18,8 @@ python3 mandelbrot.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalida ## Put some effort into trying to make multiple invocations around the same end-point values ## actually end up with the same values for sub-clips. Hopefully this means overlapping frames ## look up as identical in the cache. -##python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=1 --build-cache -#python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=5 --clip-total-frames=6 --build-cache -##python3 mandelbrot.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=1 --clip-total-frames=1 --build-cache -#python3 mandelbrot.py --gif=phase_02.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=11 --build-cache +##python3 fractal.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=1 --build-cache +#python3 fractal.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=5 --clip-total-frames=6 --build-cache +##python3 fractal.py --gif=phase_01.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=1 --clip-total-frames=1 --build-cache +#python3 fractal.py --gif=phase_02.gif --color=exp2 --burn --project-name='phase01' --duration='1.0' --fps=11.988 --max-iter=255 --img-w=1024 --img-h=768 --cmplx-w='5.0' --cmplx-h='3.5' --center="-1.769383179195515018213+.00423684791873677221j" --scaling-factor=0.8 --clip-start-frame=0 --clip-total-frames=11 --build-cache diff --git a/divemesh.py b/divemesh.py index d37d58d..c8f8f59 100644 --- a/divemesh.py +++ b/divemesh.py @@ -34,7 +34,7 @@ class DiveMesh: # Then, the separate 2D meshes are combined to become the overall mesh values. # Finally, overall distortions are applied to the overall mesh values. """ - def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, escapeRadius, maxEscapeIterations, shouldSmooth): + def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, extraParams={}): # Trying not to apply castings to these types, to keep them the same as # the original parameters, which could make swapping out different # precision libraries simpler? @@ -55,10 +55,7 @@ def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, # pass an extra parameter here and there. self.mathSupport = mathSupport - # These properties are attached to the mesh, provided by the DiveTimelineSpan that created them - self.escapeRadius = escapeRadius - self.maxEscapeIterations = maxEscapeIterations - self.shouldSmooth = shouldSmooth + self.extraParams = extraParams def generateMesh(self): realMesh = self.realMeshGenerator.generateForDiveMesh(self) diff --git a/mandelbrot.py b/fractal.py similarity index 61% rename from mandelbrot.py rename to fractal.py index c606091..91ea961 100644 --- a/mandelbrot.py +++ b/fractal.py @@ -44,12 +44,16 @@ import fractalcache as fc import fractalpalette as fp import fractalmath as fm - import divemesh as mesh +from algo import Algo # Abstract base class import, because we rely on it. +from julia import Julia +from mandelbrot import Mandelbrot +from mandeldistance import MandelDistance + MANDL_VER = "0.1" -class MandlContext: +class FractalContext: """ The context for a single dive """ @@ -76,13 +80,11 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.smoothing = False # bool turn on color smoothing self.snapshot = False # Generate a single, high res shotb - self.fractal = 'mandelbrot' # or 'julia' - self.julia_list = None + self.julia_list = None # Used just for timeline construction self.palette = None - self.burn_in = False - # Shifting from the MandlContext being the oracle of frame information, to the Timeline being the oracle. + # Shifting from the FractalContext being the oracle of frame information, to the Timeline being the oracle. # Rather than keeping 'current frame' info in the context, we just keep the timeline, and # query it for frame-specific parameters to render with. self.timeline = None @@ -92,107 +94,58 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.build_cache = False self.invalidate_cache = False - self.verbose = 0 # how much to print about progress - - def render_frame_number(self, frame_number, snapshot_filename=None): - """ - Load or calculate the frame data. - - Once we have frame data, can perform histogram and coloring - """ - (pixel_values_2d_raw, hist_raw, pixel_values_2d_smooth, hist_smooth, frame_metadata) = self.timeline.loadResultsForFrameNumber(frame_number, buildCache=self.build_cache, invalidateCache=self.invalidate_cache) - - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.smoothing == True: - pixel_values_2d = pixel_values_2d_smooth.T - hist = hist_smooth - else: - pixel_values_2d = pixel_values_2d_raw.T - hist = hist_raw - - #print("shape of things to come: %s" % str(pixel_values_2d.shape)) + self.algorithm_map = {'julia' : Julia, + 'mandelbrot' : Mandelbrot, + 'mandeldistance' : MandelDistance} + self.algorithm_name = None + self.algorithm_extra_params = {} # Keeps command-line params for later use - total = sum(hist.values()) - hues = [] - h = 0 + self.verbose = 0 # how much to print about progress - # calculate percent of total for each iteration - for i in range(self.max_iter): - if total : - h += hist[i] / total - hues.append(h) - hues.append(h) + def render_frame_number(self, frame_number): + extra_params = {} + if self.algorithm_name in self.algorithm_extra_params: + extra_params = self.algorithm_extra_params[self.algorithm_name] - # -- - # Create image for this frame - # -- + #dive_mesh realizes more info is needed, so stashes it into its extraParams + # Gotta retrieve that in calculateResults, right? + # So, extra_params needs to be looked at, and algo params need to be set by the values, right? + # Algorithm gets instantiated with all its parts in place... + # We've gotta allow mesh's extra params to add to and override the overall algorithm's extra params? - im = self.draw_image_PIL(pixel_values_2d, hues, frame_metadata) + dive_mesh = self.timeline.getMeshForFrame(frame_number) + #print("extra params from mesh: %s" % str(dive_mesh.extraParams)) + extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? - #print("Finished iteration RErange %f:%f (re width: %f)"%(RE_START, RE_END, RE_END - RE_START)) - #print("Finished iteration IMrange %f:%f (im height: %f)"%(IM_START, IM_END, IM_END - IM_START)) + #print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) + frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh, frame_number, self.project_name, self.shared_cache_path, self.build_cache, self.invalidate_cache, extra_params) - #if self.verbose > 0: - # print("Done]") + frame_algorithm.beginning_hook() - if snapshot_filename: - return im.save(snapshot_filename,"gif") - else: - return np.array(im) - - def draw_image_PIL(self, values, hues, metadata=None): - - im = Image.new('RGB', (self.img_width, self.img_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) + frame_algorithm.generate_results() - for x in range(0, self.img_width): - for y in range(0, self.img_height): - m = values[x,y] - - if not self.palette: - c = 255 - int(255 * hues[math.floor(m)]) - color=(c, c, c) - elif self.smoothing: - c1 = self.palette[1024 - int(1024 * hues[math.floor(m)])] - c2 = self.palette[1024 - int(1024 * hues[math.ceil(m)])] - color = fp.MandlPalette.linear_interpolate(c1,c2,.5) - else: - color = self.palette[1024 - int(1024 * hues[math.floor(m)])] - - # Plot the point - draw.point([x, y], color) - - #print("Finished iteration RErange %f:%f (re width: %f)"%(RE_START, RE_END, RE_END - RE_START)) - #print("Finished iteration IMrange %f:%f (im height: %f)"%(IM_START, IM_END, IM_END - IM_START)) + frame_algorithm.pre_image_hook() - if self.burn_in == True and metadata != None: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (metadata['frame_number'], metadata['mesh_center'], metadata['complex_real_width'], metadata['complex_imag_width']) + frame_image = frame_algorithm.generate_image() - burn_in_location = (10,10) - burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) - burn_in_size = burn_in_font.getsize_multiline(burn_in_text) - draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") - draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) - - return im + frame_algorithm.ending_hook() + return frame_image def __repr__(self): return """\ -[MandlContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:s} -Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Smoothing:{sm:b} Max iter:{mx:d}]\ +[FractalContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:s} +Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Max iter:{mx:d}]\ """.format( w=self.img_width,h=self.img_height,cw=str(self.cmplx_width),ch=str(self.cmplx_height), - cc=str(self.cmplx_center),s=self.scaling_factor,mx=self.max_iter,sm=self.smoothing); + cc=str(self.cmplx_center),s=self.scaling_factor,mx=self.max_iter); class MediaView: """ Handle displaying to gif / mp4 / screen etc. """ def make_frame(self, t): - return self.ctx.render_frame_number(self.frame_number_from_time(t)) + return np.array(self.ctx.render_frame_number(self.frame_number_from_time(t))) def __init__(self, duration, fps, ctx): self.duration = duration @@ -308,10 +261,12 @@ def construct_simple_timeline(self): end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) - print("{%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) + print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) - if self.ctx.fractal == 'julia': - timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='julia', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) + timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm_name=self.ctx.algorithm_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) + + if timeline.algorithm_name == 'julia': + #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='julia', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) # Just evenly divide the waypoints across the time for a simple timeline keyframeCount = len(self.ctx.julia_list) # 2 keyframes over 10 frames = 10 frames per keyframe @@ -319,28 +274,32 @@ def construct_simple_timeline(self): if keyframeSpacing < 1: raise ValueError("Can't construct julia walk with more waypoints than animation frames") - span = DiveTimelineSpan(timeline, rendered_frame_count, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + span = DiveTimelineSpan(timeline, rendered_frame_count) span.addNewWindowKeyframe(0, start_width_real, start_width_imag) span.addNewWindowKeyframe(rendered_frame_count - 1, end_width_real, end_width_imag) span.addNewUniformKeyframe(0) span.addNewUniformKeyframe(rendered_frame_count - 1) + span.addNewCenterKeyframe(0, self.ctx.cmplx_center) + span.addNewCenterKeyframe(rendered_frame_count - 1, self.ctx.cmplx_center) + currKeyframeFrameNumber = 0 for currJuliaCenter in self.ctx.julia_list: # Recognize when we're at the last item, and jump that keyframe to the final frame if currKeyframeFrameNumber + keyframeSpacing > rendered_frame_count - 1: - currKeyframeNumber = frame_count - 1 + currKeyframeNumber = rendered_frame_count - 1 - span.addNewCenterKeyframe(currKeyframeFrameNumber, currJuliaCenter, transitionIn='linear', transitionOut='linear') + #span.addNewCenterKeyframe(currKeyframeFrameNumber, currJuliaCenter, transitionIn='linear', transitionOut='linear') + span.addNewComplexParameterKeyframe(currKeyframeFrameNumber, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') currKeyframeFrameNumber += keyframeSpacing timeline.timelineSpans.append(span) else: - timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='mandelbrot', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) - # Here, I have ctx, which should know escapeRadius, maxIter, and shouldSmooth... + #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='mandelbrot', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) + #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm=self.ctx.algorithm, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) #print("Trying to make span of %d frames" % frame_count) - span = timeline.addNewSpanAtEnd(rendered_frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag, self.ctx.escape_rad, self.ctx.max_iter, self.ctx.smoothing) + span = timeline.addNewSpanAtEnd(rendered_frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag) #perspectiveFrame = math.floor(frame_count * .5) #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) @@ -376,15 +335,12 @@ class DiveTimeline: currently all live on integer frame numbers. """ - def __init__(self, projectFolderName, fractal, framerate, frameWidth, frameHeight, mathSupport, sharedCachePath): + def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, frameHeight, mathSupport, sharedCachePath): self.projectFolderName = projectFolderName self.sharedCachePath = sharedCachePath - fractalOptions = ['mandelbrot', 'julia'] - if fractal not in fractalOptions: - raise ValueError("fractal must be one of (%s)" % ", ".join(fractalOptions)) - self.fractalType = fractal + self.algorithm_name = algorithm_name self.framerate = float(framerate) self.frameWidth = int(frameWidth) @@ -401,7 +357,7 @@ def getTotalSpanFrameCount(self): seenFrameCount += currSpan.frameCount return seenFrameCount - def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag, escapeRadius, maxEscapeIterations, shouldSmooth): + def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag): """ Constructs a new span, and adds it to the end of the existing span list @@ -411,7 +367,7 @@ def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, en Apparently also adding perspective keyframes too. """ - span = DiveTimelineSpan(self, frameCount, escapeRadius, maxEscapeIterations, shouldSmooth) + span = DiveTimelineSpan(self, frameCount) span.addNewCenterKeyframe(0, center, 'quadratic-to', 'quadratic-to') span.addNewCenterKeyframe(frameCount - 1, center, 'quadratic-to', 'quadratic-to') span.addNewWindowKeyframe(0, startWidthReal, startWidthImag) @@ -445,131 +401,6 @@ def getSpanForFrameNumber(self, frameNumber): return None # Went past the end without finding a valid span, so it's too high a frame number - def loadResultsForFrameNumber(self, frameNumber, buildCache=True, invalidateCache=False): - """ Multi-cache-aware data loading or calculating """ - diveMesh = self.getMeshForFrame(frameNumber) - cacheFrame = fc.Frame(self, diveMesh, frameNumber) - - if invalidateCache == True: - cacheFrame.remove_from_results_cache() - - cacheFrame.read_results_cache() - - if cacheFrame.frame_info.raw_values is None or cacheFrame.frame_info.raw_histogram is None or cacheFrame.frame_info.smooth_values is None or cacheFrame.frame_info.smooth_histogram is None: - #print("+ calculating epoch results") - (rawValues, rawHistogram, smoothValues, smoothHistogram) = self.calculateResultsForDiveMesh(diveMesh) - cacheFrame.frame_info.raw_values = rawValues - cacheFrame.frame_info.raw_histogram = rawHistogram - cacheFrame.frame_info.smooth_values = smoothValues - cacheFrame.frame_info.smooth_histogram = smoothHistogram - - # Fresly calculated results get saved if we're building the cache - if buildCache == True: - cacheFrame.write_results_cache() - - frame_metadata = {'frame_number' : frameNumber, - 'fractal_type': self.fractalType, - 'precision_type': self.mathSupport.precisionType, - 'mesh_center': str(diveMesh.center), - 'complex_real_width' : str(diveMesh.realMeshGenerator.baseWidth), - 'complex_imag_width' : str(diveMesh.imagMeshGenerator.baseWidth), - 'escape_radius' : str(diveMesh.escapeRadius), - 'mesh_is_uniform' : str(diveMesh.isUniform()), - 'max_escape_iterations' : str(diveMesh.maxEscapeIterations)} - - return (cacheFrame.frame_info.raw_values, cacheFrame.frame_info.raw_histogram, cacheFrame.frame_info.smooth_values, cacheFrame.frame_info.smooth_histogram, frame_metadata) - - def calculateResultsForDiveMesh(self, diveMesh): - mesh = diveMesh.generateMesh() - - if self.fractalType == 'julia': - juliaFunction = np.vectorize(self.mathSupport.julia) - (pixel_values_2d, lastZees) = juliaFunction(diveMesh.center, mesh, diveMesh.escapeRadius, diveMesh.maxEscapeIterations) - else: # self.FractalType == 'mandelbrot' - mandelbrotFunction = np.vectorize(self.mathSupport.mandelbrot) - (pixel_values_2d, lastZees) = mandelbrotFunction(mesh, diveMesh.escapeRadius, diveMesh.maxEscapeIterations) - - smoothingFunction = np.vectorize(self.mathSupport.smoothAfterCalculation) - pixel_values_2d_smoothed = smoothingFunction(lastZees, pixel_values_2d, diveMesh.maxEscapeIterations) - - hist = defaultdict(int) - hist_smoothed = defaultdict(int) - - show_row_progress = False - for x in range(0, mesh.shape[0]): - for y in range(0, mesh.shape[1]): - # Not using mathSupport's floor() here, because it should just be a normal-scale float - if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: - #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) - hist[math.floor(pixel_values_2d[x,y])] += 1 - if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: - hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 - - -# pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) -# pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) -# hist = defaultdict(int) -# hist_smoothed = defaultdict(int) -# -# show_row_progress = True -# for x in range(0, mesh.shape[0]): -# for y in range(0, mesh.shape[1]): -# if self.fractalType == 'julia': -# (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) -# else: # self.FractalType == 'mandelbrot' -# (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) -# -# pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) -# -# # Not using mathSupport's floor() here, because it should just be a normal-scale float -# if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: -# #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) -# hist[math.floor(pixel_values_2d[x,y])] += 1 -# if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: -# hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 -# -# if show_row_progress == True: -# print("%d-" % x, end="") -# sys.stdout.flush() - - return (pixel_values_2d, hist, pixel_values_2d_smoothed, hist_smoothed) - - #### - # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here - # somewhere... - #### - - # In search of efficient ways to apply the map, and getting stuck with various issues - # like pickling, which are keeping me from using multiprocessing.Pool - -# Seemed to go exponential run time for some bizarre reason -# # Probably not necessary, but lining up the 2-element subarray -# pixel_inputs_1d = pixel_values_2d.reshape((mesh.shape[0] * mesh.shape[1])) -# -# # Pretty sure this is mistakenly doing an n! pass, or something just as ridiculous. -# #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) -# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeRadius, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) -# -# pixel_values_2d = pixel_values_1d.reshape((mesh.shape[0], mesh.shape[1])) -# #pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level -# #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) - - #nope - #theFunction = np.vectorize(self.mandelbrot_flint) - #pixel_values_1d = theFunction(pixel_inputs_1d) - - #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) - - #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) - #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) - #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) - - # Can't pikcle... hmm - #mandelpool = multiprocessing.Pool(processes = 1) - #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) - #mandelpool.close() - #mandelpool.join() - def getMeshForFrame(self, frameNumber): """ Calculate a discretized 2d plane of complex points based on spans and keyframes in this timeline. @@ -630,11 +461,21 @@ def getMeshForFrame(self, frameNumber): # Tilt factor is the multiplier applied to the range. realMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal, tiltFactor=widthTiltFactor) imagMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag, tiltFactor=heightTiltFactor) - - # Passing lots into the dive mesh. Notably, some info about the DiveTimelineSpan that was - # responsible for creating this mesh. Might want to store an actual reference to the object, but - # doesn't seem needed yet? - diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, targetSpan.escapeRadius, targetSpan.maxEscapeIterations, targetSpan.shouldSmooth) + + extraFrameParams = {} + parameterKeyframePairs = targetSpan.getParameterKeyframePairsClosestToFrameNumber('complex', localFrameNumber) + # parameterKeyframePairs['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] + for currParamName, currKeyframePairs in parameterKeyframePairs.items(): + for (leftKeyframe, rightKeyframe) in currKeyframePairs: + extraFrameParams[currParamName] = targetSpan.interpolateComplexBetweenParameterKeyframes(localFrameNumber, leftKeyframe, rightKeyframe) + + parameterKeyframePairs = targetSpan.getParameterKeyframePairsClosestToFrameNumber('float', localFrameNumber) + # parameterKeyframePairs['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] + for currParamName, currKeyframePairs in parameterKeyframePairs.items(): + for (leftKeyframe, rightKeyframe) in currKeyframePairs: + extraFrameParams[currParamName] = self.interpolateFloatBetweenParameterKeyframes(localFrameNumber, leftKeyframe, rightKeyframe) + + diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, extraFrameParams) #print (diveMesh) return diveMesh @@ -677,24 +518,8 @@ def interpolateTiltFactorsBetweenPerspectiveKeyframes(self, frameNumber, leftKey rightWidthValue = rightKeyframe.widthFactor rightHeightValue = rightKeyframe.heightFactor - if transitionType == 'log-to': - widthTiltFactor = self.mathSupport.interpolateLogTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateLogTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - elif transitionType == 'root-to': - widthTiltFactor = self.mathSupport.interpolateRootTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateRootTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - elif transitionType == 'linear': - widthTiltFactor = self.mathSupport.interpolateLinearTo(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateLinearTo(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - elif transitionType == 'quadratic-to': - widthTiltFactor = self.mathSupport.interpolateQuadraticEaseOut(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateQuadraticEaseOut(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - elif transitionType == 'quadratic-from': - widthTiltFactor = self.mathSupport.interpolateQuadraticEaseIn(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateQuadraticEaseIn(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - else: # transitionType == 'quadratic-to-from' - widthTiltFactor = self.mathSupport.interpolateQuadraticEaseInOut(leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolateQuadraticEaseInOut(leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) + widthTiltFactor = self.mathSupport.interpolate(transitionType, leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) + heightTiltFactor = self.mathSupport.interpolate(transitionType, leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) return(widthTiltFactor, heightTiltFactor) @@ -712,20 +537,22 @@ class DiveTimelineSpan: """ # ?(Can be used to observe/calculate n-1 zoom factors.)? """ - def __init__(self, timeline, frameCount, escapeRadius, maxEscapeIterations, shouldSmooth): + def __init__(self, timeline, frameCount): self.timeline = timeline self.frameCount = int(frameCount) - self.escapeRadius = escapeRadius - self.maxEscapeIterations = maxEscapeIterations - self.shouldSmooth = shouldSmooth - # Only a single 'track' for each keyframe type to begin with, represented # just as a keyframe lookup for each track. Being able to stack multiples of # similar-typed keyframes will probably be helpful in the long run. self.centerKeyframes = {} self.windowKeyframes = {} self.perspectiveKeyframes = {} + + self.complexParameterKeyframes = defaultdict(dict) + self.floatParameterKeyframes = defaultdict(dict) + # parameterKeyframes['julia_center'][25] = complex(0,0) + # parameterKeyframes['julia_center'][40] = complex(0,0) + # Currently, not allowing keyframes to exist outside of the span, even though # that is often helpful for defining pleasing transitions. # Currently, also not allowing keyframes to exist at non-frame targets, which @@ -788,6 +615,20 @@ def addNewTiltKeyframe(self, frameNumber, widthFactor, heightFactor, transitionI self.perspectiveKeyframes[frameNumber] = newKeyframe return newKeyframe + def addNewComplexParameterKeyframe(self, frameNumber, name, value, transitionIn='linear', transitionOut='linear'): + # TODO: Would like to be unable to assign keyframes with identical + # names to both the complex and float parameter sets. + newKeyframe = DiveSpanParameterKeyframe(self, value, transitionIn, transitionOut) + self.complexParameterKeyframes[name][frameNumber] = newKeyframe + # parameterKeyframes['julia_center'][40] = complex(0,0) + return newKeyframe + + def addNewFloatParameterKeyframe(self, frameNumber, name, value, transitionIn='linear', transitionOut='linear'): + newKeyframe = DiveSpanParameterKeyframe(self, value, transitionIn, transitionOut) + self.floatParameterKeyframes[name][frameNumber] = newKeyframe + # parameterKeyframes['a_value'][40] = 1.234 + return newKeyframe + def getKeyframesClosestToFrameNumber(self, keyframeType, frameNumber): """ Returns a tuple of keyframes, which are the nearest left and right keyframes for the frameNumber. @@ -830,6 +671,50 @@ def getKeyframesClosestToFrameNumber(self, keyframeType, frameNumber): return (previousKeyframe, nextKeyframe) + def getParameterKeyframePairsClosestToFrameNumber(self, keyframeType, frameNumber): + typeOptions = ['float', 'complex'] + if keyframeType not in typeOptions: + raise ValueError("keyframeType must be one of (%s)" % ", ".join(typeOptions)) + + if frameNumber >= self.frameCount: + raise IndexError("Requested %s keyframe frame number '%d' is out of range for a span that's '%d' frames long" % (keyframeType, frameNumber, self.frameCount)) + + outerKeyframeHash = None + if keyframeType == 'complex': + outerKeyframeHash = self.complexParameterKeyframes + else: #keyframeType == 'float': + outerKeyframeHash = self.floatParameterKeyframes + + + answerKeyframes = defaultdict(list) + # answerKeyframes['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] + + for currParamName, keyframeHash in outerKeyframeHash.items(): + #print("Looking at %s" % currParamName) + # Direct hit + if frameNumber in keyframeHash: + targetKeyframe = keyframeHash[frameNumber] + targetKeyframe.lastObservedFrameNumber = frameNumber + answerKeyframes[currParamName].append((targetKeyframe, targetKeyframe)) + continue + + previousKeyframe = None + nextKeyframe = None + for currFrameNumber in sorted(keyframeHash.keys()): + if currFrameNumber <= frameNumber: + previousKeyframe = keyframeHash[currFrameNumber] + previousKeyframe.lastObservedFrameNumber = currFrameNumber + if currFrameNumber > frameNumber: + nextKeyframe = keyframeHash[currFrameNumber] + nextKeyframe.lastObservedFrameNumber = currFrameNumber + break # Past the sorted range, so done looking + + if previousKeyframe != None and nextKeyframe != None: + answerKeyframes[currParamName].append((previousKeyframe, nextKeyframe)) + + #print("found: %s" % str(answerKeyframes)) + return answerKeyframes + def interpolateCenterValueBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): """ Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe @@ -856,27 +741,10 @@ def interpolateCenterValueBetweenKeyframes(self, frameNumber, leftKeyframe, righ # # And I kept all these separate, because I'm prety sure there will be more # interpolation-specific parameters needed when all's said and done. - if transitionType == 'log-to': - interpolatedReal = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - elif transitionType == 'root-to': - interpolatedReal = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - elif transitionType == 'linear': - interpolatedReal = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - elif transitionType == 'quadratic-to': - interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - elif transitionType == 'quadratic-from': - interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - else: # transitionType == 'quadratic-to-from': - interpolatedReal = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - - interpolatedCenter = self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) - return interpolatedCenter + interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) + + return self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) def interpolateWindowValuesBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): """ @@ -899,27 +767,75 @@ def interpolateWindowValuesBetweenKeyframes(self, frameNumber, leftKeyframe, rig # And I kept all these separate, because I'm prety sure there will be more # interpolation-specific parameters needed when all's said and done. - if transitionType == 'log-to': - interpolatedRealWidth = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateLogTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - elif transitionType == 'root-to': - interpolatedRealWidth = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateRootTo(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - elif transitionType == 'linear': - interpolatedRealWidth = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateLinear(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - elif transitionType == 'quadratic-to': - interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - elif transitionType == 'quadratic-from': - interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseIn(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - else: # transitionType == 'quadratic-to-from': - interpolatedRealWidth = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolateQuadraticEaseInOut(leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + interpolatedRealWidth = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) + interpolatedImagWidth = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) + + #print("window interpolates from: (%s,%s) to: (%s,%s) as: (%s,%s)" % (leftKeyframe.realWidth, leftKeyframe.imagWidth, rightKeyframe.realWidth, rightKeyframe.imagWidth, interpolatedRealWidth, interpolatedImagWidth)) return (interpolatedRealWidth, interpolatedImagWidth) + def interpolateComplexBetweenParameterKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + """ + Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe + """ + #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) + + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return leftKeyframe.value + + if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: + raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + + # May want to consider 'close' rather than equal for the center equivalence check. + if leftKeyframe.value == rightKeyframe.value: + return leftKeyframe.value + + # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn + if leftKeyframe.transitionOut != rightKeyframe.transitionIn: + raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) + transitionType = leftKeyframe.transitionOut + + # Python scopes seep like this, right? Just use the value later? + # + # And I kept all these separate, because I'm prety sure there will be more + # interpolation-specific parameters needed when all's said and done. + interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.value.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value.real, frameNumber) + interpolatedImag = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.value.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value.imag, frameNumber) + + return self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) + + def interpolateFloatBetweenParameterKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + """ + Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe + """ + #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) + + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return leftKeyframe.value + + if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: + raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + + # May want to consider 'close' rather than equal for the center equivalence check. + if leftKeyframe.value == rightKeyframe.value: + return leftKeyframe.value + + # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn + if leftKeyframe.transitionOut != rightKeyframe.transitionIn: + raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) + transitionType = leftKeyframe.transitionOut + + # Python scopes seep like this, right? Just use the value later? + # + # And I kept all these separate, because I'm prety sure there will be more + # interpolation-specific parameters needed when all's said and done. + interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedKeyframeNumber, leftKeyframe.value, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value, frameNumber) + + return interpolatedReal + + def __repr__(self): return """\ [DiveTimelineSpan {framecount} frames]\ @@ -966,6 +882,11 @@ def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', self.widthFactor = widthFactor self.heightFactor = heightFactor +class DiveSpanParameterKeyframe(DiveSpanKeyframe): + def __init__(self, span, value, transitionIn='quadratic-to', transitionOut='quadratic-from'): + super().__init__(span, transitionIn, transitionOut) + self.value = value + #### # Big bunch of 'still thinking about' comments here. Probably should have kept them on my own branch. ### @@ -1110,130 +1031,142 @@ def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', # Default settings for the dive. All of these can be overridden from the # command line # -- -def set_demo1_params(mandl_ctx, view_ctx): +def set_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default mandelbrot dive params") - mandl_ctx.img_width = 1024 - mandl_ctx.img_height = 768 + fractal_ctx.img_width = 1024 + fractal_ctx.img_height = 768 cmplx_width_str = '5.0' cmplx_height_str = '3.5' - mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(cmplx_width_str) - mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(cmplx_height_str) + fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) + fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) # This is close t Misiurewicz point M32,2 - # mandl_ctx.cmplx_center = mandl_ctx.ctxc(-.77568377, .13646737) + # fractal_ctx.cmplx_center = fractal_ctx.ctxc(-.77568377, .13646737) #center_real_str = '-1.769383179195515018213' #center_imag_str = '0.00423684791873677221' - #mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_real_str, center_imag_str) + #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) center_str = '-1.769383179195515018213+0.00423684791873677221j' - mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(center_str) + fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_str) - mandl_ctx.project_name = 'demo1' + fractal_ctx.project_name = 'demo1' - mandl_ctx.scaling_factor = .90 + fractal_ctx.scaling_factor = .90 - mandl_ctx.max_iter = 255 + fractal_ctx.max_iter = 255 - mandl_ctx.escape_rad = 2. - #mandl_ctx.escape_rad = 32768. + fractal_ctx.escape_rad = 2. + #fractal_ctx.escape_rad = 32768. - mandl_ctx.verbose = 3 - mandl_ctx.burn_in = True - mandl_ctx.build_cache=True + fractal_ctx.verbose = 3 + fractal_ctx.build_cache=True view_ctx.duration = 2.0 + #view_ctx.duration = 1.0 #view_ctx.duration = 0.25 # FPS still isn't set quite right, but we'll get it there eventually. view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 29.97 / 2.0 -def set_julia_walk_demo1_params(mandl_ctx, view_ctx): +def set_julia_walk_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default julia walk params") - mandl_ctx.img_width = 1024 - mandl_ctx.img_height = 768 + fractal_ctx.img_width = 1024 + fractal_ctx.img_height = 768 cmplx_width_str = '3.2' cmplx_height_str = '2.5' - mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(cmplx_width_str) - mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(cmplx_height_str) + fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) + fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) + - mandl_ctx.fractal = 'julia' - mandl_ctx.julia_list = [mandl_ctx.math_support.createComplex(0.355,0.355), mandl_ctx.math_support.createComplex(0.0,0.8), mandl_ctx.math_support.createComplex(0.3355,0.355)] + #fractal_ctx.algorithm_extra_params['julia_center'] = fractal_ctx.math_support.createComplex(-.8,.145) + fractal_ctx.algorithm_extra_params['julia']['julia_center'] = fractal_ctx.math_support.createComplex(-.8,.145) - mandl_ctx.cmplx_center = mandl_ctx.math_support.createComplex(0,0) + #fractal_ctx.algorithm_extra_params['julia_center'] = fractal_ctx.math_support.createComplex(0,0) + fractal_ctx.julia_list = [fractal_ctx.math_support.createComplex(0.355,0.355), fractal_ctx.math_support.createComplex(0.0,0.8), fractal_ctx.math_support.createComplex(0.3355,0.355)] - mandl_ctx.project_name = 'julia_demo1' + fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(0,0) - mandl_ctx.scaling_factor = 1.0 + fractal_ctx.project_name = 'julia_demo1' - mandl_ctx.max_iter = 255 + fractal_ctx.scaling_factor = 1.0 - mandl_ctx.escape_rad = 2. - #mandl_ctx.escape_rad = 32768. + fractal_ctx.max_iter = 255 - mandl_ctx.verbose = 3 - mandl_ctx.burn_in = True - mandl_ctx.build_cache = True + fractal_ctx.escape_rad = 2. + #fractal_ctx.escape_rad = 32768. + + fractal_ctx.verbose = 3 + fractal_ctx.build_cache = True view_ctx.duration = 2.0 + #view_ctx.duration = 0.5 # FPS still isn't set quite right, but we'll get it there eventually. view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 29.97 / 2.0 -def set_preview_mode(mandl_ctx, view_ctx): +def set_preview_mode(fractal_ctx, view_ctx): print("+ Running in preview mode ") - mandl_ctx.img_width = 300 - mandl_ctx.img_height = 200 + fractal_ctx.img_width = 300 + fractal_ctx.img_height = 200 - mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(3.0) - mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(2.5) + fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(3.0) + fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(2.5) - mandl_ctx.scaling_factor = .75 + fractal_ctx.scaling_factor = .75 - #mandl_ctx.escape_rad = 4. - mandl_ctx.escape_rad = 32768. + #fractal_ctx.escape_rad = 4. + fractal_ctx.escape_rad = 32768. view_ctx.duration = 4 view_ctx.fps = 4 -def set_snapshot_mode(mandl_ctx, view_ctx, snapshot_filename='snapshot.gif'): +def set_snapshot_mode(fractal_ctx, view_ctx, snapshot_filename='snapshot.gif'): print("+ Running in snapshot mode ") - mandl_ctx.snapshot = True + fractal_ctx.snapshot = True view_ctx.vfilename = snapshot_filename - mandl_ctx.img_width = 3000 - mandl_ctx.img_height = 2000 + fractal_ctx.img_width = 3000 + fractal_ctx.img_height = 2000 - mandl_ctx.max_iter = 2000 + fractal_ctx.max_iter = 2000 - mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(3.0) - mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(2.5) + fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(3.0) + fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(2.5) - mandl_ctx.scaling_factor = .99 # set so we can zoom in more accurately + fractal_ctx.scaling_factor = .99 # set so we can zoom in more accurately - #mandl_ctx.escape_rad = 4. - mandl_ctx.escape_rad = 32768. + #fractal_ctx.escape_rad = 4. + fractal_ctx.escape_rad = 32768. view_ctx.duration = 0 view_ctx.fps = 0 -def parse_options(mandl_ctx, view_ctx): +def parse_options(fractal_ctx, view_ctx): argv = sys.argv[1:] - opts, args = getopt.getopt(argv, "pd:m:s:f:z:w:h:c:", + opts, args = getopt.getopt(argv, "pd:m:s:f:w:h:c:a:", ["preview", + "algo=", + "flint", + "gmp", "demo", "demo-julia-walk", "duration=", "fps=", "clip-start-frame=", "clip-total-frames=", + "project-name=", + "shared-cache-path=", + "build-cache", + "invalidate-cache", + "banner", "max-iter=", "img-w=", "img-h=", @@ -1242,41 +1175,53 @@ def parse_options(mandl_ctx, view_ctx): "center=", "scaling-factor=", "snapshot=", - "zoom=", "gif=", "mpeg=", "verbose=", - "julia-walk=", - "center=", "palette-test=", "color=", - "burn", - "flint", - "gmp", - "project-name=", - "shared-cache-path=", - "build-cache", - "invalidate-cache", - "banner", - "smooth"]) - - # Math support as to be handled first, so other parameter - # instantiations are properly typed + "julia-center=", # Julia + "julia-list=", # Julia + "burn", # Hopefully all algorithms? + "escape_radius", # Mandelbrot, Julia + "max_escape_iterations", # Mandelbrot, Julia + "smooth", # Mandelbrot, Julia + ]) + + + # First-pass parameters handled so others can be responsive + # - Math support, so instantiations are properly typed + # - Algorithm name, so additional parameters can be read for opt, arg in opts: if opt in ['--gmp']: - mandl_ctx.math_support = fm.DiveMathSupportGmp() + fractal_ctx.math_support = fm.DiveMathSupportGmp() elif opt in ['--flint']: - mandl_ctx.math_support = fm.DiveMathSupportFlint() + fractal_ctx.math_support = fm.DiveMathSupportFlint() + elif opt in ['-a', '--algo']: + if str(arg) in fractal_ctx.algorithm_map: + fractal_ctx.algorithm_name = str(arg) + + if fractal_ctx.algorithm_name is None: + fractal_ctx.algorithm_name = 'mandelbrot' + + # Kinda a crazy invocation. Loads algorithm-specific parameters into + # a dictionary, based on that algorithm's static class parse function. + fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] = fractal_ctx.algorithm_map[fractal_ctx.algorithm_name].parse_options(opts) + # Theoretically possible we'll eventually want to run this for all + # possible algorithm types, but for now, just loading for the + # 'active' algorithm. for opt,arg in opts: if opt in ['-p', '--preview']: - set_preview_mode(mandl_ctx, view_ctx) + set_preview_mode(fractal_ctx, view_ctx) elif opt in ['-s', '--snapshot']: - set_snapshot_mode(mandl_ctx, view_ctx, arg) + set_snapshot_mode(fractal_ctx, view_ctx, arg) elif opt in ['--demo']: - set_demo1_params(mandl_ctx, view_ctx) + set_demo1_params(fractal_ctx, view_ctx) elif opt in ['--demo-julia-walk']: - set_julia_walk_demo1_params(mandl_ctx, view_ctx) + set_julia_walk_demo1_params(fractal_ctx, view_ctx) + + palette = fp.FractalPalette() # Will get stashed for the algorithm to use for opt, arg in opts: if opt in ['-d', '--duration']: @@ -1284,76 +1229,72 @@ def parse_options(mandl_ctx, view_ctx): elif opt in ['-f', '--fps']: view_ctx.fps = float(arg) elif opt in ['--clip-start-frame']: - mandl_ctx.clip_start_frame = int(arg) + fractal_ctx.clip_start_frame = int(arg) elif opt in ['--clip-total-frames']: - mandl_ctx.clip_total_frames = int(arg) + fractal_ctx.clip_total_frames = int(arg) elif opt in ['-m', '--max-iter']: - mandl_ctx.max_iter = int(arg) + fractal_ctx.max_iter = int(arg) elif opt in ['-w', '--img-w']: - mandl_ctx.img_width = int(arg) + fractal_ctx.img_width = int(arg) elif opt in ['-h', '--img-h']: - mandl_ctx.img_height = int(arg) + fractal_ctx.img_height = int(arg) elif opt in ['--cmplx-w']: - mandl_ctx.cmplx_width = mandl_ctx.math_support.createFloat(arg) + fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(arg) elif opt in ['--cmplx-h']: - mandl_ctx.cmplx_height = mandl_ctx.math_support.createFloat(arg) + fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(arg) elif opt in ['-c', '--center']: - mandl_ctx.cmplx_center= mandl_ctx.math_support.createComplex(arg) + fractal_ctx.cmplx_center= fractal_ctx.math_support.createComplex(arg) elif opt in ['--scaling-factor']: - mandl_ctx.scaling_factor = float(arg) + fractal_ctx.scaling_factor = float(arg) elif opt in ['-z', '--zoom']: - mandl_ctx.set_zoom_level = int(arg) - elif opt in ['--smooth']: - mandl_ctx.smoothing = True + fractal_ctx.set_zoom_level = int(arg) + + # Worth noting, julia-list is used only for Timeline construction, + # and isn't an intrisic thing to the Algo. elif opt in ['--julia-list']: - mandl_ctx.fractal = 'julia' raw_julia_list = eval(arg) # expects a list of complex numbers if len(raw_julia_list) <= 1: print("Error: List of complex numbers for Julia walk must be at least two points") sys.exit(0) julia_list = [] for currCenter in raw_julia_list: - julia_list.append(mandl_ctx.math_support.create_complex(currCenter)) - mandl_ctx.julia_list = julia_list + julia_list.append(fractal_ctx.math_support.create_complex(currCenter)) + fractal_ctx.julia_list = julia_list elif opt in ['--palette-test']: - m = fp.MandlPalette() if str(arg) == "gauss": - m.create_gauss_gradient((255,255,255),(0,0,0)) + palette.create_gauss_gradient((255,255,255),(0,0,0)) elif str(arg) == "exp": - m.create_exp_gradient((255,255,255),(0,0,0)) + palette.create_exp_gradient((255,255,255),(0,0,0)) elif str(arg) == "exp2": - m.create_exp2_gradient((0,0,0),(128,128,128)) + palette.create_exp2_gradient((0,0,0),(128,128,128)) elif str(arg) == "list": - m.create_gradient_from_list() + palette.create_gradient_from_list() else: print("Error: --palette-test arg must be one of gauss|exp|list") sys.exit(0) - m.display() + palette.display() sys.exit(0) elif opt in ['--color']: - m = fp.MandlPalette() if str(arg) == "gauss": - m.create_gauss_gradient((255,255,255),(0,0,0)) + palette.create_gauss_gradient((255,255,255),(0,0,0)) elif str(arg) == "exp": - m.create_exp_gradient((255,255,255),(0,0,0)) + palette.create_exp_gradient((255,255,255),(0,0,0)) elif str(arg) == "exp2": - m.create_exp2_gradient((0,0,0),(128,128,128)) + palette.create_exp2_gradient((0,0,0),(128,128,128)) elif str(arg) == "list": - m.create_gradient_from_list() + palette.create_gradient_from_list() else: print("Error: --palette-test arg must be one of gauss|exp|list") sys.exit(0) - mandl_ctx.palette = m - elif opt in ['--burn']: - mandl_ctx.burn_in = True + fractal_ctx.palette = palette elif opt in ['--project-name']: - mandl_ctx.project_name = arg + fractal_ctx.project_name = arg elif opt in ['--shared-cache-path']: - mandl_ctx.shared_cache_path = arg + fractal_ctx.shared_cache_path = arg elif opt in ['--build-cache']: - mandl_ctx.build_cache = True + fractal_ctx.build_cache = True elif opt in ['--invalidate-cache']: - mandl_ctx.invalidate_cache = True + fractal_ctx.invalidate_cache = True elif opt in ['--banner']: view_ctx.banner = True elif opt in ['--verbose']: @@ -1361,7 +1302,7 @@ def parse_options(mandl_ctx, view_ctx): if verbosity not in [0,1,2,3]: print("Invalid verbosity level (%d) use range 0-3"%(verbosity)) sys.exit(0) - mandl_ctx.verbose = verbosity + fractal_ctx.verbose = verbosity elif opt in ['--gif']: if view_ctx.vfilename != None: print("Error : Already specific media type %s"%(view_ctx.vfilename)) @@ -1373,15 +1314,18 @@ def parse_options(mandl_ctx, view_ctx): sys.exit(0) view_ctx.vfilename = arg + # Stash the palette as an extra parameter + extra_params = fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] + extra_params['palette'] = palette if __name__ == "__main__": - print("++ mandlebort.py version %s" % (MANDL_VER)) + print("++ fractal.py version %s" % (MANDL_VER)) - mandl_ctx = MandlContext() - view_ctx = MediaView(16, 16, mandl_ctx) + fractal_ctx = FractalContext() + view_ctx = MediaView(16, 16, fractal_ctx) - parse_options(mandl_ctx, view_ctx) + parse_options(fractal_ctx, view_ctx) view_ctx.setup() view_ctx.run() diff --git a/fractalcache.py b/fractalcache.py index 7f5c3d8..466508f 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -12,32 +12,33 @@ import struct import pickle -from decimal import Decimal - CACHE_VER = 0.11 class FrameInfo: - def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): self.mesh_width = mesh_width self.mesh_height = mesh_height self.center = center self.complex_real_width = complex_real_width self.complex_imag_width = complex_imag_width - self.escape_r = escape_r - self.max_escape_iter = max_escape_iter self.raw_values = raw_values self.raw_histogram = raw_histogram self.smooth_values = smooth_values self.smooth_histogram = smooth_histogram - def emptyCopy(self): + def empty_copy(self): """ Looks like storing strings of everything makes us pickle-proof? """ - return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter)) - #return FrameInfo(self.mesh_width, self.mesh_height, self.center, self.complex_real_width, self.complex_imag_width, self.escape_r, self.max_escape_iter) + return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width)) + + def pickle_copy(self): + return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) - def pickleCopy(self): - return FrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + def data_is_loaded(self): + # This returns whether values AND histogram aren't empty + if self.raw_values is None or self.raw_histogram is None: + return False + return len(self.raw_values) != 0 and len(self.raw_histogram) != 0 def __repr__(self): return """\ @@ -47,8 +48,8 @@ def __repr__(self): class Frame: """ Two different results caches available, one for uniform meshes - at "", and a project-specific one for - non-uniform meshes at "_cache". + at "", and a project-specific one for + non-uniform meshes at "_cache". The project-specific cache uses just frame numbers for cache file names. The shared cache uses a hash of this frame's FrameInfo for cache file names. @@ -60,26 +61,28 @@ class Frame: shared_cache/v_0.11/flint/julia/feedbabe And the project called 'demo1' would have non-uniform meshes cached at: - demo1/v_0.11/native/mandelbrot/123.pik - demo1/v_0.11/flint/mandelbrot/123.pik - demo1/v_0.11/native/julia/12.pik - demo1/v_0.11/flint/julia/13.pik + demo1_cache/v_0.11/native/mandelbrot/123.pik + demo1_cache/v_0.11/flint/mandelbrot/123.pik + demo1_cache/v_0.11/native/julia/12.pik + demo1_cache/v_0.11/flint/julia/13.pik """ - def __init__(self, timeline, dive_mesh, frame_number=-1, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + def __init__(self, project_folder_name, shared_cache_path, algorithm_name, dive_mesh, frame_info, frame_number=-1): self.cache_version = CACHE_VER - self.timeline = timeline + + self.project_folder_name = project_folder_name + self.shared_cache_path = shared_cache_path + self.algorithm_name = algorithm_name self.dive_mesh = dive_mesh + self.frame_info = frame_info self.frame_number = frame_number - self.frame_info = FrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.dive_mesh.escapeRadius, self.dive_mesh.maxEscapeIterations, raw_values, raw_histogram, smooth_values, smooth_histogram) - def create_results_file_name(self): return os.path.join(self.create_results_subpath(), self.create_results_file_identifier()) def create_results_file_identifier(self): if self.dive_mesh.isUniform(): ba = None - ba = pickle.dumps(self.frame_info.emptyCopy()) + ba = pickle.dumps(self.frame_info.empty_copy()) h = hmac.new(ba, digestmod=hashlib.sha1) return h.hexdigest() else: @@ -87,12 +90,12 @@ def create_results_file_identifier(self): def create_results_subpath(self, mkdir_if_needed=False): if self.dive_mesh.isUniform(): - root_cache_path = self.timeline.sharedCachePath + root_cache_path = self.shared_cache_path else: - root_cache_path = u"%s_cache" % self.timeline.projectFolderName + root_cache_path = u"%s_cache" % self.project_folder_name version_subdir = u"v_%s" % str(self.cache_version) - results_subpath = os.path.join(root_cache_path, version_subdir, self.timeline.mathSupport.precisionType, self.timeline.fractalType) + results_subpath = os.path.join(root_cache_path, version_subdir, self.dive_mesh.mathSupport.precisionType, self.algorithm_name) if mkdir_if_needed and not os.path.exists(results_subpath): os.makedirs(results_subpath) return results_subpath @@ -105,11 +108,11 @@ def write_results_cache(self): #print("Writing: %s" % str(self.frame_info)) # Probably a mistake to write a no-data cache file, so panic. - if self.frame_info.raw_values is None or self.frame_info.smooth_values is None: + if self.frame_info.raw_values is None or self.frame_info.raw_histogram is None: raise ValueError("Aborting cache file write of missing data to \"%s\"" % filename) with open(filename, 'wb') as fd: - pickle.dump(self.frame_info.pickleCopy(),fd) + pickle.dump(self.frame_info.pickle_copy(),fd) def read_results_cache(self): filename = self.create_results_file_name() @@ -123,7 +126,9 @@ def read_results_cache(self): with open(filename, 'rb') as fd: frame_data = pickle.load(fd) - + +# I'd like to enforce matching, but float values don't like +# behaving perfectly. # assert frame_data.mesh_width == self.frame_info.mesh_width # assert frame_data.mesh_height == self.frame_info.mesh_height # assert frame_data.center == self.frame_info.center @@ -132,10 +137,7 @@ def read_results_cache(self): # assert frame_data.escape_r == self.frame_info.escape_r # assert frame_data.max_escape_iter == self.frame_info.max_escape_iter - self.frame_info.raw_values = frame_data.raw_values - self.frame_info.raw_histogram = frame_data.raw_histogram - self.frame_info.smooth_values = frame_data.smooth_values - self.frame_info.smooth_histogram = frame_data.smooth_histogram + self.frame_info = frame_data return @@ -143,6 +145,8 @@ def remove_from_results_cache(self): cache_file_name = self.create_results_file_name() if os.path.exists(cache_file_name) and os.path.isfile(cache_file_name): os.remove(cache_file_name) + # Kinda don't need to wipe these, but might help not + # getting objects mixed up? self.raw_values = None self.raw_histogram = None self.smooth_values = None diff --git a/fractalmath.py b/fractalmath.py index 7ea5e21..e2f5c2f 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -79,6 +79,25 @@ def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): """ return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) + def interpolate(self, transitionType, startX, startY, endX, endY, targetX, extraParams={}): + """ + Resisted having a single call point, in case more parameters + were needed for each type, but we might just be able to pass + in an extra param hash? + """ + if transitionType == 'log-to': + return self.interpolateLogTo(startX, startY, endX, endY, targetX) + elif transitionType == 'root-to': + return self.interpolateRootTo(startX, startY, endX, endY, targetX) + elif transitionType == 'linear': + return self.interpolateLinear(startX, startY, endX, endY, targetX) + elif transitionType == 'quadratic-to': + return self.interpolateQuadraticEaseOut(startX, startY, endX, endY, targetX) + elif transitionType == 'quadratic-from': + return self.interpolateQuadraticEaseIn(startX, startY, endX, endY, targetX) + else: # transitionType == 'quadratic-to-from' + return self.interpolateQuadraticEaseInOut(startX, startY, endX, endY, targetX) + def interpolateLogTo(self, startX, startY, endX, endY, targetX): """ Probably want additional log-defining params, but for now, let's just bake in one equation @@ -234,8 +253,8 @@ def julia(self, c, z0, escapeRadius, maxIter): c = complex(-0.4, 0.6) c = complex(-0.7269, 0.1889) - Looks like this implementation is able to handle flint types, now that smoothing - is handled separately. + Looks like this implementation is able to handle flint types, + now that smoothing is handled separately. fabs(z) returns the modulus of a complex number, which is the distance to 0 (on a 2x2 cartesian plane) diff --git a/fractalpalette.py b/fractalpalette.py index 939500b..f2f53f6 100644 --- a/fractalpalette.py +++ b/fractalpalette.py @@ -5,13 +5,15 @@ # # -- +from collections import defaultdict + import math import numpy as np def gaussian(x, mu, sig): return np.exp(-np.power(x - mu, 2.) / (2 * np.power(sig, 2.))) -class MandlPalette: +class FractalPalette: """ Color gradient """ @@ -19,7 +21,53 @@ class MandlPalette: # Color in RGB def __init__(self): self.gradient_size = 1024 - self.palette = [] + self.palette = [] + + self.hues = None + self.histogram = None + self.per_frame_reset() + + +# # called for each pixel +# def raw_calc_from_algo(self, m): +# if m < self.context.max_iter: +# self.histogram[math.floor(m)] += 1 + + def calc_hues(self, value_range): + #- + # From histogram normalize to percent-of-total. This is + # effectively a probability distribution of escape values + # + # Note that this is not effecitly a probability distribution for + # a given escape value. We can use this to calculate the Shannon + + total = sum(self.histogram.values()) + h = 0 + + for i in range(value_range): + if total : + h += self.histogram[i] / total + self.hues.append(h) + self.hues.append(h) + + def per_frame_reset(self): + self.hues = [] + self.histogram = defaultdict(int) + + + def map_value_to_color(self, m, smoothing=False): + + if len(self.palette) == 0: + c = 255 - int(255 * self.hues[math.floor(m)]) + return (c, c, c) + + if smoothing: + c1 = self.palette[1024 - int(1024 * self.hues[math.floor(m)])] + c2 = self.palette[1024 - int(1024 * self.hues[math.ceil(m)])] + return fp.FractalPalette.linear_interpolate(c1,c2,.5) + else: + c = self.palette[1024 - int(1024 * self.hues[math.floor(m)])] + return c def linear_interpolate(color1, color2, fraction): @@ -42,7 +90,7 @@ def create_gauss_gradient(self, c1, c2, mu=0.0, sigma=1.0): x = 0.0 while len(self.palette) <= self.gradient_size: g = gaussian(x,0,.10) - c = MandlPalette.linear_interpolate(c1, c2, g) + c = FractalPalette.linear_interpolate(c1, c2, g) self.palette.append(c) x = x + (1./(self.gradient_size+1)) @@ -59,7 +107,7 @@ def create_exp_gradient(self, c1, c2, decay_const = 1.01): while len(self.palette) <= self.gradient_size: fraction = math.pow(math.e,-15.*x) - c = MandlPalette.linear_interpolate(c1, c2, fraction) + c = FractalPalette.linear_interpolate(c1, c2, fraction) self.palette.append(c) x = x + (1. / 1025) @@ -78,7 +126,7 @@ def create_exp2_gradient(self, c1, c2, decay_const = 1.01): # Do a very quick decent for the first 1/16 while len(self.palette) <= float(self.gradient_size)/32.: fraction = math.pow(math.e,-30.*x) - c = MandlPalette.linear_interpolate((255,255,255), c1, fraction) + c = FractalPalette.linear_interpolate((255,255,255), c1, fraction) self.palette.append(c) x = x + (1. / float(self.gradient_size)) @@ -87,7 +135,7 @@ def create_exp2_gradient(self, c1, c2, decay_const = 1.01): # Do another quick decent back to first color for the next 1/16 while len(self.palette) <= 2.*(float(self.gradient_size) / 16.): fraction = math.pow(math.e,-15.*x) - c = MandlPalette.linear_interpolate((255,255,255), last_c, fraction) + c = FractalPalette.linear_interpolate((255,255,255), last_c, fraction) self.palette.append(c) x = x + (1. / float(self.gradient_size)) @@ -96,7 +144,7 @@ def create_exp2_gradient(self, c1, c2, decay_const = 1.01): # Do another quick decent back to first color for the next 1/16 while len(self.palette) <= 2.*(float(self.gradient_size) / 16.): fraction = math.pow(math.e,-5.*x) - c = MandlPalette.linear_interpolate((255,255,255), last_c, fraction) + c = FractalPalette.linear_interpolate((255,255,255), last_c, fraction) self.palette.append(c) x = x + (1. / float(self.gradient_size)) @@ -105,7 +153,7 @@ def create_exp2_gradient(self, c1, c2, decay_const = 1.01): x = 0.0 while len(self.palette) <= self.gradient_size : fraction = math.pow(math.e,-2.*x) - c = MandlPalette.linear_interpolate((255,255,255),last_c, fraction) + c = FractalPalette.linear_interpolate((255,255,255),last_c, fraction) self.palette.append(c) x = x + (1. / float(self.gradient_size)) @@ -118,7 +166,7 @@ def create_normal_gradient(self, c1, c2, decay_const = 1.05): fraction = 1. while len(self.palette) <= self.gradient_size: - c = MandlPalette.linear_interpolate(c1, c2, fraction) + c = FractalPalette.linear_interpolate(c1, c2, fraction) self.palette.append(c) fraction = fraction / decay_const @@ -137,10 +185,10 @@ def create_gradient_from_list(self, color_list = [(255,255,255),(0,0,0),(255,255 #the first few colors are critical, so just fill by hand. self.palette.append((0,0,0)) self.palette.append((0,0,0)) - self.palette.append(MandlPalette.linear_interpolate((0,0,0),(255,255,255),.2)) - self.palette.append(MandlPalette.linear_interpolate((0,0,0),(255,255,255),.4)) - self.palette.append(MandlPalette.linear_interpolate((0,0,0),(255,255,255),.6)) - self.palette.append(MandlPalette.linear_interpolate((0,0,0),(255,255,255),.8)) + self.palette.append(FractalPalette.linear_interpolate((0,0,0),(255,255,255),.2)) + self.palette.append(FractalPalette.linear_interpolate((0,0,0),(255,255,255),.4)) + self.palette.append(FractalPalette.linear_interpolate((0,0,0),(255,255,255),.6)) + self.palette.append(FractalPalette.linear_interpolate((0,0,0),(255,255,255),.8)) # The magic number 6 here just denotes the previous colors we # filled by hand @@ -149,7 +197,7 @@ def create_gradient_from_list(self, color_list = [(255,255,255),(0,0,0),(255,255 for c in range(0, len(color_list) - 1): for i in range(0, section_size+1): fraction = float(i)/float(section_size) - new_color = MandlPalette.linear_interpolate(color_list[c], color_list[c+1], fraction) + new_color = FractalPalette.linear_interpolate(color_list[c], color_list[c+1], fraction) self.palette.append(new_color) while len(self.palette) < self.gradient_size: c = self.palette[-1] @@ -183,4 +231,4 @@ def __getitem__(self, index): def display(self): clip = mpy.VideoClip(self.make_frame, duration=64) clip.preview(fps=1) #fps 1 is really all that works -## MandlPalette +## FractalPalette From 27143e008941ba4b904b97d83be5c3105ecdbd5a Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 28 Jul 2021 23:33:38 -0700 Subject: [PATCH 15/44] First bits of merging distance estimation in, though flint doesn't work yet. --- clint_runscript.sh | 10 ++++-- fractal.py | 27 +++++++++----- fractalcache.py | 2 +- fractalmath.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index f73df0e..a3aa5c8 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,6 +1,12 @@ -#python3 fractal.py --gif=native_demo.gif --color=exp2 --demo -python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --invalidate-cache +#python3 fractal.py --gif=native_demo.gif --color=exp2 --demo --burn +#python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --burn --invalidate-cache + +python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn +#python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --invalidate-cache + +#python3 fractal.py --gif=flint_distance_demo.gif --algo=mandeldistance --flint --demo --burn +#python3 fractal.py --gif=flint_distance_demo_fresh.gif --algo=mandeldistance --flint --demo --burn --invalidate-cache #python3 fractal.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 fractal.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache diff --git a/fractal.py b/fractal.py index 91ea961..ed6d05b 100644 --- a/fractal.py +++ b/fractal.py @@ -117,7 +117,7 @@ def render_frame_number(self, frame_number): #print("extra params from mesh: %s" % str(dive_mesh.extraParams)) extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? - #print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) + print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh, frame_number, self.project_name, self.shared_cache_path, self.build_cache, self.invalidate_cache, extra_params) frame_algorithm.beginning_hook() @@ -215,6 +215,7 @@ def run(self): if not self.vfilename: self.clip.preview(fps=1) #fps 1 is really all that works elif self.vfilename.endswith(".gif"): + print("fps: %s" % str(self.fps)) self.clip.write_gif(self.vfilename, fps=self.fps) elif self.vfilename.endswith(".mp4"): self.clip.write_videofile(self.vfilename, @@ -1036,8 +1037,10 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.img_width = 1024 fractal_ctx.img_height = 768 - cmplx_width_str = '5.0' - cmplx_height_str = '3.5' + #cmplx_width_str = '5.0' + #cmplx_height_str = '3.5' + cmplx_width_str = '.001' + cmplx_height_str = '.00075' fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) @@ -1046,16 +1049,19 @@ def set_demo1_params(fractal_ctx, view_ctx): #center_real_str = '-1.769383179195515018213' #center_imag_str = '0.00423684791873677221' #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) - center_str = '-1.769383179195515018213+0.00423684791873677221j' + #center_str = '-1.769383179195515018213+0.00423684791873677221j' + center_str = '-0.05+0.6805j' fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_str) fractal_ctx.project_name = 'demo1' fractal_ctx.scaling_factor = .90 - fractal_ctx.max_iter = 255 + #fractal_ctx.max_iter = 255 + fractal_ctx.max_iter = 500 - fractal_ctx.escape_rad = 2. + #fractal_ctx.escape_rad = 2. + fractal_ctx.escape_rad = math.sqrt(1024.0) #fractal_ctx.escape_rad = 32768. fractal_ctx.verbose = 3 @@ -1066,7 +1072,9 @@ def set_demo1_params(fractal_ctx, view_ctx): #view_ctx.duration = 0.25 # FPS still isn't set quite right, but we'll get it there eventually. - view_ctx.fps = 23.976 / 2.0 + view_ctx.fps = 23.976 / 4.0 + #view_ctx.fps = 23.976 / 8.0 + #view_ctx.fps = 23.976 #view_ctx.fps = 29.97 / 2.0 def set_julia_walk_demo1_params(fractal_ctx, view_ctx): @@ -1100,11 +1108,12 @@ def set_julia_walk_demo1_params(fractal_ctx, view_ctx): fractal_ctx.verbose = 3 fractal_ctx.build_cache = True - view_ctx.duration = 2.0 + view_ctx.duration = 4.0 #view_ctx.duration = 0.5 # FPS still isn't set quite right, but we'll get it there eventually. - view_ctx.fps = 23.976 / 2.0 + view_ctx.fps = 23.976 / 8.0 + #view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 29.97 / 2.0 def set_preview_mode(fractal_ctx, view_ctx): diff --git a/fractalcache.py b/fractalcache.py index 466508f..e725580 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -108,7 +108,7 @@ def write_results_cache(self): #print("Writing: %s" % str(self.frame_info)) # Probably a mistake to write a no-data cache file, so panic. - if self.frame_info.raw_values is None or self.frame_info.raw_histogram is None: + if self.frame_info.raw_values is None: raise ValueError("Aborting cache file write of missing data to \"%s\"" % filename) with open(filename, 'wb') as fd: diff --git a/fractalmath.py b/fractalmath.py index e2f5c2f..1ace813 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -285,6 +285,75 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): #return endingIter + 1 - math.log(math.log2(abs(endingZ))) return endingIter + 1 - math.log(math.log2(abs(endingZ))) + def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): +# TODO: profile to make sure the exclusion is worth the extra multiplications +# c2 = c.real*c.real + c.imag*c.imag +# # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm +# if 256.0*c2*c2 - 96.0*c2 + 32.0*c.real - 3.0 < 0.0: +# return 0.0 +# # skip computation inside M2 - http://iquilezles.org/www/articles/mset_2bulb/mset2bulb.htm +# if 16.0*(c2+2.0*c.real+1.0) - 1.0 < 0.0: +# return 0.0 + + didEscape = False + z = complex(0,0) + dz = complex(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia + + for i in range(0, maxIter): + if abs(z) > escapeRadius: + didEscape = True + break + + # Z' -> 2·Z·Z' + 1 + dz = 2.0 * (z*dz) + 1 + # Z -> Z² + c + z = z*z + c + + if didEscape == False: + return 0.0 + else: + absZ = abs(z) + return absZ * math.log(absZ) / abs(dz) + + def orig_mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): + c2 = c.real*c.real + c.imag*c.imag + # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm + if 256.0*c2*c2 - 96.0*c2 + 32.0*c.real - 3.0 < 0.0: + return 0.0 + # skip computation inside M2 - http://iquilezles.org/www/articles/mset_2bulb/mset2bulb.htm + if 16.0*(c2+2.0*c.real+1.0) - 1.0 < 0.0: + return 0.0 + + # iterate + di = 1.0; + z = complex(0.0); + m2 = 0.0; + dz = complex(0.0); + for i in range(0,maxIter): + if m2>escapeRadius : + di=0.0 + break + + # Z' -> 2·Z·Z' + 1 + dz = 2.0*complex(z.real*dz.real-z.imag*dz.imag, z.real*dz.imag + z.imag*dz.real) + complex(1.0,0.0); + + # Z -> Z² + c + z = complex( z.real*z.real - z.imag*z.imag, 2.0*z.real*z.imag ) + c; + + m2 = self.squared_modulus(z) + + # distance + # d(c) = |Z|·log|Z|/|Z'| + d = 0.5*math.sqrt(self.squared_modulus(z)/self.squared_modulus(dz))*math.log(self.squared_modulus(z)); + if di>0.5: + d=0.0 + + return d + + def squared_modulus(self, z): + return ((z.real*z.real)+(z.imag*z.imag)) + + class DiveMathSupportFlint(DiveMathSupport): """ Overrides to instantiate flint-specific complex types @@ -432,6 +501,27 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): # Note: Results in a float. We think. return float(endingIter + 1 - endingZ.abs_lower().const_log2().const_log10()) + def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): + didEscape = False + z = self.flint.acb(0,0) + dz = self.flint.acb(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia + + for i in range(0, maxIter): + if float(z.abs_lower()) > escapeRadius: + didEscape = True + break + + # Z' -> 2·Z·Z' + 1 + dz = 2.0 * (z*dz) + 1 + # Z -> Z² + c + z = z*z + c + + if didEscape == False: + return 0.0 + else: + absZ = z.abs_lower() + return float(absZ * absZ.const_log2() / dz.abs_lower()) + class DiveMathSupportGmp(DiveMathSupport): """ Overrides to instantiate gmpy2-specific complex types From f549896e963b0be5408bd200db51c7259c9e05ed Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Thu, 29 Jul 2021 16:31:42 -0700 Subject: [PATCH 16/44] seem to have forgotten to actually add the needed files. --- julia.py | 97 ++++++++++++++++++++++++ mandelbrot.py | 185 ++++++++++++++++++++++++++++++++++++++++++++++ mandeldistance.py | 108 +++++++++++++++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 julia.py create mode 100644 mandelbrot.py create mode 100644 mandeldistance.py diff --git a/julia.py b/julia.py new file mode 100644 index 0000000..dd5682d --- /dev/null +++ b/julia.py @@ -0,0 +1,97 @@ +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw, ImageFont + +from algo import JuliaFrameInfo, JuliaAlgo + +import fractalpalette as fp + +class Julia(JuliaAlgo): + + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + self.algorithm_name = 'julia' + + def calculate_results(self): + mesh_array = self.dive_mesh.generateMesh() + math_support = self.dive_mesh.mathSupport + + julia_function = np.vectorize(math_support.julia) + (pixel_values_2d, last_zees) = julia_function(self.julia_center, mesh_array, self.escape_radius, self.max_escape_iterations) + + smoothing_function = np.vectorize(math_support.smoothAfterCalculation) + pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations) + + hist = defaultdict(int) + hist_smoothed = defaultdict(int) + + for x in range(0, mesh_array.shape[0]): + for y in range(0, mesh_array.shape[1]): + # Not using mathSupport's floor() here, because it should just be a normal-scale float + if pixel_values_2d[x,y] < self.max_escape_iterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) + hist[math.floor(pixel_values_2d[x,y])] += 1 + if pixel_values_2d_smoothed[x,y] < self.max_escape_iterations: + hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 + + self.cache_frame.frame_info.raw_values = pixel_values_2d + self.cache_frame.frame_info.raw_histogram = hist + self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed + self.cache_frame.frame_info.smooth_histogram = hist_smoothed + + def pre_image_hook(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + self.palette.histogram = self.cache_frame.frame_info.smooth_histogram + else: + self.palette.histogram = self.cache_frame.frame_info.raw_histogram + + # TODO: Hey, calc_hues should get a different range of bins if + # smoothed values are floats! + self.palette.calc_hues(self.max_escape_iterations) + + def generate_image(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + pixel_values_2d = self.cache_frame.frame_info.smooth_values.T + else: + pixel_values_2d = self.cache_frame.frame_info.raw_values.T + #print("shape of things to come: %s" % str(pixel_values_2d.shape)) + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + color = self.palette.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + + return im + + def ending_hook(self): + self.palette.per_frame_reset() + + diff --git a/mandelbrot.py b/mandelbrot.py new file mode 100644 index 0000000..c86dce2 --- /dev/null +++ b/mandelbrot.py @@ -0,0 +1,185 @@ +# -- +# File: mandelbrot.py +# +# Basic escape iteration method for calculating the mandlebrot set +# +# Code cribbed from all over the place ... notably : +# +# https://www.codingame.com/playgrounds/2358/how-to-plot-the-mandelbrot-set/mandelbrot-set +# http://linas.org/art-gallery/escape/escape.html +# +# Misiurewicz points also cribbed from all over +# +# https://mrob.com/pub/muency/misiurewiczpoint.html +# https://www.youtube.com/watch?v=u1pwtSBTnPU&t=274s +# +# +# MPs: +# +# 0.4244 + 0.200759i; + +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw, ImageFont + +from algo import EscapeFrameInfo, EscapeAlgo +import fractalpalette as fp + +# TODO: probably rename these to MandelbrotEscape and MandelbrotDistanceEstimate? + +class Mandelbrot(EscapeAlgo): + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + self.algorithm_name = 'mandelbrot' + #print(dive_mesh) + + def calculate_results(self): + mesh_array = self.dive_mesh.generateMesh() + math_support = self.dive_mesh.mathSupport + + mandelbrot_function = np.vectorize(math_support.mandelbrot) + (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + + smoothing_function = np.vectorize(math_support.smoothAfterCalculation) + pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations) + + hist = defaultdict(int) + hist_smoothed = defaultdict(int) + + for x in range(0, mesh_array.shape[0]): + for y in range(0, mesh_array.shape[1]): + # Not using mathSupport's floor() here, because it should just be a normal-scale float + if pixel_values_2d[x,y] < self.max_escape_iterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) + hist[math.floor(pixel_values_2d[x,y])] += 1 + if pixel_values_2d_smoothed[x,y] < self.max_escape_iterations: + hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 + +#### +# Sorry, didn't update this iterative version of the code when +# it got moved into algo, so it won't run like this. +#### +# pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) +# pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) +# hist = defaultdict(int) +# hist_smoothed = defaultdict(int) +# +# show_row_progress = True +# for x in range(0, mesh.shape[0]): +# for y in range(0, mesh.shape[1]): +# if self.fractalType == 'julia': +# (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) +# else: # self.FractalType == 'mandelbrot' +# (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) +# +# pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) +# +# # Not using mathSupport's floor() here, because it should just be a normal-scale float +# if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: +# #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) +# hist[math.floor(pixel_values_2d[x,y])] += 1 +# if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: +# hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 +# +# if show_row_progress == True: +# print("%d-" % x, end="") +# sys.stdout.flush() + + self.cache_frame.frame_info.raw_values = pixel_values_2d + self.cache_frame.frame_info.raw_histogram = hist + self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed + self.cache_frame.frame_info.smooth_histogram = hist_smoothed + + return + + #### + # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here + # somewhere... + #### + + # In search of efficient ways to apply the map, and getting stuck with various issues + # like pickling, which are keeping me from using multiprocessing.Pool + +# Seemed to go exponential run time for some bizarre reason +# # Probably not necessary, but lining up the 2-element subarray +# pixel_inputs_1d = pixel_values_2d.reshape((mesh.shape[0] * mesh.shape[1])) +# +# # Pretty sure this is mistakenly doing an n! pass, or something just as ridiculous. +# #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) +# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeRadius, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) +# +# pixel_values_2d = pixel_values_1d.reshape((mesh.shape[0], mesh.shape[1])) +# #pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level +# #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) + + #nope + #theFunction = np.vectorize(self.mandelbrot_flint) + #pixel_values_1d = theFunction(pixel_inputs_1d) + + #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) + + #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) + #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) + #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) + + # Can't pikcle... hmm + #mandelpool = multiprocessing.Pool(processes = 1) + #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) + #mandelpool.close() + #mandelpool.join() + + def pre_image_hook(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + self.palette.histogram = self.cache_frame.frame_info.smooth_histogram + else: + self.palette.histogram = self.cache_frame.frame_info.raw_histogram + + # TODO: Hey, calc_hues should get a different range of bins if + # smoothed values are floats! + self.palette.calc_hues(self.max_escape_iterations) + + def generate_image(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + pixel_values_2d = self.cache_frame.frame_info.smooth_values.T + else: + pixel_values_2d = self.cache_frame.frame_info.raw_values.T + #print("shape of things to come: %s" % str(pixel_values_2d.shape)) + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + color = self.palette.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + + return im + + def ending_hook(self): + self.palette.per_frame_reset() + diff --git a/mandeldistance.py b/mandeldistance.py new file mode 100644 index 0000000..7bc780d --- /dev/null +++ b/mandeldistance.py @@ -0,0 +1,108 @@ +# -- +# File: mandeldistance.py +# +# +# -- + +from algo import Algo + +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw, ImageFont + +from algo import EscapeFrameInfo, EscapeAlgo +import fractalpalette as fp + + +def clamp(num, min_value, max_value): + return max(min(num, max_value), min_value) + + +class MandelDistance(EscapeAlgo): + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + self.algorithm_name = 'mandeldistance' + + def calculate_results(self): + mesh_array = self.dive_mesh.generateMesh() + math_support = self.dive_mesh.mathSupport + + #mandelbrot_function = np.vectorize(math_support.orig_mandelbrotDistanceEstimate) +# mandelbrot_function = np.vectorize(math_support.mandelbrotDistanceEstimate) +# self.cache_frame.frame_info.raw_values = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + + + mandelbrot_function = np.vectorize(math_support.mandelbrotDistanceEstimate) + (pixel_values_2d, distances) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + + rescaleFunction = np.vectorize(math_support.rescaleForRange) + (pixel_values_2d_smoothed) = rescaleFunction(distances, pixel_values_2d, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) + + self.cache_frame.frame_info.raw_values = pixel_values_2d + self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed + + return + + def generate_image(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + pixel_values_2d = self.cache_frame.frame_info.smooth_values.T + else: + pixel_values_2d = self.cache_frame.frame_info.raw_values.T + + ##pixel_values_2d = self.cache_frame.frame_info.raw_values.T + #print("shape of things to come: %s" % str(pixel_values_2d.shape)) + + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + color = self.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + + return im + + def map_value_to_color(self, raw_val): + cint = int(float(raw_val) * 255) + return (cint,cint,cint) + +# def map_value_to_color(self, raw_val): +# ## Was an error, but when val was negative, this blew up. +# val = float(raw_val) +# if val < 0.0: +# val = 0.0 +# zoo = .1 +# zoom_level = 1. / (self.dive_mesh.imagMeshGenerator.baseWidth) +# d = clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); +# #if math.isnan(d): +# # print("NAN val: %s zoom_level: %s width: %s" % (str(val), str(zoom_level), self.dive_mesh.imagMeshGenerator.baseWidth)) +# cint = int(float(d)*255) +# # Forced float() here because baseWidth is maybe a special math subtype +# +# return (cint,cint,cint) + + From 89632c336aab1f8a2664cdf6bc3d7cff5923196b Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Thu, 29 Jul 2021 16:32:07 -0700 Subject: [PATCH 17/44] still adding lost files --- clint_runscript.sh | 4 ++-- fractal.py | 25 ++++++++++--------- fractalmath.py | 60 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index a3aa5c8..ae8e943 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -2,8 +2,8 @@ #python3 fractal.py --gif=native_demo.gif --color=exp2 --demo --burn #python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --burn --invalidate-cache -python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn -#python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --invalidate-cache +#python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn +python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --invalidate-cache --smooth #python3 fractal.py --gif=flint_distance_demo.gif --algo=mandeldistance --flint --demo --burn #python3 fractal.py --gif=flint_distance_demo_fresh.gif --algo=mandeldistance --flint --demo --burn --invalidate-cache diff --git a/fractal.py b/fractal.py index ed6d05b..48e77ed 100644 --- a/fractal.py +++ b/fractal.py @@ -1037,10 +1037,12 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.img_width = 1024 fractal_ctx.img_height = 768 - #cmplx_width_str = '5.0' - #cmplx_height_str = '3.5' - cmplx_width_str = '.001' - cmplx_height_str = '.00075' + cmplx_width_str = '5.0' + cmplx_height_str = '3.5' + #cmplx_width_str = '.001' + #cmplx_height_str = '.00075' + #cmplx_width_str = '2.0' + #cmplx_height_str = str(1024*2/768) fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) @@ -1055,25 +1057,26 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.project_name = 'demo1' - fractal_ctx.scaling_factor = .90 + fractal_ctx.scaling_factor = .70 - #fractal_ctx.max_iter = 255 - fractal_ctx.max_iter = 500 + fractal_ctx.max_iter = 255 + #fractal_ctx.max_iter = 500 - #fractal_ctx.escape_rad = 2. - fractal_ctx.escape_rad = math.sqrt(1024.0) + fractal_ctx.escape_rad = 2. + #fractal_ctx.escape_rad = math.sqrt(1024.0) #fractal_ctx.escape_rad = 32768. fractal_ctx.verbose = 3 fractal_ctx.build_cache=True + #view_ctx.duration = 4.0 view_ctx.duration = 2.0 #view_ctx.duration = 1.0 #view_ctx.duration = 0.25 # FPS still isn't set quite right, but we'll get it there eventually. - view_ctx.fps = 23.976 / 4.0 - #view_ctx.fps = 23.976 / 8.0 + #view_ctx.fps = 23.976 / 4.0 + view_ctx.fps = 23.976 / 8.0 #view_ctx.fps = 23.976 #view_ctx.fps = 29.97 / 2.0 diff --git a/fractalmath.py b/fractalmath.py index 1ace813..c4fbe3d 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -298,23 +298,61 @@ def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): didEscape = False z = complex(0,0) dz = complex(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia - - for i in range(0, maxIter): - if abs(z) > escapeRadius: - didEscape = True - break - + absOfZ = 0.0 # Will re-use the final result for smoothing + n = 0 + while absOfZ < escapeRadius and n < maxIter: # Z' -> 2·Z·Z' + 1 dz = 2.0 * (z*dz) + 1 # Z -> Z² + c z = z*z + c - if didEscape == False: - return 0.0 + absOfZ = abs(z) + n += 1 + + if n == maxIter: + return (n, 0.0) else: - absZ = abs(z) - return absZ * math.log(absZ) / abs(dz) - + return (n, absOfZ * math.log(absOfZ) / abs(dz)) + +# lastIter = 0 +# for i in range(0, maxIter): +# if abs(z) > escapeRadius: +# didEscape = True +# break +# +# # Z' -> 2·Z·Z' + 1 +# dz = 2.0 * (z*dz) + 1 +# # Z -> Z² + c +# z = z*z + c +# +# lastIter += 1 +# +# if didEscape == False: +# return (lastIter, 0.0) +# else: +# absZ = abs(z) # Save an extra multiply +# return (lastIter, absZ * math.log(absZ) / abs(dz)) + + + def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): +# #print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) +# if endingIter == maxIter or rawValue <= 0.0: +# return 0.0 +# else: +# zoomValue = float(math.pow((1/scaleRange) * rawValue / 0.1, 0.1)) +# return self.clamp(zoomValue, 0.0, 1.0) + + val = float(rawValue) + if val < 0.0: + val = 0.0 + zoo = .1 + zoom_level = 1. / scaleRange + d = self.clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); + return float(d) + + def clamp(self, num, min_value, max_value): + return max(min(num, max_value), min_value) + def orig_mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): c2 = c.real*c.real + c.imag*c.imag # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm From f4cb5ac27bd70554f2357b9d6226aeacc8a08586 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Thu, 29 Jul 2021 17:35:32 -0700 Subject: [PATCH 18/44] seems like flint is functional, but not for caching --- algo.py | 202 +++++++++++++++++++++++++++++++++++++++++++++ clint_runscript.sh | 8 +- fractalmath.py | 69 +++++++++++++--- 3 files changed, 265 insertions(+), 14 deletions(-) create mode 100644 algo.py diff --git a/algo.py b/algo.py new file mode 100644 index 0000000..5f96e3d --- /dev/null +++ b/algo.py @@ -0,0 +1,202 @@ + +import fractalcache as fc +import fractalpalette as fp +import divemesh as mesh + +# Should probably import Timeline, but not technically needed, since +# it's cyclic? + +class Algo(object): + """ + Algo is responsible for generating a single, entire, image + frame from a specified mesh. + + This means the algo is responsible for generating and caching its own + intermediate results. + + Frame generation happens in two phases, with intermediate hooks available. + - beginning hook + - generate results + - pre-image hook + - generate image + - ending hook + """ + + @staticmethod + def parse_options(opts): + """ + Algorithm implementations can fill a dictionary with key/value + pairs to pass algorithm-specific parameters from the command line + into the per-frame algorithm instantiation + """ + return {} + + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache): + self.algorithm_name = None + + self.dive_mesh = dive_mesh + self.frame_number = frame_number + + self.project_folder_name = project_folder_name + self.shared_cache_path = shared_cache_path + self.build_cache = build_cache + self.invalidate_cache = invalidate_cache + + self.cache_frame = None + + def build_cache_frame(self): + raise NotImplementedError('Subclass must implement build_cache_frame()') + + def get_frame_metadata(self): + return {'frame_number' : self.frame_number, + 'fractal_type': self.algorithm_name, + 'precision_type': self.dive_mesh.mathSupport.precisionType, + 'mesh_center': str(self.dive_mesh.center), + 'complex_real_width' : str(self.dive_mesh.realMeshGenerator.baseWidth), + 'complex_imag_width' : str(self.dive_mesh.imagMeshGenerator.baseWidth), + 'mesh_is_uniform' : str(self.dive_mesh.isUniform())} + + def beginning_hook(self): + pass + + def generate_results(self): + """ + Load or calculate the frame data. + + Probably need to move this to a hierarchical subclass + eventually, but for now, the cache objects hold up to + 2 arrays and 2 histograms, so it can be used for all + our Algos. + """ + self.cache_frame = self.build_cache_frame() + + if self.invalidate_cache == True: + self.cache_frame.remove_from_results_cache() + + self.cache_frame.read_results_cache() + + if not self.cache_frame.frame_info.data_is_loaded(): + self.calculate_results() + + # Fresly calculated results get saved if we're building the cache + if self.build_cache == True: + self.cache_frame.write_results_cache() + + return + + def calculate_results(self): + """ Business end of getting results for a mesh """ + raise NotImplementedError('Subclass must implement calculate_results()') + + def pre_image_hook(self): + pass + + def generate_image(self): + pass + + def ending_hook(self): + pass + +class EscapeFrameInfo(fc.FrameInfo): + def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + super().__init__(mesh_width, mesh_height, center, complex_real_width, complex_imag_width, raw_values, raw_histogram, smooth_values, smooth_histogram) + + self.escape_r = escape_r + self.max_escape_iter = max_escape_iter + + def empty_copy(self): + """ Looks like storing strings of everything makes us pickle-proof? """ + return EscapeFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter)) + + def pickle_copy(self): + return EscapeFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + + +class EscapeAlgo(Algo): + + @staticmethod + def parse_options(opts): + options = Algo.parse_options(opts) + + for opt,arg in opts: + if opt in ['--escape-radius']: + options['escape_radius'] = float(arg) + elif opt in ['--max-escape-iterations']: + options['max_escape_iterations'] = int(arg) + elif opt in ['--smooth']: + options['use_smoothing'] = True + elif opt in ['--burn']: + options['burn_in'] = True + elif opt in ['--palette']: + options['palette'] = arg + + return options + + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache) + + # Load, with optional default values + self.escape_radius = extra_params.get('escape_radius', 2.0) + self.max_escape_iterations = extra_params.get('max_escape_iterations', 255) + self.use_smoothing = extra_params.get('use_smoothing', False) + self.burn_in = extra_params.get('burn_in', False) + self.palette = extra_params.get('palette', fp.FractalPalette()) + + def build_cache_frame(self): + frame_info = EscapeFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations) + return fc.Frame(self.project_folder_name, self.shared_cache_path, self.algorithm_name, self.dive_mesh, frame_info, self.frame_number) + + def get_frame_metadata(self): + metadata = super().get_frame_metadata() + metadata['escape_radius'] = self.escape_radius + metadata['max_escape_iterations'] = self.max_escape_iterations + return metadata + +class JuliaFrameInfo(EscapeFrameInfo): + def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, julia_center, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): + super().__init__(mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values, raw_histogram, smooth_values, smooth_histogram) + + self.julia_center = julia_center + + def empty_copy(self): + """ Looks like storing strings of everything makes us pickle-proof? """ + return JuliaFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), str(self.julia_center)) + + def pickle_copy(self): + return JuliaFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), str(self.julia_center), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + +class JuliaAlgo(EscapeAlgo): + + @staticmethod + def parse_options(opts): + # Considered loading this with default values, but didn't + # want to compete with the defaults in __init__()? + options = EscapeAlgo.parse_options(opts) + + for opt,arg in opts: + if opt in ['--julia-center']: + options['julia_center'] = arg + + return options + + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + #self.julia_center = extra_params.get('julia_center', self.dive_mesh.mathSupport.createComplex(0,0) + self.julia_center = extra_params.get('julia_center', self.dive_mesh.mathSupport.createComplex(-.8,.145)) + + #print("settled on %s" % self.julia_center) + + def build_cache_frame(self): + frame_info = JuliaFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations, self.julia_center) + return fc.Frame(self.project_folder_name, self.shared_cache_path, self.algorithm_name, self.dive_mesh, frame_info, self.frame_number) + + def get_frame_metadata(self): + metadata = super().get_frame_metadata() + metadata['julia_center'] = self.julia_center + return metadata + + def beginning_hook(self): + pass + + diff --git a/clint_runscript.sh b/clint_runscript.sh index ae8e943..fb0c252 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -2,11 +2,11 @@ #python3 fractal.py --gif=native_demo.gif --color=exp2 --demo --burn #python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --burn --invalidate-cache -#python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn -python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --invalidate-cache --smooth +#python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn --smooth +python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --smooth --invalidate-cache -#python3 fractal.py --gif=flint_distance_demo.gif --algo=mandeldistance --flint --demo --burn -#python3 fractal.py --gif=flint_distance_demo_fresh.gif --algo=mandeldistance --flint --demo --burn --invalidate-cache +#python3 fractal.py --gif=flint_distance_demo.gif --algo=mandeldistance --flint --demo --burn --smooth +#python3 fractal.py --gif=flint_distance_demo_fresh.gif --algo=mandeldistance --flint --demo --burn --smooth --invalidate-cache #python3 fractal.py --gif=flint_demo.gif --color=exp2 --demo --flint #python3 fractal.py --gif=flint_demo_fresh.gif --color=exp2 --demo --flint --invalidate-cache diff --git a/fractalmath.py b/fractalmath.py index c4fbe3d..261bb9e 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -543,22 +543,71 @@ def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): didEscape = False z = self.flint.acb(0,0) dz = self.flint.acb(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia - - for i in range(0, maxIter): - if float(z.abs_lower()) > escapeRadius: - didEscape = True - break - + absOfZ = 0.0 # Will re-use the final result for smoothing + n = 0 + while float(absOfZ) < escapeRadius and n < maxIter: # Z' -> 2·Z·Z' + 1 dz = 2.0 * (z*dz) + 1 # Z -> Z² + c z = z*z + c - if didEscape == False: - return 0.0 + absOfZ = z.abs_lower() + n += 1 + + dzMag = dz.abs_lower() + if n == maxIter or dzMag == 0.0: + return (n, 0.0) else: - absZ = z.abs_lower() - return float(absZ * absZ.const_log2() / dz.abs_lower()) + return (n, float(absOfZ * absOfZ.const_log2() / dzMag)) + +# didEscape = False +# z = self.flint.acb(0,0) +# dz = self.flint.acb(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia +# +# for i in range(0, maxIter): +# if float(z.abs_lower()) > escapeRadius: +# didEscape = True +# break +# +# # Z' -> 2·Z·Z' + 1 +# dz = 2.0 * (z*dz) + 1 +# # Z -> Z² + c +# z = z*z + c +# +# if didEscape == False: +# return 0.0 +# else: +# absZ = z.abs_lower() +# return float(absZ * absZ.const_log2() / dz.abs_lower()) + + def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): +# #print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) + val = float(rawValue) + if val < 0.0: + val = 0.0 + zoo = .1 + zoom_level = 1. / scaleRange + if math.isnan(zoom_level): + print("NaN wtf 1") + return 0.0 + if zoom_level == 0: + print("zoom level zero") + return 0.0 + + pow_result = pow(zoom_level * val/zoo, 0.1) + if math.isnan(pow_result): + print("NaN wtf 2: %s * %s / %s" % (str(zoom_level), str(val), str(zoo))) + return 0.0 + if pow_result == 0: + #print("pow zero") + return 0.0 + + #d = self.clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); + d = self.clamp(pow_result, 0.0, 1.0 ); + return float(d) + + def clamp(self, num, min_value, max_value): + return max(min(num, max_value), min_value) class DiveMathSupportGmp(DiveMathSupport): """ From 450d179f7e867f7b95cd87f891146ddca53a197f Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Fri, 30 Jul 2021 17:50:46 -0700 Subject: [PATCH 19/44] attempting mac flint instructions --- env.txt | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/env.txt b/env.txt index 5346a48..e5f43c7 100644 --- a/env.txt +++ b/env.txt @@ -21,3 +21,56 @@ pip3 install ffmpeg movpiepy /usr/local/bin/python3 -m pip install flint-py + +################################################## +#Another attempt at flint, on mac/homebrew: +################################################## +# +## Not sure if any of these commands are really needed, but they're +## remembered here, just in case +# +#xcode-select --install +# +#brew install autoconf +#brew install automake +#brew install gettext +#brew install libtool +# +#autoreconf -i + + +mkdir python-flint-build +cd python-flint-build + +brew install cython +# Redundant? #pip3 install cython +pip3 install numpy + +# MPIR (a fork of GMP) +brew install mpir@3.0.0 + +# MPFR (4.x kinda matters here) +brew install mpfr@4.1.0 + +# Arb takes a while to build. Might need flint, but things got fuzzy here on resolutions +brew install arb@2.20.0 + +# Flint library +# Flint2 takes a while to build +git clone https://github.com/fredrik-johansson/flint2 +cd flint2 +./configure +make +# No test? +sudo make install +cd .. + +# Python wrapper +git clone https://github.com/python-flint/python-flint +cd python-flint +python setup.py build_ext +sudo python setup install +cd .. + + + From 3ada631c58662431f2775e242fb5588d3d0d0fd2 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 31 Jul 2021 10:45:19 -0700 Subject: [PATCH 20/44] typos in instructions --- env.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/env.txt b/env.txt index e5f43c7..c650886 100644 --- a/env.txt +++ b/env.txt @@ -68,8 +68,8 @@ cd .. # Python wrapper git clone https://github.com/python-flint/python-flint cd python-flint -python setup.py build_ext -sudo python setup install +python3 setup.py build_ext +sudo python3 setup.py install cd .. From bc911ed5e4258d162fa2a5814ae26bce030696b1 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sun, 1 Aug 2021 00:29:24 -0700 Subject: [PATCH 21/44] seems like smooth works with flint now too. --- clint_runscript.sh | 4 ++ fractal.py | 33 ++++++------ fractalmath.py | 47 ++++++++++++++--- julia.py | 2 +- mandelbrot.py | 2 +- smooth.py | 122 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 smooth.py diff --git a/clint_runscript.sh b/clint_runscript.sh index fb0c252..17d561d 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -5,6 +5,10 @@ #python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn --smooth python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --smooth --invalidate-cache +#python3 fractal.py --gif=smooth_demo.gif --algo=smooth --demo --burn --color +#python3 fractal.py --gif=smooth_demo_fresh.gif --algo=smooth --demo --burn --color --invalidate-cache +#python3 fractal.py --gif=flint_smooth_demo_fresh.gif --algo=smooth --flint --demo --burn --color --invalidate-cache + #python3 fractal.py --gif=flint_distance_demo.gif --algo=mandeldistance --flint --demo --burn --smooth #python3 fractal.py --gif=flint_distance_demo_fresh.gif --algo=mandeldistance --flint --demo --burn --smooth --invalidate-cache diff --git a/fractal.py b/fractal.py index 48e77ed..32b1a04 100644 --- a/fractal.py +++ b/fractal.py @@ -50,6 +50,7 @@ from julia import Julia from mandelbrot import Mandelbrot from mandeldistance import MandelDistance +from smooth import Smooth MANDL_VER = "0.1" @@ -96,7 +97,8 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.algorithm_map = {'julia' : Julia, 'mandelbrot' : Mandelbrot, - 'mandeldistance' : MandelDistance} + 'mandeldistance' : MandelDistance, + 'smooth': Smooth} self.algorithm_name = None self.algorithm_extra_params = {} # Keeps command-line params for later use @@ -1191,7 +1193,8 @@ def parse_options(fractal_ctx, view_ctx): "mpeg=", "verbose=", "palette-test=", - "color=", + #"color=", + "color", "julia-center=", # Julia "julia-list=", # Julia "burn", # Hopefully all algorithms? @@ -1286,19 +1289,19 @@ def parse_options(fractal_ctx, view_ctx): sys.exit(0) palette.display() sys.exit(0) - elif opt in ['--color']: - if str(arg) == "gauss": - palette.create_gauss_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp": - palette.create_exp_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp2": - palette.create_exp2_gradient((0,0,0),(128,128,128)) - elif str(arg) == "list": - palette.create_gradient_from_list() - else: - print("Error: --palette-test arg must be one of gauss|exp|list") - sys.exit(0) - fractal_ctx.palette = palette +# elif opt in ['--color']: +# if str(arg) == "gauss": +# palette.create_gauss_gradient((255,255,255),(0,0,0)) +# elif str(arg) == "exp": +# palette.create_exp_gradient((255,255,255),(0,0,0)) +# elif str(arg) == "exp2": +# palette.create_exp2_gradient((0,0,0),(128,128,128)) +# elif str(arg) == "list": +# palette.create_gradient_from_list() +# else: +# print("Error: --palette-test arg must be one of gauss|exp|list") +# sys.exit(0) +# fractal_ctx.palette = palette elif opt in ['--project-name']: fractal_ctx.project_name = arg elif opt in ['--shared-cache-path']: diff --git a/fractalmath.py b/fractalmath.py index 261bb9e..5388d10 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -272,7 +272,7 @@ def julia(self, c, z0, escapeRadius, maxIter): return (n, z) - def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): if endingIter == maxIter: return float(maxIter) else: @@ -283,7 +283,25 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): # print("iter was %d" % endingIter) #print("z: \"%s\" max_iter: %d iter: %d" % (endingZ, maxIter, endingIter)) #return endingIter + 1 - math.log(math.log2(abs(endingZ))) - return endingIter + 1 - math.log(math.log2(abs(endingZ))) + #return endingIter + 1 - math.log(math.log2(abs(endingZ)) / math.log(2.0)) + return endingIter + 1 - self.twoLogsHelper(endingZ, escapeRadius) / math.log(2.0) + + def justTwoLogs(self, value): + return math.log(math.log(value)) + + def twoLogsHelper(self, value, radius): + """ + The UNoptimized smoothing calculation. + + sn = n - ( ln( ln(abs(value))/ln(radius) ) / ln(2.0)) + + Of which, we're just running the upper part here, because the ln(2.0) + and subtraction don't need extra precision support. + + So, this calculates: + ln(ln(abs(value))/ln(radius)) + """ + return math.log(math.log(abs(value))/math.log(radius)) def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): # TODO: profile to make sure the exclusion is worth the extra multiplications @@ -526,10 +544,12 @@ def mandelbrot(self, c, escapeRadius, maxIter): return (n, z) - def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): """ This flint-specific implementation only really does flint-y logs in the smoothing. The Decimal-specific implementation needed some extra steps, but I've ditched that one. + Heh, now that twoLogsHelper exists, it looks like this flint version + is identical to the native version. Might not need it anymore. """ if endingIter == maxIter: return float(maxIter) @@ -537,7 +557,19 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. - return float(endingIter + 1 - endingZ.abs_lower().const_log2().const_log10()) + return float(endingIter + 1 - self.twoLogsHelper(endingZ, escapeRadius) / math.log(2.0)) + + def justTwoLogs(self, value): + return self.flint.arb(value).log().log() + + def twoLogsHelper(self, value, radius): + # ln(ln(abs(value))/ln(radius)) + # Note: flint needs 'value.log()' for ln, but 'value.const_log2() for log2?! + + # Also, radius is expected to be in 'normal' ranges, so just using math.log + return (value.abs_lower().log() / math.log(radius)).log() + #return math.log(math.log(abs(value))/math.log(radius)) + def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): didEscape = False @@ -687,10 +719,13 @@ def mandelbrot(self, c, escapeRadius, maxIter): n += 1 return (n, z) - def smoothAfterCalculation(self, endingZ, endingIter, maxIter): + def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): """ This flint-specific implementation only really does flint-y logs in the smoothing. The Decimal-specific implementation needed some extra steps, but I've ditched that one. + + NOTE: this wasn't updated with the new smoothing calcs like + the 'native' and 'flint' implementations were """ if endingIter == maxIter: return float(maxIter) @@ -698,7 +733,7 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter): # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. - return float(endingIter + 1 - self.gmp.log10(self.gmp.log2(self.gmp.norm(endingZ)))) + return float(endingIter + 1 - self.gmp.log10(self.gmp.log2(self.gmp.norm(endingZ))) / math.log(2.0)) #return float(endingIter + 1 - self.gmp.log10(self.gmp.log10(self.gmp.sqrt(self.gmp.square(endingZ.real) + self.gmp.square(endingZ.imag))))) diff --git a/julia.py b/julia.py index dd5682d..620822d 100644 --- a/julia.py +++ b/julia.py @@ -24,7 +24,7 @@ def calculate_results(self): (pixel_values_2d, last_zees) = julia_function(self.julia_center, mesh_array, self.escape_radius, self.max_escape_iterations) smoothing_function = np.vectorize(math_support.smoothAfterCalculation) - pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations) + pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) hist = defaultdict(int) hist_smoothed = defaultdict(int) diff --git a/mandelbrot.py b/mandelbrot.py index c86dce2..441e2d1 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -45,7 +45,7 @@ def calculate_results(self): (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) smoothing_function = np.vectorize(math_support.smoothAfterCalculation) - pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations) + pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) hist = defaultdict(int) hist_smoothed = defaultdict(int) diff --git a/smooth.py b/smooth.py new file mode 100644 index 0000000..0c07cee --- /dev/null +++ b/smooth.py @@ -0,0 +1,122 @@ +# -- +# File: smooth.py +# +# Implementation of the smoothing algorithm for iteration escape method +# of drawing mandelbrot as describe and implemented by Inigo Quilez +# +# https://iquilezles.org/www/articles/mset_smooth/mset_smooth.htm +# https://www.shadertoy.com/view/4df3Rn coloring and smoothing working +# +# +# Seashell cove - -0.745+0.186j +# +# -- +import math +import numpy as np + +from PIL import Image, ImageDraw, ImageFont + +from algo import Algo, EscapeFrameInfo, EscapeAlgo + + +class Smooth(EscapeAlgo): + + @staticmethod + def parse_options(opts): + """ Smooth ignores the parameter --smooth, because it's redundantish """ + options = EscapeAlgo.parse_options(opts) + + for opt,arg in opts: + if opt in ['--color']: + options['color'] = (.1,.2,.3) # dark + #options['color'] = (.0,.6,1.0) # blue / yellow + #options['color'] = (.0,.6,1.0) + + return options + + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + self.algorithm_name = 'smooth' + self.color = extra_params.get('color', None) + + def calculate_results(self): + mesh_array = self.dive_mesh.generateMesh() + math_support = self.dive_mesh.mathSupport + + mandelbrot_function = np.vectorize(math_support.mandelbrot) + (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + + smoothing_function = np.vectorize(math_support.smoothAfterCalculation) + pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) + + self.cache_frame.frame_info.raw_values = pixel_values_2d + self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed + + return + + def generate_image(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + # Using smooth only here, because that's what this is. + pixel_values_2d = self.cache_frame.frame_info.smooth_values.T + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + # NOTE: This is the difference - not using palette, but + # instead, a (kinda locally) calculated range + color = self.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + + return im + + def map_value_to_color(self, val): + + magnification = 1. / self.dive_mesh.imagMeshGenerator.baseWidth + if magnification <= 100: + magnification = 100 + + denom = float(self.dive_mesh.mathSupport.justTwoLogs(magnification)) + + if self.color: + c1 = 0. + c2 = 0. + c3 = 0. + + # (yellow blue 0,.6,1.0) + c1 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[0]); + c1 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[0]); + c2 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[1]); + c2 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[1]); + c3 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[2]); + c3 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[2]); + c1int = int(255.*((c1/4.) * 3.) / denom) + c2int = int(255.*((c2/4.) * 3.) / denom) + c3int = int(255.*((c3/4.) * 3.) / denom) + return (c1int,c2int,c3int) + else: + #magnification = 1. / self.context.cmplx_width + cint = int((val * 3.) / denom) + return (cint,cint,cint) + + From 59e65fff88e16575f2daa6425bb522f1d503a855 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Mon, 2 Aug 2021 08:53:41 -0700 Subject: [PATCH 22/44] mostly fixed how precision was set in flint, but added a long number locus to the demo too. --- clint_runscript.sh | 5 ++++- fractal.py | 29 ++++++++++++++++++++++------- fractalmath.py | 9 ++++++--- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/clint_runscript.sh b/clint_runscript.sh index 17d561d..566fcec 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,9 +1,12 @@ +python3 fractal.py --gif=low_res_demo.gif --algo=smooth --demo --burn --color --invalidate-cache --flint + + #python3 fractal.py --gif=native_demo.gif --color=exp2 --demo --burn #python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --burn --invalidate-cache #python3 fractal.py --gif=native_distance_demo.gif --algo=mandeldistance --demo --burn --smooth -python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --smooth --invalidate-cache +#python3 fractal.py --gif=native_distance_demo_fresh.gif --algo=mandeldistance --demo --burn --smooth --invalidate-cache #python3 fractal.py --gif=smooth_demo.gif --algo=smooth --demo --burn --color #python3 fractal.py --gif=smooth_demo_fresh.gif --algo=smooth --demo --burn --color --invalidate-cache diff --git a/fractal.py b/fractal.py index 32b1a04..79efeac 100644 --- a/fractal.py +++ b/fractal.py @@ -197,6 +197,9 @@ def setup(self): print(self.ctx) +# def runTimeline(self, timeline): +# movieClip = mpy.VideoClip(self.make_frame, + def run(self): if self.ctx.snapshot == True: @@ -1036,8 +1039,10 @@ def __init__(self, span, value, transitionIn='quadratic-to', transitionOut='quad # -- def set_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default mandelbrot dive params") - fractal_ctx.img_width = 1024 - fractal_ctx.img_height = 768 + #fractal_ctx.img_width = 1024 + #fractal_ctx.img_height = 768 + fractal_ctx.img_width = 320 + fractal_ctx.img_height = 240 cmplx_width_str = '5.0' cmplx_height_str = '3.5' @@ -1054,17 +1059,27 @@ def set_demo1_params(fractal_ctx, view_ctx): #center_imag_str = '0.00423684791873677221' #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) #center_str = '-1.769383179195515018213+0.00423684791873677221j' - center_str = '-0.05+0.6805j' - fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_str) + + + center_real_str = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884' + center_imag_str = '0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690' + + #center_str = '-1.76938317919551501821384728608547378290574726365475143746552821652788819126475645883616344638952966730448582578182030315748749123842171940312824619511374752125508480620857874547728033032251679986623911241845427430171292144236397931692967543941816568313013426227935414237685724357839108499720568695273052075081914417347810617942906997531749111337143517341661174565202727561591789320429089324651026717908784146646282137559906504607383722834707778703064588828982026040017443489083888449628870745058537+0.004236847918736772214926507171367997076682670917403757279459435650112344000805545157302430995023636506313532683359652571823004948055387363061275248149392923559308928343920507967248879049219866660455766269469006661034940149047143237255869797899085206566832026580640241153003788267897863946416220353410551029004563057237186845272103773258463079175126287746720056933262328069538227967558325171888734791243614309894854955011240963294216828273306935321715053674555266373827069885834569156846732024622119j' + + + #center_str = '-0.05+0.6805j' + #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_str) + fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) fractal_ctx.project_name = 'demo1' - fractal_ctx.scaling_factor = .70 + fractal_ctx.scaling_factor = .50 fractal_ctx.max_iter = 255 #fractal_ctx.max_iter = 500 - fractal_ctx.escape_rad = 2. + #fractal_ctx.escape_rad = 2. + fractal_ctx.escape_rad = 1024 #fractal_ctx.escape_rad = math.sqrt(1024.0) #fractal_ctx.escape_rad = 32768. @@ -1072,7 +1087,7 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.build_cache=True #view_ctx.duration = 4.0 - view_ctx.duration = 2.0 + view_ctx.duration = 60.0 #view_ctx.duration = 1.0 #view_ctx.duration = 0.25 diff --git a/fractalmath.py b/fractalmath.py index 5388d10..1e1655a 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -10,8 +10,11 @@ import numpy as np #FLINT_HIGH_PRECISION_SIZE = 16 # 53 is how many bits are in float64 -FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 -#FLINT_HIGH_PRECISION_SIZE = 200 +#FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 +#3.32 bits per digit, on average +#2200 was therefore, ~662 digits, got 54 frames down at .5 scaling + +FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol GMP_HIGH_PRECISION_SIZE=53 @@ -420,7 +423,7 @@ def __init__(self): super().__init__() self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. - self.flint.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) + self.flint.ctx.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) self.precisionType = 'flint' def createComplex(self, *args): From c40ddd5420a93770d6c461e28a7c9f6a2ca94e09 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Mon, 2 Aug 2021 08:54:33 -0700 Subject: [PATCH 23/44] only frame number burn in now --- smooth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smooth.py b/smooth.py index 0c07cee..7550861 100644 --- a/smooth.py +++ b/smooth.py @@ -79,11 +79,11 @@ def generate_image(self): if self.burn_in == True: meta = self.get_frame_metadata() if meta: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + burn_in_text = u"%d" % (meta['frame_number']) burn_in_location = (10,10) burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 8) burn_in_size = burn_in_font.getsize_multiline(burn_in_text) draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) From 681f80b6d3b71a435b432a772c6e5bbda7e4f600 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Fri, 6 Aug 2021 20:25:09 -0700 Subject: [PATCH 24/44] New out-to-tiff parts built, and complie_video.py added, to assemble a gif or video from an image sequence. Tinkered with how burn-in works in several places. Added first attempt at a test or two. --- algo.py | 22 +++++- batch_build.sh | 31 ++++++++ clint_runscript.sh | 17 ++++- compile_video.py | 88 ++++++++++++++++++++++ divemesh.py | 4 + env.txt | 13 +++- fractal.py | 173 +++++++++++++++++++++++++++++--------------- fractalcache.py | 32 ++++++++ fractalmath.py | 43 +++++++++-- julia.py | 8 +- mandelbrot.py | 13 +--- mandelbrot_solo.py | 99 +++++++++++++++++++++++++ mandeldistance.py | 10 +-- smooth.py | 47 ++++++------ test_fractalmath.py | 92 +++++++++++++++++++++++ 15 files changed, 571 insertions(+), 121 deletions(-) create mode 100755 batch_build.sh create mode 100644 compile_video.py create mode 100644 mandelbrot_solo.py create mode 100644 test_fractalmath.py diff --git a/algo.py b/algo.py index 5f96e3d..672371d 100644 --- a/algo.py +++ b/algo.py @@ -3,6 +3,8 @@ import fractalpalette as fp import divemesh as mesh +from PIL import Image, ImageDraw, ImageFont # EscapeAlgo uses for burn-in + # Should probably import Timeline, but not technically needed, since # it's cyclic? @@ -31,7 +33,7 @@ def parse_options(opts): """ return {} - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache): + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): self.algorithm_name = None self.dive_mesh = dive_mesh @@ -39,6 +41,7 @@ def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_pa self.project_folder_name = project_folder_name self.shared_cache_path = shared_cache_path + self.build_cache = build_cache self.invalidate_cache = invalidate_cache @@ -94,6 +97,9 @@ def pre_image_hook(self): def generate_image(self): pass + def write_image_to_file(self, image): + return self.cache_frame.write_image_to_file(image) + def ending_hook(self): pass @@ -133,7 +139,7 @@ def parse_options(opts): return options def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache) + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) # Load, with optional default values self.escape_radius = extra_params.get('escape_radius', 2.0) @@ -144,7 +150,7 @@ def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_pa def build_cache_frame(self): frame_info = EscapeFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations) - return fc.Frame(self.project_folder_name, self.shared_cache_path, self.algorithm_name, self.dive_mesh, frame_info, self.frame_number) + return fc.Frame(project_folder_name=self.project_folder_name, shared_cache_path=self.shared_cache_path, algorithm_name=self.algorithm_name, dive_mesh=self.dive_mesh, frame_info=frame_info, frame_number=self.frame_number) def get_frame_metadata(self): metadata = super().get_frame_metadata() @@ -152,6 +158,14 @@ def get_frame_metadata(self): metadata['max_escape_iterations'] = self.max_escape_iterations return metadata + def burn_text_to_drawing(self, burn_in_text, drawing): + burn_in_location = (10,10) + burn_in_margin = 5 + burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) + burn_in_size = burn_in_font.getsize_multiline(burn_in_text) + drawing.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") + drawing.text((burn_in_location[0]-2, burn_in_location[1] - 2), burn_in_text, 'white', burn_in_font) + class JuliaFrameInfo(EscapeFrameInfo): def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, julia_center, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): super().__init__(mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values, raw_histogram, smooth_values, smooth_histogram) @@ -189,7 +203,7 @@ def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_pa def build_cache_frame(self): frame_info = JuliaFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations, self.julia_center) - return fc.Frame(self.project_folder_name, self.shared_cache_path, self.algorithm_name, self.dive_mesh, frame_info, self.frame_number) + return fc.Frame(project_folder_name=self.project_folder_name, shared_cache_path=self.shared_cache_path, algorithm_name=self.algorithm_name, dive_mesh=self.dive_mesh, frame_info=frame_info, frame_number=self.frame_number) def get_frame_metadata(self): metadata = super().get_frame_metadata() diff --git a/batch_build.sh b/batch_build.sh new file mode 100755 index 0000000..c5f45fe --- /dev/null +++ b/batch_build.sh @@ -0,0 +1,31 @@ +# Example of embarassingly parallel start/stop range commands that all write +# out tiffs that can be assembled with 'compile_video.py' after finishing. + + +# Make sure the params in --demo will allow the frame numbers to be made. +# Best to test out the LAST of these frame range manually to make sure +# the subranges are all valid +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=0 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=25 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=50 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=75 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=100 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=125 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=150 --clip-frame-count=25& +python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=175 --clip-frame-count=25& + +# AFTER all the processes finish +#python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' + + + + +## Custom flint library version - attempt at optimization +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=0 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=25 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=50 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=75 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=100 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=125 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=150 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=175 --clip-frame-count=25& diff --git a/clint_runscript.sh b/clint_runscript.sh index 566fcec..c14af75 100755 --- a/clint_runscript.sh +++ b/clint_runscript.sh @@ -1,6 +1,21 @@ -python3 fractal.py --gif=low_res_demo.gif --algo=smooth --demo --burn --color --invalidate-cache --flint +#python3.9 fractal.py --gif=low_res_02-04.gif --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=2 --clip-frame-count=3 +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=0 --clip-frame-count=10& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=10 --clip-frame-count=10& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=20 --clip-frame-count=10& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=30 --clip-frame-count=10& +# +#python3.9 fractal.py --gif=low_res_demo.gif --algo=smooth --demo --burn --color --invalidate-cache +python3.9 fractal.py --gif=low_res_demo.gif --algo=smooth --demo --burn --color --invalidate-cache --flint + +#python3.9 fractal.py --gif=low_res_demo.gif --algo=mandelbrot --demo --burn --invalidate-cache --flintcustom --clip-start-frame=10 --clip-frame-count=10 +#python3.9 fractal.py --gif=low_res_demo.gif --algo=mandelbrot --demo --burn --invalidate-cache --flintcustom +#python3.9 fractal.py --gif=low_res_demo_e60.gif --algo=mandelbrot --demo --burn --invalidate-cache --flintcustom + +#python3.9 fractal.py --gif=low_res_demo.gif --algo=mandelbrot --demo --burn --color --invalidate-cache --flintcustom --clip-start-frame=25 --clip-frame-count=5 + +#python3 -m cProfile fractal.py --gif=low_res_demo.gif --flint --algo=smooth --demo --burn --color --invalidate-cache #python3 fractal.py --gif=native_demo.gif --color=exp2 --demo --burn #python3 fractal.py --gif=native_demo_fresh.gif --color=exp2 --demo --burn --invalidate-cache diff --git a/compile_video.py b/compile_video.py new file mode 100644 index 0000000..156bc65 --- /dev/null +++ b/compile_video.py @@ -0,0 +1,88 @@ + +import os, sys, getopt, math + +import moviepy.editor as mpy + +def parse_options(): + argv = sys.argv[1:] + + opts, args = getopt.getopt(argv, "d:o:", + ["dir=", + "out=", + "fps=", + "first-frame-number=", + "frame-count=", + ]) + + parsed_params = {} + for opt, arg in opts: + if opt in ['-d', '--dir']: + parsed_params['path_name'] = arg + elif opt in ['-o', '--out']: + parsed_params['output_file_name'] = arg + elif opt in ['--fps']: + parsed_params['framerate'] = float(arg) + elif opt in ['--first-frame-number']: + parsed_params['first_frame_number'] = int(arg) + elif opt in ['--frame-count']: + parsed_params['frame_count'] = int(arg) + + + if not parsed_params['path_name']: + raise ValueError('--dir is a required parameter') + if not parsed_params['output_file_name']: + raise ValueError('--out is a required parameter') + + if not parsed_params['output_file_name'].endswith(".gif") and not parsed_params['output_file_name'].endswith(".mp4"): + raise ValueError('Only .gif and .mp4 file names') + + return parsed_params + +def make_video(params): + file_name_list = get_image_sequence_in_directory(params) + if len(file_name_list) == 0: + raise ValueError('No images found for this sequence') + + framerate = params.get('framerate', 23.976 / 8.0) + #framerate = params.get('framerate', 23.976) + + clip = mpy.ImageSequenceClip(file_name_list, fps=framerate) + + output_file_name = params['output_file_name'] + + if output_file_name.endswith(".gif"): + #print("fps: %s" % str(self.fps)) + #self.clip.write_gif(self.vfilename, fps=self.fps) + print("fps: %s" % str(framerate)) + clip.write_gif(output_file_name, fps=framerate) + elif output_file_name.endswith(".mp4"): + clip.write_videofile(output_file_name, + fps=framerate, + audio=False, + codec="mpeg4") + + return + +def get_image_sequence_in_directory(params): + first_frame_number = params.get('first_frame_number', 0) + frame_count = params.get('frame_count', 1000000) + + directory_name = params['path_name'] + + # Go until either a gap is found, or max + file_names = [] + file_counter = 0 + for file_counter in range(frame_count): + curr_file_name = os.path.join(directory_name, "%d.tiff" % (file_counter + first_frame_number)) + #print("looking for %s" % curr_file_name) + if not os.path.exists(curr_file_name): + break + file_names.append(curr_file_name) + + return file_names +if __name__ == "__main__": + print("++ compile_video.py") + + params = parse_options() + make_video(params) + diff --git a/divemesh.py b/divemesh.py index c8f8f59..22e658a 100644 --- a/divemesh.py +++ b/divemesh.py @@ -126,9 +126,13 @@ def generateForDiveMesh(self, diveMesh): if self.varyingAxis == 'width': #calculate start/end... Probably need to be subtype aware for this... discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshWidth) + #print("W baseWidth: %s meshWidth: %s" % (str(self.baseWidth), str(diveMesh.meshWidth))) + #print("W: %s" % discretizedValues) mesh[0:] = discretizedValues # Assign the one-row discretization to every row of the mesh else: # self.varyingAxis == 'height' discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshHeight) + #print("H baseWidth: %s meshHeight: %s" % (str(self.baseWidth), str(diveMesh.meshHeight))) + #print("H: %s" % discretizedValues) # Assign the one-row discretization (as a column) to every column of the mesh mesh[0:] = discretizedValues[:,np.newaxis] diff --git a/env.txt b/env.txt index c650886..a36c8f1 100644 --- a/env.txt +++ b/env.txt @@ -47,19 +47,24 @@ brew install cython pip3 install numpy # MPIR (a fork of GMP) -brew install mpir@3.0.0 +#brew install mpir@3.0.0 +brew install mpir # MPFR (4.x kinda matters here) -brew install mpfr@4.1.0 +#brew install mpfr@4.1.0 +brew install mpfr # Arb takes a while to build. Might need flint, but things got fuzzy here on resolutions -brew install arb@2.20.0 +#brew install arb@2.20.0 +brew install arb # Flint library # Flint2 takes a while to build git clone https://github.com/fredrik-johansson/flint2 cd flint2 -./configure +#./configure +./configure --with-mpir=/opt/homebrew/Cellar/mpir/3.0.0 --with-gmp=/opt/homebrew/Cellar/gmp/6.2.1 --with-mpfr=/opt/homebrew/Cellar/mpfr/4.1.0 + make # No test? sudo make install diff --git a/fractal.py b/fractal.py index 79efeac..452e2ba 100644 --- a/fractal.py +++ b/fractal.py @@ -49,6 +49,7 @@ from algo import Algo # Abstract base class import, because we rely on it. from julia import Julia from mandelbrot import Mandelbrot +from mandelbrot_solo import MandelbrotSolo from mandeldistance import MandelDistance from smooth import Smooth @@ -76,7 +77,8 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.set_zoom_level = 0 # Zoom in prior to the dive self.clip_start_frame = -1 - self.clip_total_frames = 1 + self.clip_frame_count = 1 + self.write_video = True self.smoothing = False # bool turn on color smoothing self.snapshot = False # Generate a single, high res shotb @@ -97,6 +99,7 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.algorithm_map = {'julia' : Julia, 'mandelbrot' : Mandelbrot, + 'mandelbrot_solo' : MandelbrotSolo, 'mandeldistance' : MandelDistance, 'smooth': Smooth} self.algorithm_name = None @@ -119,8 +122,13 @@ def render_frame_number(self, frame_number): #print("extra params from mesh: %s" % str(dive_mesh.extraParams)) extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? + display_frame_number = frame_number + if self.clip_start_frame != -1: + display_frame_number += self.clip_start_frame + print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) - frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh, frame_number, self.project_name, self.shared_cache_path, self.build_cache, self.invalidate_cache, extra_params) + #print(".") # Just to keep the time-per-frame calculations from being overwritten in the terminal + frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh=dive_mesh, frame_number=display_frame_number, project_folder_name=self.project_name, shared_cache_path=self.shared_cache_path, build_cache=self.build_cache, invalidate_cache=self.invalidate_cache, extra_params=extra_params) frame_algorithm.beginning_hook() @@ -130,9 +138,11 @@ def render_frame_number(self, frame_number): frame_image = frame_algorithm.generate_image() + image_file_name = frame_algorithm.write_image_to_file(frame_image) + frame_algorithm.ending_hook() - return frame_image + return image_file_name def __repr__(self): return """\ @@ -209,27 +219,35 @@ def run(self): self.ctx.timeline = self.construct_simple_timeline() # Duration may be less than overall, if this is a sub-clip, so # figure out what our REAL duration is. + frame_file_names = [] timeline_frame_count = self.ctx.timeline.getTotalSpanFrameCount() + for curr_frame_number in range(timeline_frame_count): + frame_file_names.append(self.ctx.render_frame_number(curr_frame_number)) + timeline_duration = self.time_from_frame_number(timeline_frame_count) - self.clip = mpy.VideoClip(self.make_frame, duration=timeline_duration) + self.clip = mpy.ImageSequenceClip(frame_file_names, fps = self.ctx.timeline.framerate) - if self.banner: - self.clip = self.intro_banner() + if self.ctx.write_video == True: + if self.banner: + self.clip = self.intro_banner() + + # if not self.vfilename: + # self.clip.preview(fps=1) #fps 1 is really all that works + if self.vfilename.endswith(".gif"): + #print("fps: %s" % str(self.fps)) + #self.clip.write_gif(self.vfilename, fps=self.fps) + print("fps: %s" % str(self.ctx.timeline.framerate)) + self.clip.write_gif(self.vfilename, fps=self.ctx.timeline.framerate) + elif self.vfilename.endswith(".mp4"): + self.clip.write_videofile(self.vfilename, + fps=self.fps, + audio=False, + codec="mpeg4") + else: + print("Error: file extension not supported, must be gif or mp4") + sys.exit(0) - if not self.vfilename: - self.clip.preview(fps=1) #fps 1 is really all that works - elif self.vfilename.endswith(".gif"): - print("fps: %s" % str(self.fps)) - self.clip.write_gif(self.vfilename, fps=self.fps) - elif self.vfilename.endswith(".mp4"): - self.clip.write_videofile(self.vfilename, - fps=self.fps, - audio=False, - codec="mpeg4") - else: - print("Error: file extension not supported, must be gif or mp4") - sys.exit(0) def construct_simple_timeline(self): """ @@ -238,32 +256,37 @@ def construct_simple_timeline(self): Basically, let's construct a timeline from one set of start/end points, as defined by the current context. """ + overall_zoom_factor = self.ctx.scaling_factor overall_frame_count = self.duration * self.fps + # Integers only, but be generous and round up? if math.floor(overall_frame_count) != overall_frame_count: overall_frame_count = math.floor(overall_frame_count) + 1 clip_start_frame = 0 - rendered_frame_count = overall_frame_count - last_frame_number = overall_frame_count + last_frame_number = overall_frame_count - 1 + if self.ctx.clip_start_frame != -1: + # if start frame is defined, then rely on frame count + # also being valid + clip_start_frame = self.ctx.clip_start_frame; + last_frame_number = clip_start_frame + self.ctx.clip_frame_count - 1 - overall_zoom_factor = self.ctx.scaling_factor - - if self.ctx.clip_start_frame == -1: # Whole clip is the span + # So we know the overall trajectory of window zoom, and we know + # the frames we're actually trying to render. + rendered_frame_count = last_frame_number - clip_start_frame + 1 + + print("clip_start_frame: %d rendered_frame_count: %d" % (clip_start_frame, rendered_frame_count)) + + if last_frame_number > overall_frame_count: + raise ValueError("Can't construct timeline of %d frames, starting at frame %d, for a sequence of only %d frames (%f seconds at %f fps) (%d)" % (rendered_frame_count, clip_start_frame, overall_frame_count, self.duration, self.fps, last_frame_number)) + + if clip_start_frame == 0: start_width_real = self.ctx.cmplx_width start_width_imag = self.ctx.cmplx_height else: - if self.ctx.clip_start_frame + self.ctx.clip_total_frames > overall_frame_count: - raise ValueError("Can't construct timeline of %d frames for a sequence of only %d frames (%f seconds at %f fps)" % (self.ctx.clip_total_frames, overall_frame_count, self.duration, self.fps)) - - clip_start_frame = self.ctx.clip_start_frame - last_frame_number = clip_start_frame + self.ctx.clip_total_frames - 1 - rendered_frame_count = last_frame_number - clip_start_frame + 1 - - # Start frame is 1 or greater (hopefully), so subtracting 1 from the exponent here should be okay. + # Start frame is 1 or greater, the exponent here should be okay. start_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, clip_start_frame) start_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, clip_start_frame) - # Sub-section the frames, if needed end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) @@ -272,7 +295,6 @@ def construct_simple_timeline(self): timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm_name=self.ctx.algorithm_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) if timeline.algorithm_name == 'julia': - #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='julia', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) # Just evenly divide the waypoints across the time for a simple timeline keyframeCount = len(self.ctx.julia_list) # 2 keyframes over 10 frames = 10 frames per keyframe @@ -295,15 +317,12 @@ def construct_simple_timeline(self): if currKeyframeFrameNumber + keyframeSpacing > rendered_frame_count - 1: currKeyframeNumber = rendered_frame_count - 1 - #span.addNewCenterKeyframe(currKeyframeFrameNumber, currJuliaCenter, transitionIn='linear', transitionOut='linear') span.addNewComplexParameterKeyframe(currKeyframeFrameNumber, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') currKeyframeFrameNumber += keyframeSpacing timeline.timelineSpans.append(span) else: - #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, fractal='mandelbrot', framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) - #timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm=self.ctx.algorithm, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) #print("Trying to make span of %d frames" % frame_count) span = timeline.addNewSpanAtEnd(rendered_frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag) @@ -1041,8 +1060,14 @@ def set_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default mandelbrot dive params") #fractal_ctx.img_width = 1024 #fractal_ctx.img_height = 768 - fractal_ctx.img_width = 320 - fractal_ctx.img_height = 240 + #fractal_ctx.img_width = 320 + #fractal_ctx.img_height = 240 + fractal_ctx.img_width = 160 + fractal_ctx.img_height = 120 + #fractal_ctx.img_width = 80 + #fractal_ctx.img_height = 60 + #fractal_ctx.img_width = 16 + #fractal_ctx.img_height = 12 cmplx_width_str = '5.0' cmplx_height_str = '3.5' @@ -1050,9 +1075,15 @@ def set_demo1_params(fractal_ctx, view_ctx): #cmplx_height_str = '.00075' #cmplx_width_str = '2.0' #cmplx_height_str = str(1024*2/768) + + # For debugging, this window (and a couple zooms after) is where things + # tend to go wrong when precision is getting clipped off + #cmplx_width_str = '6.736977389253725e-08' + #cmplx_height_str = '4.7158841724776074e-08' fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) + # This is close t Misiurewicz point M32,2 # fractal_ctx.cmplx_center = fractal_ctx.ctxc(-.77568377, .13646737) #center_real_str = '-1.769383179195515018213' @@ -1064,7 +1095,9 @@ def set_demo1_params(fractal_ctx, view_ctx): center_real_str = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884' center_imag_str = '0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690' - #center_str = '-1.76938317919551501821384728608547378290574726365475143746552821652788819126475645883616344638952966730448582578182030315748749123842171940312824619511374752125508480620857874547728033032251679986623911241845427430171292144236397931692967543941816568313013426227935414237685724357839108499720568695273052075081914417347810617942906997531749111337143517341661174565202727561591789320429089324651026717908784146646282137559906504607383722834707778703064588828982026040017443489083888449628870745058537+0.004236847918736772214926507171367997076682670917403757279459435650112344000805545157302430995023636506313532683359652571823004948055387363061275248149392923559308928343920507967248879049219866660455766269469006661034940149047143237255869797899085206566832026580640241153003788267897863946416220353410551029004563057237186845272103773258463079175126287746720056933262328069538227967558325171888734791243614309894854955011240963294216828273306935321715053674555266373827069885834569156846732024622119j' +# Shorter, for debugging +# center_real_str = '-1.76938317919551501821384728608547378290574726365475143746552821652788819126475645883616344638952966730448582578182030315748749123842171940312824619511374752125508480620857874547728033032251679986623911241845427430171292144236397931692967543941816568313013426227935414237685724357839108499720568695273052075081914417347810617942906997531749111337143517341661174565202727561591789320429089324651026717908784146646282137559906504607383722834707778703064588828982026040017443489083888449628870745058537' +# center_imag_str = '0.004236847918736772214926507171367997076682670917403757279459435650112344000805545157302430995023636506313532683359652571823004948055387363061275248149392923559308928343920507967248879049219866660455766269469006661034940149047143237255869797899085206566832026580640241153003788267897863946416220353410551029004563057237186845272103773258463079175126287746720056933262328069538227967558325171888734791243614309894854955011240963294216828273306935321715053674555266373827069885834569156846732024622119' #center_str = '-0.05+0.6805j' @@ -1072,31 +1105,47 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) fractal_ctx.project_name = 'demo1' - - fractal_ctx.scaling_factor = .50 - - fractal_ctx.max_iter = 255 - #fractal_ctx.max_iter = 500 - - #fractal_ctx.escape_rad = 2. - fractal_ctx.escape_rad = 1024 + #fractal_ctx.scaling_factor = .8 + fractal_ctx.scaling_factor = .5 + + fractal_ctx.write_video = False + + #fractal_ctx.max_iter = 128 + #fractal_ctx.max_iter = 255 + #fractal_ctx.max_iter = 512 # covers ~e-34 or so + #fractal_ctx.max_iter = 1024 # covers ~e-48 or so + #fractal_ctx.max_iter = 2048 + fractal_ctx.max_iter = 4096 + + fractal_ctx.escape_rad = 2 + #fractal_ctx.escape_rad = 4 + #fractal_ctx.escape_rad = 8 + #fractal_ctx.escape_rad = 512 #fractal_ctx.escape_rad = math.sqrt(1024.0) - #fractal_ctx.escape_rad = 32768. + fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['escape_radius'] = fractal_ctx.escape_rad + fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['max_escape_iterations'] = fractal_ctx.max_iter - fractal_ctx.verbose = 3 - fractal_ctx.build_cache=True + # Basically make this command-line, not demo? + #fractal_ctx.clip_start_frame = 7 + #fractal_ctx.clip_frame_count = 3 - #view_ctx.duration = 4.0 - view_ctx.duration = 60.0 - #view_ctx.duration = 1.0 - #view_ctx.duration = 0.25 + fractal_ctx.verbose = 3 + fractal_ctx.build_cache=False # FPS still isn't set quite right, but we'll get it there eventually. - #view_ctx.fps = 23.976 / 4.0 view_ctx.fps = 23.976 / 8.0 + #view_ctx.fps = 23.976 / 4.0 + #view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 23.976 #view_ctx.fps = 29.97 / 2.0 + #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) + #view_ctx.duration = 4.0 + view_ctx.duration = 60.0 + #view_ctx.duration = 30.0 + #view_ctx.duration = 2.0 + #view_ctx.duration = 0.25 + def set_julia_walk_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default julia walk params") fractal_ctx.img_width = 1024 @@ -1184,14 +1233,16 @@ def parse_options(fractal_ctx, view_ctx): ["preview", "algo=", "flint", + "flintcustom", "gmp", "demo", "demo-julia-walk", "duration=", "fps=", "clip-start-frame=", - "clip-total-frames=", + "clip-frame-count=", "project-name=", + "image-frame-path=", "shared-cache-path=", "build-cache", "invalidate-cache", @@ -1227,6 +1278,8 @@ def parse_options(fractal_ctx, view_ctx): fractal_ctx.math_support = fm.DiveMathSupportGmp() elif opt in ['--flint']: fractal_ctx.math_support = fm.DiveMathSupportFlint() + elif opt in ['--flintcustom']: + fractal_ctx.math_support = fm.DiveMathSupportFlintCustom() elif opt in ['-a', '--algo']: if str(arg) in fractal_ctx.algorithm_map: fractal_ctx.algorithm_name = str(arg) @@ -1259,9 +1312,11 @@ def parse_options(fractal_ctx, view_ctx): elif opt in ['-f', '--fps']: view_ctx.fps = float(arg) elif opt in ['--clip-start-frame']: + # For construct_simple_timeline, use of --clip-start-frame + # makes --clip-frame-count kinda required fractal_ctx.clip_start_frame = int(arg) - elif opt in ['--clip-total-frames']: - fractal_ctx.clip_total_frames = int(arg) + elif opt in ['--clip-frame-count']: + fractal_ctx.clip_frame_count = int(arg) elif opt in ['-m', '--max-iter']: fractal_ctx.max_iter = int(arg) elif opt in ['-w', '--img-w']: diff --git a/fractalcache.py b/fractalcache.py index e725580..8085370 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -71,6 +71,7 @@ def __init__(self, project_folder_name, shared_cache_path, algorithm_name, dive_ self.project_folder_name = project_folder_name self.shared_cache_path = shared_cache_path + self.algorithm_name = algorithm_name self.dive_mesh = dive_mesh self.frame_info = frame_info @@ -152,3 +153,34 @@ def remove_from_results_cache(self): self.smooth_values = None self.smooth_histogram = None + def create_image_subpath(self, mkdir_if_needed=False): + root_cache_path = u"%s_cache" % self.project_folder_name + results_subpath = os.path.join(root_cache_path, "image_frames", self.dive_mesh.mathSupport.precisionType, self.algorithm_name) + if mkdir_if_needed and not os.path.exists(results_subpath): + os.makedirs(results_subpath) + return results_subpath + + def create_image_file_identifier(self): + return u"%d.tiff" % self.frame_number + + def create_image_file_name(self): + return os.path.join(self.create_image_subpath(), self.create_image_file_identifier()) + + def create_image_metadata_file_name(self): + return u"%s.pik" % self.create_image_file_name() + + def write_image_to_file(self, image): + self.create_image_subpath(mkdir_if_needed=True) # Just for side-effect folder creation + + filename = self.create_image_file_name() + metadata_filename = self.create_image_metadata_file_name() + + image.save(filename) + with open(metadata_filename, 'wb') as fd: + frame_meta = self.frame_info.empty_copy() + frame_meta.raw_histogram = self.frame_info.raw_histogram + pickle.dump(frame_meta,fd) + + return filename + + diff --git a/fractalmath.py b/fractalmath.py index 1e1655a..58aa375 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -14,7 +14,16 @@ #3.32 bits per digit, on average #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling -FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol +FLINT_HIGH_PRECISION_SIZE = int(120 * 3.32) + +# For debugging, looks like we're bottoming out somewhere around e-11 +# So, only really need ~20 digits for this test +# Blowing out frame 30, which is ~28? +#FLINT_HIGH_PRECISION_SIZE = int(50 * 3.32) +# Native blew out somewhere e-13 to e-15 +#FLINT_HIGH_PRECISION_SIZE = int(200 * 3.32) + GMP_HIGH_PRECISION_SIZE=53 @@ -43,7 +52,12 @@ def createComplex(self, *args): Native complex type just passes on all params to complex() """ - return complex(*args) + # Can't make a native complex from 2 strings though, so when we + # detect that, jam them into floats + if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str): + return complex(float(args[0]), float(args[1])) + else: + return complex(*args) def createFloat(self, floatValue): return float(floatValue) @@ -141,9 +155,9 @@ def interpolateLogTo(self, startX, startY, endX, endY, targetX): def interpolateRootTo(self, startX, startY, endX, endY, targetX): """ - Iterative multiplications of window sizes for zooming means we want to be able to - interpolate between two points using the root if the frame count between them as - the scale factor + Iterative multiplications of window sizes for zooming means + we want to be able to interpolate between two points using + the frame count as the root. """ if targetX == endX: return endY @@ -566,6 +580,7 @@ def justTwoLogs(self, value): return self.flint.arb(value).log().log() def twoLogsHelper(self, value, radius): + #print("trying two logs...") # ln(ln(abs(value))/ln(radius)) # Note: flint needs 'value.log()' for ln, but 'value.const_log2() for log2?! @@ -573,7 +588,6 @@ def twoLogsHelper(self, value, radius): return (value.abs_lower().log() / math.log(radius)).log() #return math.log(math.log(abs(value))/math.log(radius)) - def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): didEscape = False z = self.flint.acb(0,0) @@ -644,6 +658,23 @@ def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): def clamp(self, num, min_value, max_value): return max(min(num, max_value), min_value) +class DiveMathSupportFlintCustom(DiveMathSupportFlint): + def mandelbrot(self, c, escapeRadius, maxIter): + #radiusArb = self.flint.arb(escapeRadius) + #return c.libmandelbrot(radiusArb, maxIter) + return c.libmandelbrot(escapeRadius, maxIter) + #return c.libmandelbrot_full(escapeRadius, maxIter) + #print("Calling library, radius: %s, iter: %s" % (str(escapeRadius), str(maxIter))) + #(tmpRes, tmpZ) = c.libmandelbrot_full(escapeRadius, maxIter) + #print("%d" % (tmpRes)) + #print("%d - %s" % (tmpRes, str(tmpZ))) + #print("%d - %s" % (tmpRes, str(abs(tmpZ)))) + #return (tmpRes, tmpZ) + + def libMandelbrotFull(self, c, escapeRadius, maxIter): + """ Reference implementation, just for error check? """ + return c.libmandelbrot_full(escapeRadius, maxIter) + class DiveMathSupportGmp(DiveMathSupport): """ Overrides to instantiate gmpy2-specific complex types diff --git a/julia.py b/julia.py index 620822d..c32ed0a 100644 --- a/julia.py +++ b/julia.py @@ -81,13 +81,7 @@ def generate_image(self): meta = self.get_frame_metadata() if meta: burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - - burn_in_location = (10,10) - burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) - burn_in_size = burn_in_font.getsize_multiline(burn_in_text) - draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") - draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + self.burn_text_to_drawing(burn_in_text, draw) return im diff --git a/mandelbrot.py b/mandelbrot.py index 441e2d1..46a575e 100644 --- a/mandelbrot.py +++ b/mandelbrot.py @@ -133,8 +133,6 @@ def calculate_results(self): #mandelpool.join() def pre_image_hook(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. if self.use_smoothing == True: self.palette.histogram = self.cache_frame.frame_info.smooth_histogram else: @@ -169,14 +167,9 @@ def generate_image(self): if self.burn_in == True: meta = self.get_frame_metadata() if meta: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - - burn_in_location = (10,10) - burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) - burn_in_size = burn_in_font.getsize_multiline(burn_in_text) - draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") - draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + burn_in_text = u"%d" % (meta['frame_number']) + #burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + self.burn_text_to_drawing(burn_in_text, draw) return im diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py new file mode 100644 index 0000000..75e460f --- /dev/null +++ b/mandelbrot_solo.py @@ -0,0 +1,99 @@ +# -- +# File: mandelbrot.py +# +# Basic escape iteration method for calculating the mandlebrot set +# +# Code cribbed from all over the place ... notably : +# +# https://www.codingame.com/playgrounds/2358/how-to-plot-the-mandelbrot-set/mandelbrot-set +# http://linas.org/art-gallery/escape/escape.html +# +# Misiurewicz points also cribbed from all over +# +# https://mrob.com/pub/muency/misiurewiczpoint.html +# https://www.youtube.com/watch?v=u1pwtSBTnPU&t=274s +# +# +# MPs: +# +# 0.4244 + 0.200759i; + +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw, ImageFont + +from algo import EscapeFrameInfo, EscapeAlgo +import fractalpalette as fp + + +class MandelbrotSolo(EscapeAlgo): + def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + + self.algorithm_name = 'mandelbrot_solo' + + def generate_results(self): + """ + Special no-cache implementation of the main results sequence + """ + self.cache_frame = self.build_cache_frame() + self.calculate_results() + + return + + def calculate_results(self): + mesh_array = self.dive_mesh.generateMesh() + math_support = self.dive_mesh.mathSupport + + mandelbrot_function = np.vectorize(math_support.mandelbrot) + (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + + hist = defaultdict(int) + + for x in range(0, mesh_array.shape[0]): + for y in range(0, mesh_array.shape[1]): + # Not using mathSupport's floor() here, because it should just be a normal-scale float + if pixel_values_2d[x,y] < self.max_escape_iterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) + hist[math.floor(pixel_values_2d[x,y])] += 1 + + self.cache_frame.frame_info.raw_values = pixel_values_2d + self.cache_frame.frame_info.raw_histogram = hist + + return + + def pre_image_hook(self): + self.palette.histogram = self.cache_frame.frame_info.raw_histogram + self.palette.calc_hues(self.max_escape_iterations) + + def generate_image(self): + pixel_values_2d = self.cache_frame.frame_info.raw_values.T + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + color = self.palette.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d" % (meta['frame_number']) + self.burn_text_to_drawing(burn_in_text, draw) + + return im + + def ending_hook(self): + self.palette.per_frame_reset() + + diff --git a/mandeldistance.py b/mandeldistance.py index 7bc780d..a468b52 100644 --- a/mandeldistance.py +++ b/mandeldistance.py @@ -75,14 +75,8 @@ def generate_image(self): if self.burn_in == True: meta = self.get_frame_metadata() if meta: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - - burn_in_location = (10,10) - burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 12) - burn_in_size = burn_in_font.getsize_multiline(burn_in_text) - draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") - draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + burn_in_text = u"%d" % (meta['frame_number']) + self.burn_text_to_drawing(burn_in_text, draw) return im diff --git a/smooth.py b/smooth.py index 7550861..7dc61bf 100644 --- a/smooth.py +++ b/smooth.py @@ -47,6 +47,10 @@ def calculate_results(self): mandelbrot_function = np.vectorize(math_support.mandelbrot) (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + #import sys + #np.set_printoptions(threshold=sys.maxsize) + #print(pixel_values_2d) + smoothing_function = np.vectorize(math_support.smoothAfterCalculation) pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) @@ -80,13 +84,7 @@ def generate_image(self): meta = self.get_frame_metadata() if meta: burn_in_text = u"%d" % (meta['frame_number']) - - burn_in_location = (10,10) - burn_in_margin = 5 - burn_in_font = ImageFont.truetype('fonts/cour.ttf', 8) - burn_in_size = burn_in_font.getsize_multiline(burn_in_text) - draw.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") - draw.text(burn_in_location, burn_in_text, 'white', burn_in_font) + self.burn_text_to_drawing(burn_in_text, draw) return im @@ -97,22 +95,27 @@ def map_value_to_color(self, val): magnification = 100 denom = float(self.dive_mesh.mathSupport.justTwoLogs(magnification)) - - if self.color: - c1 = 0. - c2 = 0. - c3 = 0. - + if denom <= 0.0 or math.isnan(denom): + return (0,0,0) + elif self.color: # (yellow blue 0,.6,1.0) - c1 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[0]); - c1 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[0]); - c2 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[1]); - c2 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[1]); - c3 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[2]); - c3 += 0.5 + 0.5*math.cos( 3.0 + val*0.15 + self.color[2]); - c1int = int(255.*((c1/4.) * 3.) / denom) - c2int = int(255.*((c2/4.) * 3.) / denom) - c3int = int(255.*((c3/4.) * 3.) / denom) + c1 = 1 + math.cos(3.0 + val*0.15 + self.color[0]) + c2 = 1 + math.cos(3.0 + val*0.15 + self.color[1]) + c3 = 1 + math.cos(3.0 + val*0.15 + self.color[2]) + + if c1 <= 0 or math.isnan(c1): + c1int = 0 + else: + c1int = int(255.*((c1/4.) * 3.) / denom) + if c2 <= 0 or math.isnan(c2): + c2int = 0 + else: + c2int = int(255.*((c2/4.) * 3.) / denom) + if c3 <= 0 or math.isnan(c3): + c3int = 0 + else: + c3int = int(255.*((c3/4.) * 3.) / denom) + return (c1int,c2int,c3int) else: #magnification = 1. / self.context.cmplx_width diff --git a/test_fractalmath.py b/test_fractalmath.py new file mode 100644 index 0000000..59ba2f7 --- /dev/null +++ b/test_fractalmath.py @@ -0,0 +1,92 @@ +import unittest + +import fractalmath as fm + + +class TestMathSupport(unittest.TestCase): + mathSupport = None + + @classmethod + def setUpClass(cls): + cls.mathSupport = fm.DiveMathSupport() + + @classmethod + def tearDownClass(cls): + cls.mathSupport = None + + def test_scaleValue(self): + """ Extra casts of returns to float() so assertAlmostEqual works """ + startValue = 5.0 + zoomFactor = 0.8 + + # Zero iteration zoom, should be same as input + self.assertEqual(startValue, float(self.mathSupport.scaleValueByFactorForIterations(startValue, zoomFactor, 0))) + + # 1 iteration zoom, should be same as single multiplication + self.assertAlmostEqual(startValue * zoomFactor, float(self.mathSupport.scaleValueByFactorForIterations(startValue, zoomFactor, 1))) + + # 2 iterations, should be doubly-multiplied zoom + self.assertAlmostEqual(startValue * zoomFactor * zoomFactor, float(self.mathSupport.scaleValueByFactorForIterations(startValue, zoomFactor, 2))) + + + def test_interpolateLinear(self): + startX = 1.0 + startY = 2.0 + endX = 2.0 + endY = 4.0 + # Endpoints check + self.assertAlmostEqual(startY, self.mathSupport.interpolate('linear', startX, startY, endX, endY, startX)) + self.assertAlmostEqual(endY, self.mathSupport.interpolate('linear', startX, startY, endX, endY, endX)) + + # Not endpoint check + self.assertAlmostEqual(2.5, self.mathSupport.interpolate('linear', startX, startY, endX, endY, 1.25)) + + def test_mandelbrot(self): + # Native python isn't accurate past ~120 iterations. + # So, we either reduce iterations, and require fewer decimal places to match. + maxIterations = 100 + placesToMatch = 9 + + radius = 2.0 + + # The answer for this particular point, is supposed to be 133, but we're + # capping iterations to 128 here. + center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) + + # Slightly adjusted center, just to get a different answer + center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(3, iterations) + self.assertAlmostEqual(-2.182516841632256, float(lastZee.real), placesToMatch) + self.assertAlmostEqual(2.3301940018826137, float(lastZee.imag), placesToMatch) + +class TestMathSupportFlint(TestMathSupport): + @classmethod + def setUpClass(cls): + cls.mathSupport = fm.DiveMathSupportFlint() + # Set prec to what gets the closest value for comparing to + # native python. It's a little arbitrary, even knowing the + # prec value *should* be 53 to match precision. + cls.mathSupport.flint.ctx.prec = 53 + +class TestMathSupportFlintCustom(TestMathSupport): + @classmethod + def setUpClass(cls): + cls.mathSupport = fm.DiveMathSupportFlintCustom() + # Set prec to what gets the closest value for comparing to + # native python. It's a little arbitrary, even knowing the + # prec value *should* be 53 to match precision. + cls.mathSupport.flint.ctx.prec = 53 + + +if __name__ == '__main__': + unittest.main() + From cbc2562057618179e7d36e6abf4aaf10e77b5002 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sat, 7 Aug 2021 18:23:37 -0700 Subject: [PATCH 25/44] Mostly turning off debug printouts. Added a couple optimization discussion paragraphs to README --- README.md | 24 +++++++++++++ batch_build.sh | 67 ++++++++++++++++++++++++++++++------ fractal.py | 83 ++++++++++++++++++++++++++++++++++++++++++--- fractalmath.py | 3 +- test_fractalmath.py | 2 +- 5 files changed, 162 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6d1c02a..16c6b23 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,27 @@ MathSupport - Native Python implementation in the base class. +# Optimization Discussion + +## Inner loop multiplications optimizations at high bit depths + +Text taken from: +https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ + +When using floating-point math the speed of addition, subtraction, and multiplication are generally identical. However floating-point math has only about 52 bits of precision which is woefully inadequate for serious fractal exploration. All of my fractal programs have featured high-precision math routines to allow virtually unlimited zooming. Fractal eXtreme supports up to 7,680 bits of precision. While this isn’t truly unlimited it is close enough because fractal calculation time generally increases as the cube of the zoom level, and even on the fastest PCs available it exceeds most people’s patience between 500 and 2,000 bits of precision. + +The speed of squaring and multiplication is critical because in high-precision math they are O(n^2) operations and everything else is O(n). As ‘n’ (the number of 32-bit or 64-bit words) increases, the time to do the multiplications becomes the only thing that matters. Okay, at extremely high precisions multiplication doesn’t have to be O(n^2), but the alternatives are a lot of work for uncertain gain, so I’m ignoring them. + +While multiplication and squaring are both O(n^2) it turns out that squaring can be done roughly twice as fast as multiplication. Half of the partial results when squaring are needed twice, but only need to be calculated once. + +All the information necessary to find the algebraic optimization is now available. + +The observation (which came from somebody I know only through e-mail) was that the Mandelbrot calculations can be done using three squaring operations rather than two squares and a multiply. At the limit this gives approximately a one-third speedup! + +The one multiply was being used to calculate zr * zi. If we instead calculate (zr + zi)^2 then we get zr^2 + 2*zr*zi + zi^2. Since we’ve already calculated zr^2 and zi^2 we can just subtract them off and, voila, multiplication through squaring: + + +==== + + + diff --git a/batch_build.sh b/batch_build.sh index c5f45fe..dced430 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -5,22 +5,69 @@ # Make sure the params in --demo will allow the frame numbers to be made. # Best to test out the LAST of these frame range manually to make sure # the subranges are all valid -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=0 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=25 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=50 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=75 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=100 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=125 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=150 --clip-frame-count=25& -python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=175 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=0 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=25 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=50 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=75 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=100 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=125 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=150 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-start-frame=175 --clip-frame-count=25& + + +# Looks like it takes 13 or 14 processes to hit 100% cpu? +# Deeper depths, was still a bit over, so dropping to 12 for now? +# Seem to see it hit 122% cpu per process sometimes... + +#processCount=12 +# 12 is ending up in 13 processes... hrm. +# Honestly, I can't imagine cache is doing anything but thrashing at higher bit depths... +# So let's run 6 processes next attempt? (Seems like it sped up at the end, so probably right). +#processCount=12 + +# Tried 12 again, think it's very slow. +processCount=8 +startFrame=375 +lastNumber=425 + +stride=$(((lastNumber-startFrame)/processCount)) + +#stride=45 +#lastNumber=359 +batchCount=0 +pidList=() + +until [ $startFrame -ge $lastNumber ] +do + echo startFrame: $startFrame & + python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& + pidList[${batchCount}]=$! + ((startFrame=startFrame+stride)) + ((batchCount=batchCount+1)) +done + +# wait for all pids +for currPID in ${pidList[*]}; do + wait $currPID + echo "Done" +done # AFTER all the processes finish -#python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' +python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' + -## Custom flint library version - attempt at optimization +# Custom flint library version - attempt at optimization +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=0 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=25 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=50 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=75 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=100 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=125 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=150 --clip-frame-count=25& +#python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=175 --clip-frame-count=25& #python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=0 --clip-frame-count=25& #python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=25 --clip-frame-count=25& #python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=50 --clip-frame-count=25& diff --git a/fractal.py b/fractal.py index 452e2ba..a4703af 100644 --- a/fractal.py +++ b/fractal.py @@ -126,7 +126,7 @@ def render_frame_number(self, frame_number): if self.clip_start_frame != -1: display_frame_number += self.clip_start_frame - print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) + #print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) #print(".") # Just to keep the time-per-frame calculations from being overwritten in the terminal frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh=dive_mesh, frame_number=display_frame_number, project_folder_name=self.project_name, shared_cache_path=self.shared_cache_path, build_cache=self.build_cache, invalidate_cache=self.invalidate_cache, extra_params=extra_params) @@ -204,7 +204,7 @@ def create_snapshot(self): def setup(self): print(self) - print(self.ctx) + #print(self.ctx) # def runTimeline(self, timeline): @@ -290,7 +290,7 @@ def construct_simple_timeline(self): end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) - print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) + #print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm_name=self.ctx.algorithm_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) @@ -1110,12 +1110,85 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.write_video = False + + # Looks like 23.976/8 fps (almost 3fps), duration=60 (which could be 180 frames) + # max-iter=4096 + # Bit depth was only ~400 + # Ended up reaching e-51 + # Most iteration answers were around 1200-1300? + + + # Trying to double that... Basically, can just double duration? + # And then, can start off at frame 175? + # + # Looks like 23.976/8 fps (almost 3fps), duration=120 (which could be 360 frames) + # Which is 45 frames per process, for 8 processes. + # max-iter=4096 + # Let's stack bit depth to accommodate max_iter? + # Lower bound I'm going with is: .4438 + # So bits = 1817? Let's make it a round 1800? + # Ended up reaching... + # Most iteration answers were around ? + + # Think I forgot to set bits deep enough? + # Blew out at e-57? + # Looks like iters were around 1500 + # Precision was (accidentally?) 400 + + # 550 * 3.32 = 1826 + # max-iter=4096 + # Looks like 23.976/8 fps (almost 3fps), + # duration=90 (which could be 270 frames) + # lost all detail at ~227? + # iters were hitting 4000 + # Looks like it hit e-68 + + # Ok. + # 8192 iter + # ~3621 bits => 1125 digits + # duration=180 (~540 frames defined, just giving myself runway) + # Gonna run frames 200+? + + # Frame 240 is hitting e-72 widths, 4400 iters + # Looks like window definition has ~953 digits of unused precision? + # Lol. + + # Hit ~e-91 at frame 303 + # iters sitting around 5300 + + + # Shaving off 400 digits of precision... (to 725 digits = 2407 bits) + # Also trying only 6 simultaneous, frames 300-350 + + # frame 350 hit ~e-107 + # Iters around 6300 + + # Shaving more precision off - 625 digits + + # Blew out around 385? + # Hitting 7900 iters + # Hitting e-114, so is using something like 300 positions total? + + # K, for frames 375+, gonna go with... + # doubling iters again, + # Adding another 100 positions? (went to 825 positions) + + # Finally timed that run + # 50 frames (375-425) + # 2048*8 max iter + # 825 digits (2739 bits) + # + + + + + #fractal_ctx.max_iter = 128 #fractal_ctx.max_iter = 255 #fractal_ctx.max_iter = 512 # covers ~e-34 or so #fractal_ctx.max_iter = 1024 # covers ~e-48 or so #fractal_ctx.max_iter = 2048 - fractal_ctx.max_iter = 4096 + fractal_ctx.max_iter = 2048 * 8 fractal_ctx.escape_rad = 2 #fractal_ctx.escape_rad = 4 @@ -1141,7 +1214,7 @@ def set_demo1_params(fractal_ctx, view_ctx): #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) #view_ctx.duration = 4.0 - view_ctx.duration = 60.0 + view_ctx.duration = 180.0 #view_ctx.duration = 30.0 #view_ctx.duration = 2.0 #view_ctx.duration = 0.25 diff --git a/fractalmath.py b/fractalmath.py index 58aa375..1aaff61 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -15,7 +15,8 @@ #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol -FLINT_HIGH_PRECISION_SIZE = int(120 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) +FLINT_HIGH_PRECISION_SIZE = int(825 * 3.32) # For debugging, looks like we're bottoming out somewhere around e-11 # So, only really need ~20 digits for this test diff --git a/test_fractalmath.py b/test_fractalmath.py index 59ba2f7..2cd63aa 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -50,7 +50,7 @@ def test_mandelbrot(self): radius = 2.0 # The answer for this particular point, is supposed to be 133, but we're - # capping iterations to 128 here. + # capping iterations to 100 here. center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) #print(str(iterations)) From 919e83256d6fea549236d04fa0b7fb1876a22099 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sun, 8 Aug 2021 17:46:53 -0700 Subject: [PATCH 26/44] tuning up custom arb mandelbrot distribution, and lots of in-process command tinkering --- batch_build.sh | 150 ++++++++++++++++++++++++++++++++++++++++++++++--- fractal.py | 50 +++++++++++++++-- fractalmath.py | 26 ++++----- 3 files changed, 196 insertions(+), 30 deletions(-) diff --git a/batch_build.sh b/batch_build.sh index dced430..18f9ba1 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -24,16 +24,18 @@ # Honestly, I can't imagine cache is doing anything but thrashing at higher bit depths... # So let's run 6 processes next attempt? (Seems like it sped up at the end, so probably right). #processCount=12 - # Tried 12 again, think it's very slow. -processCount=8 -startFrame=375 -lastNumber=425 + +processCount=9 + +echo "Batch 2" +date + +startFrame=500 +lastNumber=525 stride=$(((lastNumber-startFrame)/processCount)) -#stride=45 -#lastNumber=359 batchCount=0 pidList=() @@ -52,9 +54,139 @@ for currPID in ${pidList[*]}; do echo "Done" done -# AFTER all the processes finish -python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' - +#startFrame=525 +#lastNumber=550 +# +#stride=$(((lastNumber-startFrame)/processCount)) +# +#batchCount=0 +#pidList=() +# +#until [ $startFrame -ge $lastNumber ] +#do +# echo startFrame: $startFrame & +# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& +# pidList[${batchCount}]=$! +# ((startFrame=startFrame+stride)) +# ((batchCount=batchCount+1)) +#done +# +## wait for all pids +#for currPID in ${pidList[*]}; do +# wait $currPID +# echo "Done" +#done +#startFrame=550 +#lastNumber=600 +# +#stride=$(((lastNumber-startFrame)/processCount)) +# +#batchCount=0 +#pidList=() +# +#until [ $startFrame -ge $lastNumber ] +#do +# echo startFrame: $startFrame & +# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& +# pidList[${batchCount}]=$! +# ((startFrame=startFrame+stride)) +# ((batchCount=batchCount+1)) +#done +# +## wait for all pids +#for currPID in ${pidList[*]}; do +# wait $currPID +# echo "Done" +#done +# +# +#echo "Batch 3" +#date +# +# +#startFrame=600 +#lastNumber=700 +# +#stride=$(((lastNumber-startFrame)/processCount)) +# +#batchCount=0 +#pidList=() +# +#until [ $startFrame -ge $lastNumber ] +#do +# echo startFrame: $startFrame & +# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& +# pidList[${batchCount}]=$! +# ((startFrame=startFrame+stride)) +# ((batchCount=batchCount+1)) +#done +# +## wait for all pids +#for currPID in ${pidList[*]}; do +# wait $currPID +# echo "Done" +#done +# +# +#echo "Batch 4" +#date +# +#startFrame=700 +#lastNumber=800 +# +#stride=$(((lastNumber-startFrame)/processCount)) +# +##stride=45 +##lastNumber=359 +#batchCount=0 +#pidList=() +# +#until [ $startFrame -ge $lastNumber ] +#do +# echo startFrame: $startFrame & +# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& +# pidList[${batchCount}]=$! +# ((startFrame=startFrame+stride)) +# ((batchCount=batchCount+1)) +#done +# +## wait for all pids +#for currPID in ${pidList[*]}; do +# wait $currPID +# echo "Done" +#done +# +# +#echo "Batch 5" +#date +# +#startFrame=800 +#lastNumber=900 +# +#stride=$(((lastNumber-startFrame)/processCount)) +# +#batchCount=0 +#pidList=() +# +#until [ $startFrame -ge $lastNumber ] +#do +# echo startFrame: $startFrame & +# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& +# pidList[${batchCount}]=$! +# ((startFrame=startFrame+stride)) +# ((batchCount=batchCount+1)) +#done +# +## wait for all pids +#for currPID in ${pidList[*]}; do +# wait $currPID +# echo "Done" +#done +# +# +## AFTER all the processes finish +#python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' +# diff --git a/fractal.py b/fractal.py index a4703af..de061cc 100644 --- a/fractal.py +++ b/fractal.py @@ -1175,12 +1175,50 @@ def set_demo1_params(fractal_ctx, view_ctx): # Finally timed that run # 50 frames (375-425) - # 2048*8 max iter + # 2048*8 max iter (== 16384 iters) # 825 digits (2739 bits) - # + # real 61m38.026s + # user 514m30.844s + # sys 0m9.203s + # Which is ~74 seconds per frame. + # ~10,400 iters used + # ~e-129 widths + + # Moving endpoint to 320 seconds? (~957 frames?) + # Going up to 2048 * 10 (= 20,480 iters) + # Looks like the last increment used 15 more digits... + # So, Maybe add 100 positions? + # That'll be 925 digits (3071 bits) + # real 30m22.199s + # user 381m58.938s + # sys 0m12.344s + # hit e-135 at 450 + # using 11,000 iters + + + # Frames 450-475 (no big changes for this segment) + # real 38m16.362s + # user 445m59.469s + # sys 0m12.938s + # hit e-143 at 475 + # using ~14,800 itersA + # Looks like this gets to ~10:30 in the edge of infinity dive. + # So, out of 2:29:04, that's about 6% of the dive? + # + # Gonna raise to 41k iterations (2048 * 20) + # Gonna bump up to 1500 digits? (4830 bits?) + # Trying to take a bigger bite, 475-900? + + # Attempt at batching to 599 failed the first time, + # BUT resulted in: + # used 27k iter + # e-180 widths + # Looks like this was gonna work just fine... + + # 25 frames hit ~109 minutes (~4.5 minutes per frame) + # (1308 minutes user time) = 52 minutes per frame. + # Iters hit ~24k - - #fractal_ctx.max_iter = 128 @@ -1188,7 +1226,7 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.max_iter = 512 # covers ~e-34 or so #fractal_ctx.max_iter = 1024 # covers ~e-48 or so #fractal_ctx.max_iter = 2048 - fractal_ctx.max_iter = 2048 * 8 + fractal_ctx.max_iter = 2048 * 20 fractal_ctx.escape_rad = 2 #fractal_ctx.escape_rad = 4 @@ -1214,7 +1252,7 @@ def set_demo1_params(fractal_ctx, view_ctx): #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) #view_ctx.duration = 4.0 - view_ctx.duration = 180.0 + view_ctx.duration = 540.0 #view_ctx.duration = 30.0 #view_ctx.duration = 2.0 #view_ctx.duration = 0.25 diff --git a/fractalmath.py b/fractalmath.py index 1aaff61..39353f1 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -16,7 +16,7 @@ #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol #FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) -FLINT_HIGH_PRECISION_SIZE = int(825 * 3.32) +FLINT_HIGH_PRECISION_SIZE = int(1500 * 3.32) # For debugging, looks like we're bottoming out somewhere around e-11 # So, only really need ~20 digits for this test @@ -661,20 +661,16 @@ def clamp(self, num, min_value, max_value): class DiveMathSupportFlintCustom(DiveMathSupportFlint): def mandelbrot(self, c, escapeRadius, maxIter): - #radiusArb = self.flint.arb(escapeRadius) - #return c.libmandelbrot(radiusArb, maxIter) - return c.libmandelbrot(escapeRadius, maxIter) - #return c.libmandelbrot_full(escapeRadius, maxIter) - #print("Calling library, radius: %s, iter: %s" % (str(escapeRadius), str(maxIter))) - #(tmpRes, tmpZ) = c.libmandelbrot_full(escapeRadius, maxIter) - #print("%d" % (tmpRes)) - #print("%d - %s" % (tmpRes, str(tmpZ))) - #print("%d - %s" % (tmpRes, str(abs(tmpZ)))) - #return (tmpRes, tmpZ) - - def libMandelbrotFull(self, c, escapeRadius, maxIter): - """ Reference implementation, just for error check? """ - return c.libmandelbrot_full(escapeRadius, maxIter) + return c.our_mandelbrot(escapeRadius, maxIter) +# return c.libmandelbrot(escapeRadius, maxIter) + +# def libMandelbrotFull(self, c, escapeRadius, maxIter): +# """ Reference implementation, just for error check? """ +# return c.libmandelbrot_full(escapeRadius, maxIter) +# +# def ourMandelbrot(self, c, escapeRadius, maxIter): +# #return c.libmandelbrot(escapeRadius, maxIter) +# return c.our_mandelbrot(escapeRadius, maxIter) class DiveMathSupportGmp(DiveMathSupport): """ From ebe88a3c2d45fdb316f70e437e2dd3283b867251 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Mon, 9 Aug 2021 14:24:50 -0700 Subject: [PATCH 27/44] Making DiveMathSupportFlintCustom use the step-wise mandelbrot as a default. Other tinkering with parameters that doesn't really need to be checked in, but here we are. --- batch_build.sh | 26 ++------------ fractal.py | 17 ++++++--- fractalmath.py | 85 +++++++++++++++++++++++++++++++++++++++------ test_fractalmath.py | 80 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 165 insertions(+), 43 deletions(-) diff --git a/batch_build.sh b/batch_build.sh index 18f9ba1..08c4e44 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -31,8 +31,8 @@ processCount=9 echo "Batch 2" date -startFrame=500 -lastNumber=525 +startFrame=525 +lastNumber=550 stride=$(((lastNumber-startFrame)/processCount)) @@ -54,28 +54,6 @@ for currPID in ${pidList[*]}; do echo "Done" done -#startFrame=525 -#lastNumber=550 -# -#stride=$(((lastNumber-startFrame)/processCount)) -# -#batchCount=0 -#pidList=() -# -#until [ $startFrame -ge $lastNumber ] -#do -# echo startFrame: $startFrame & -# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& -# pidList[${batchCount}]=$! -# ((startFrame=startFrame+stride)) -# ((batchCount=batchCount+1)) -#done -# -## wait for all pids -#for currPID in ${pidList[*]}; do -# wait $currPID -# echo "Done" -#done #startFrame=550 #lastNumber=600 # diff --git a/fractal.py b/fractal.py index de061cc..0cc38c4 100644 --- a/fractal.py +++ b/fractal.py @@ -290,7 +290,7 @@ def construct_simple_timeline(self): end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) - #print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) + print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm_name=self.ctx.algorithm_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) @@ -1062,8 +1062,10 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.img_height = 768 #fractal_ctx.img_width = 320 #fractal_ctx.img_height = 240 + fractal_ctx.img_width = 160 fractal_ctx.img_height = 120 + #fractal_ctx.img_width = 80 #fractal_ctx.img_height = 60 #fractal_ctx.img_width = 16 @@ -1218,7 +1220,10 @@ def set_demo1_params(fractal_ctx, view_ctx): # 25 frames hit ~109 minutes (~4.5 minutes per frame) # (1308 minutes user time) = 52 minutes per frame. # Iters hit ~24k - + + # real 142m25.250s + # user 1797m10.031s + # sys 0m11.922s #fractal_ctx.max_iter = 128 @@ -1226,7 +1231,9 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.max_iter = 512 # covers ~e-34 or so #fractal_ctx.max_iter = 1024 # covers ~e-48 or so #fractal_ctx.max_iter = 2048 - fractal_ctx.max_iter = 2048 * 20 + #fractal_ctx.max_iter = 2048 * 16 + fractal_ctx.max_iter = 2048 * 71 # About half way up EoI's dive + #fractal_ctx.max_iter = 2048 * 142 # Near the end of EoI's dive, maybe fractal_ctx.escape_rad = 2 #fractal_ctx.escape_rad = 4 @@ -1252,7 +1259,9 @@ def set_demo1_params(fractal_ctx, view_ctx): #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) #view_ctx.duration = 4.0 - view_ctx.duration = 540.0 + #view_ctx.duration = 540.0 + view_ctx.duration = 2200.0 # A bit more than EoI's dive, if a .5 zoom factor at 23,976/8 fps. + #view_ctx.duration = 1500.0 #view_ctx.duration = 30.0 #view_ctx.duration = 2.0 #view_ctx.duration = 0.25 diff --git a/fractalmath.py b/fractalmath.py index 39353f1..2067bec 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -15,8 +15,9 @@ #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol +FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol #FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) -FLINT_HIGH_PRECISION_SIZE = int(1500 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(1500 * 3.32) # For debugging, looks like we're bottoming out somewhere around e-11 # So, only really need ~20 digits for this test @@ -73,7 +74,8 @@ def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterati because if any keyframes are added, the best we can do is measure an effective zoom factor between any 2 frames. - Somehow, this seems to be ok for flint too?! + Note: Flint's __pow__ needs TWO arb types as args, or else it drops + to default python bit depths """ return startValue * (overallZoomFactor ** iterations) @@ -531,6 +533,21 @@ def createFloat(self, floatValue): def floor(self, value): return self.flint.arb(value).floor() + def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): + """ + Shortcut calculate the starting point for the last frame's properties, + which we'll use for instantiation with specific widths. This is + because if any keyframes are added, the best we can do is measure an + effective zoom factor between any 2 frames. + + Note: Flint's __pow__ needs TWO arb types as args, or else it drops + to default python bit depths + """ + zoomAsArb = self.flint.arb(overallZoomFactor) + iterationsAsArb = self.flint.arb(iterations) + return startValue * pow(zoomAsArb, iterationsAsArb) + + def interpolateLogTo(self, startX, startY, endX, endY, targetX): """ Probably want additional log-defining params, but for now, let's just bake in one equation @@ -543,6 +560,56 @@ def interpolateLogTo(self, startX, startY, endX, endY, targetX): aVal = (endY - startY) / (self.flint.arb(endX - startX + 1).log()) return aVal * (self.flint.arb(targetX - startX + 1).log()) + startY + def interpolateRootTo(self, paramStartX, paramStartY, paramEndX, paramEndY, paramTargetX): + """ + Iterative multiplications of window sizes for zooming means + we want to be able to interpolate between two points using + the frame count as the root. + """ + startX = self.flint.arb(paramStartX) + startY = self.flint.arb(paramStartY) + endX = self.flint.arb(paramEndX) + endY = self.flint.arb(paramEndY) + targetX = self.flint.arb(paramTargetX) + + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + root = endX - startX + scaleFactor = (endY / startY) ** (1 / root) + return startY * (scaleFactor ** targetX) + + def interpolateQuadraticEaseOut(self, paramStartX, paramStartY, paramEndX, paramEndY, paramTargetX): + """ + QuadraticEaseOut leaves the majority of changes to the end of the X range. + + Probably want additional quadratic params, but for now, let's just bake in one equation + which uses the first point as the vertex, and passes through the second point. + """ + startX = self.flint.arb(paramStartX) + startY = self.flint.arb(paramStartY) + endX = self.flint.arb(paramEndX) + endY = self.flint.arb(paramEndY) + targetX = self.flint.arb(paramTargetX) + + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + # Find a, given that the start point is the vertex, and the parabola passes + # through the other point + # y = a * (x - h)**2 + k + # y = a * (x - startX)**2 + startY + # endY = a * (endX - startX)**2 + startY + # a = ((endY-startY)/((endX-startX)**2) + # + # answer = a * (targetX - startX)**2 + startY + # answer = ((endY-startY)/((endX-startX)**2) * ((targetX-startX)**2) + startY + return (endY-startY)/((endX-startX)**self.flint.arb(2.0)) * ((targetX-startX)**self.flint.arb(2.0)) + startY + def mandelbrot(self, c, escapeRadius, maxIter): """ Now that smoothing is handled separately, the native python implementation @@ -661,16 +728,12 @@ def clamp(self, num, min_value, max_value): class DiveMathSupportFlintCustom(DiveMathSupportFlint): def mandelbrot(self, c, escapeRadius, maxIter): - return c.our_mandelbrot(escapeRadius, maxIter) -# return c.libmandelbrot(escapeRadius, maxIter) + """ Slightly more efficient for HIGH maxIter values """ + return c.our_steps_mandelbrot(escapeRadius, maxIter) -# def libMandelbrotFull(self, c, escapeRadius, maxIter): -# """ Reference implementation, just for error check? """ -# return c.libmandelbrot_full(escapeRadius, maxIter) -# -# def ourMandelbrot(self, c, escapeRadius, maxIter): -# #return c.libmandelbrot(escapeRadius, maxIter) -# return c.our_mandelbrot(escapeRadius, maxIter) + def mandelbrot_beginning(self, c, escapeRadius, maxIter): + """ Slightly more efficient for LOW maxIter values """ + return c.our_mandelbrot(escapeRadius, maxIter) class DiveMathSupportGmp(DiveMathSupport): """ diff --git a/test_fractalmath.py b/test_fractalmath.py index 2cd63aa..941cef7 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -44,11 +44,11 @@ def test_interpolateLinear(self): def test_mandelbrot(self): # Native python isn't accurate past ~120 iterations. # So, we either reduce iterations, and require fewer decimal places to match. + radius = 2.0 + maxIterations = 100 placesToMatch = 9 - radius = 2.0 - # The answer for this particular point, is supposed to be 133, but we're # capping iterations to 100 here. center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') @@ -56,8 +56,8 @@ def test_mandelbrot(self): #print(str(iterations)) #print(str(lastZee)) self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 # Slightly adjusted center, just to get a different answer center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') @@ -68,6 +68,23 @@ def test_mandelbrot(self): self.assertAlmostEqual(-2.182516841632256, float(lastZee.real), placesToMatch) self.assertAlmostEqual(2.3301940018826137, float(lastZee.imag), placesToMatch) + + # More iterations, fewer decimal places required to match for comparison + # 3 digits seems to be near the low-end for correctness of the iteration count? + maxIterations = 110 + placesToMatch = 3 + + # The answer for this particular point, is supposed to be 133, but we're + # capping iterations to 110 here. + center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(110, iterations) + self.assertAlmostEqual(0.04427332710633869, float(lastZee.real), placesToMatch) # when maxIterations == 110 + self.assertAlmostEqual(0.053034798128938195, float(lastZee.imag), placesToMatch) # when maxIterations == 110 + + class TestMathSupportFlint(TestMathSupport): @classmethod def setUpClass(cls): @@ -86,6 +103,61 @@ def setUpClass(cls): # prec value *should* be 53 to match precision. cls.mathSupport.flint.ctx.prec = 53 + def testDifferentMandelbrots(self): + """ + This checks results between different mandelbrot implementations. + + Currently there are 4 implementations of varying speeds, + 1.) A pure python version (in DiveMathSupport.mandelbrot()) + 2.) A python-flint version (in DiveMathSupportFlint.mandelbrot()) + 3.) A 'normal' cython version (in DiveMathSupportFlintCustom.mandelbrot_beginning()) + 4.) A 'step-wise' cython version (in DiveMathSupportFlintCustom.mandelbrot()) + + Kinda hacky to explicitly instantiate other mathSupports here, but it's + probably sufficient for checking sanity, unless and until all the matn supports + implement all of the options? + """ + placesToMatch = 9 + radius = 2.0 + + maxIterations = 100 + + center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') + + + localMathSupport = fm.DiveMathSupport() + # Native python basically good for ~120 iterations, using 53 bits of depth in a float 64 + (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + + localMathSupport = fm.DiveMathSupportFlint() + localMathSupport.flint.ctx.prec = 53 + (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + + localMathSupport = fm.DiveMathSupportFlintCustom() + localMathSupport.flint.ctx.prec = 53 + (iterations, lastZee) = localMathSupport.mandelbrot_beginning(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + + (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 if __name__ == '__main__': unittest.main() From ba35f2f50e31218b0dd335c6d9ab7b81d6e80bdc Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Tue, 10 Aug 2021 21:08:55 -0700 Subject: [PATCH 28/44] added a couple flint precision-aware calls to the custom flint mandelbrot, which I think is starting to let us peer into accuracy behaviors with very small numbers --- fractal.py | 7 +++++-- fractalmath.py | 25 ++++++++++++++++++++++-- test_fractalmath.py | 47 +++++++++++++++++++++++---------------------- 3 files changed, 52 insertions(+), 27 deletions(-) diff --git a/fractal.py b/fractal.py index 0cc38c4..6fe69e0 100644 --- a/fractal.py +++ b/fractal.py @@ -1063,14 +1063,17 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.img_width = 320 #fractal_ctx.img_height = 240 - fractal_ctx.img_width = 160 - fractal_ctx.img_height = 120 +# fractal_ctx.img_width = 160 +# fractal_ctx.img_height = 120 #fractal_ctx.img_width = 80 #fractal_ctx.img_height = 60 #fractal_ctx.img_width = 16 #fractal_ctx.img_height = 12 + fractal_ctx.img_width = 8 + fractal_ctx.img_height = 4 + cmplx_width_str = '5.0' cmplx_height_str = '3.5' #cmplx_width_str = '.001' diff --git a/fractalmath.py b/fractalmath.py index 2067bec..e669f60 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -729,11 +729,32 @@ def clamp(self, num, min_value, max_value): class DiveMathSupportFlintCustom(DiveMathSupportFlint): def mandelbrot(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ - return c.our_steps_mandelbrot(escapeRadius, maxIter) + #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + (answer, lastZ, remainingPrecision) = c.our_steps_mandelbrot(escapeRadius, maxIter) + #print("answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + return(answer, lastZ) + + def mandelbrot_check_precision(self, c, escapeRadius, maxIter): + """ Slightly more efficient for HIGH maxIter values """ + #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + (answer, lastZ, remainingPrecision) = c.our_steps_mandelbrot(escapeRadius, maxIter) + #print("answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + return(answer, lastZ, remainingPrecision) def mandelbrot_beginning(self, c, escapeRadius, maxIter): """ Slightly more efficient for LOW maxIter values """ - return c.our_mandelbrot(escapeRadius, maxIter) + #print("mandelbrot_beginning center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + (answer, lastZ, remainingPrecision) = c.our_mandelbrot(escapeRadius, maxIter) + #print("beginning answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + return(answer, lastZ) + + def mandelbrot_beginning_check_precision(self, c, escapeRadius, maxIter): + """ Slightly more efficient for LOW maxIter values """ + #print("mandelbrot_beginning center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + (answer, lastZ, remainingPrecision) = c.our_mandelbrot(escapeRadius, maxIter) + #print("beginning answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + return(answer, lastZ, remainingPrecision) + class DiveMathSupportGmp(DiveMathSupport): """ diff --git a/test_fractalmath.py b/test_fractalmath.py index 941cef7..b498a7d 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -103,9 +103,9 @@ def setUpClass(cls): # prec value *should* be 53 to match precision. cls.mathSupport.flint.ctx.prec = 53 - def testDifferentMandelbrots(self): + def test_differentMandelbrots(self): """ - This checks results between different mandelbrot implementations. + This checks results between different custom mandelbrot implementations. Currently there are 4 implementations of varying speeds, 1.) A pure python version (in DiveMathSupport.mandelbrot()) @@ -113,51 +113,52 @@ def testDifferentMandelbrots(self): 3.) A 'normal' cython version (in DiveMathSupportFlintCustom.mandelbrot_beginning()) 4.) A 'step-wise' cython version (in DiveMathSupportFlintCustom.mandelbrot()) - Kinda hacky to explicitly instantiate other mathSupports here, but it's - probably sufficient for checking sanity, unless and until all the matn supports - implement all of the options? + Since 2 of these (#3 and #4) belong to the custom math support, we make sure their + values match each other here. Could be more flexible and remember the answer, but oh well. """ placesToMatch = 9 radius = 2.0 maxIterations = 100 - center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') - - localMathSupport = fm.DiveMathSupport() + center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') # Native python basically good for ~120 iterations, using 53 bits of depth in a float 64 - (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) #print(str(iterations)) - #print(str(lastZee)) + #print("lastZee as string: %s" % str(lastZee)) self.assertEqual(100, iterations) self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 - localMathSupport = fm.DiveMathSupportFlint() - localMathSupport.flint.ctx.prec = 53 - (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + # Slightly adjusted center, just to get a different answer + center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) #print(str(iterations)) #print(str(lastZee)) - self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + self.assertEqual(3, iterations) + self.assertAlmostEqual(-2.182516841632256, float(lastZee.real), placesToMatch) + self.assertAlmostEqual(2.3301940018826137, float(lastZee.imag), placesToMatch) - localMathSupport = fm.DiveMathSupportFlintCustom() - localMathSupport.flint.ctx.prec = 53 - (iterations, lastZee) = localMathSupport.mandelbrot_beginning(center, radius, maxIterations) + center = self.mathSupport.createComplex('-1.7693831791+0.0042368479j') + # Native python basically good for ~120 iterations, using 53 bits of depth in a float 64 + (iterations, lastZee) = self.mathSupport.mandelbrot_beginning(center, radius, maxIterations) #print(str(iterations)) #print(str(lastZee)) self.assertEqual(100, iterations) self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 - (iterations, lastZee) = localMathSupport.mandelbrot(center, radius, maxIterations) + # Slightly adjusted center, just to get a different answer + center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') + (iterations, lastZee) = self.mathSupport.mandelbrot_beginning(center, radius, maxIterations) #print(str(iterations)) #print(str(lastZee)) - self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + self.assertEqual(3, iterations) + self.assertAlmostEqual(-2.182516841632256, float(lastZee.real), placesToMatch) + self.assertAlmostEqual(2.3301940018826137, float(lastZee.imag), placesToMatch) + + #print("Done comparing") if __name__ == '__main__': unittest.main() From 1c1d9d0ea9d3343651846893dd747d798f1f547e Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Wed, 11 Aug 2021 23:42:07 -0700 Subject: [PATCH 29/44] Updated smoothing calculations in fractalmath to match the latest from main branch. Debugged the not-in-this-source-tree-yet custom flint mandelbrot implementations a bit. --- batch_build.sh | 71 ++++++---------------------------------------- fractal.py | 26 +++++++++-------- fractalmath.py | 15 +++++++--- mandelbrot_solo.py | 1 + smooth.py | 29 ++++++++++--------- 5 files changed, 49 insertions(+), 93 deletions(-) diff --git a/batch_build.sh b/batch_build.sh index 08c4e44..b567d87 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -26,13 +26,10 @@ #processCount=12 # Tried 12 again, think it's very slow. -processCount=9 +processCount=4 -echo "Batch 2" -date - -startFrame=525 -lastNumber=550 +startFrame=610 +lastNumber=646 stride=$(((lastNumber-startFrame)/processCount)) @@ -54,58 +51,7 @@ for currPID in ${pidList[*]}; do echo "Done" done -#startFrame=550 -#lastNumber=600 -# -#stride=$(((lastNumber-startFrame)/processCount)) -# -#batchCount=0 -#pidList=() -# -#until [ $startFrame -ge $lastNumber ] -#do -# echo startFrame: $startFrame & -# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& -# pidList[${batchCount}]=$! -# ((startFrame=startFrame+stride)) -# ((batchCount=batchCount+1)) -#done -# -## wait for all pids -#for currPID in ${pidList[*]}; do -# wait $currPID -# echo "Done" -#done -# -# -#echo "Batch 3" -#date -# -# -#startFrame=600 -#lastNumber=700 -# -#stride=$(((lastNumber-startFrame)/processCount)) -# -#batchCount=0 -#pidList=() -# -#until [ $startFrame -ge $lastNumber ] -#do -# echo startFrame: $startFrame & -# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& -# pidList[${batchCount}]=$! -# ((startFrame=startFrame+stride)) -# ((batchCount=batchCount+1)) -#done -# -## wait for all pids -#for currPID in ${pidList[*]}; do -# wait $currPID -# echo "Done" -#done -# -# + #echo "Batch 4" #date # @@ -160,11 +106,10 @@ done # wait $currPID # echo "Done" #done -# -# -## AFTER all the processes finish -#python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.gif' -# + + +# AFTER all the processes finish +python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.mp4' diff --git a/fractal.py b/fractal.py index 6fe69e0..e5c9575 100644 --- a/fractal.py +++ b/fractal.py @@ -1063,16 +1063,16 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.img_width = 320 #fractal_ctx.img_height = 240 -# fractal_ctx.img_width = 160 -# fractal_ctx.img_height = 120 + fractal_ctx.img_width = 160 + fractal_ctx.img_height = 120 #fractal_ctx.img_width = 80 #fractal_ctx.img_height = 60 #fractal_ctx.img_width = 16 #fractal_ctx.img_height = 12 - fractal_ctx.img_width = 8 - fractal_ctx.img_height = 4 + #fractal_ctx.img_width = 4 + #fractal_ctx.img_height = 3 cmplx_width_str = '5.0' cmplx_height_str = '3.5' @@ -1234,15 +1234,17 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.max_iter = 512 # covers ~e-34 or so #fractal_ctx.max_iter = 1024 # covers ~e-48 or so #fractal_ctx.max_iter = 2048 - #fractal_ctx.max_iter = 2048 * 16 - fractal_ctx.max_iter = 2048 * 71 # About half way up EoI's dive - #fractal_ctx.max_iter = 2048 * 142 # Near the end of EoI's dive, maybe + #fractal_ctx.max_iter = 2048 * 15 # ~30k, ok at e-180? + #fractal_ctx.max_iter = 2048 * 20 # ~41k, ok at e-391 + #fractal_ctx.max_iter = 2048 * 40 # ~82k, ok at e-497 + fractal_ctx.max_iter = 2048 * 34 # ~70k, ok at e-504 + #fractal_ctx.max_iter = 2048 * 71 # ~145k, ok at e-752 + #fractal_ctx.max_iter = 2048 * 140 # ~286k iter, ok at e-1204 + #fractal_ctx.max_iter = 2048 * 300 # ~614k iter, ok at e-1505 + #fractal_ctx.max_iter = 2048 * 400 # ~819k iter, ok at e-1806 + #fractal_ctx.max_iter = 2048 * 1300 # ~2,662k iter, ok at e-2011 fractal_ctx.escape_rad = 2 - #fractal_ctx.escape_rad = 4 - #fractal_ctx.escape_rad = 8 - #fractal_ctx.escape_rad = 512 - #fractal_ctx.escape_rad = math.sqrt(1024.0) fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['escape_radius'] = fractal_ctx.escape_rad fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['max_escape_iterations'] = fractal_ctx.max_iter @@ -1263,7 +1265,7 @@ def set_demo1_params(fractal_ctx, view_ctx): #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) #view_ctx.duration = 4.0 #view_ctx.duration = 540.0 - view_ctx.duration = 2200.0 # A bit more than EoI's dive, if a .5 zoom factor at 23,976/8 fps. + view_ctx.duration = 2300.0 # A bit more than EoI's dive (6682), this is ~6890 frames, if a .5 zoom factor at 23,976/8 fps. #view_ctx.duration = 1500.0 #view_ctx.duration = 30.0 #view_ctx.duration = 2.0 diff --git a/fractalmath.py b/fractalmath.py index e669f60..5c759bf 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -14,10 +14,12 @@ #3.32 bits per digit, on average #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling +#FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol -FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol -#FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) -#FLINT_HIGH_PRECISION_SIZE = int(1500 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol +FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) # For debugging, looks like we're bottoming out somewhere around e-11 # So, only really need ~20 digits for this test @@ -642,7 +644,8 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): # The following code smooths out the colors so there aren't bands # Algorithm taken from http://linas.org/art-gallery/escape/escape.html # Note: Results in a float. We think. - return float(endingIter + 1 - self.twoLogsHelper(endingZ, escapeRadius) / math.log(2.0)) + #return float(endingIter - self.twoLogsHelper(endingZ, escapeRadius) / math.log(2.0)) + return float(endingIter - self.justTwoLogs(self.squared_modulus(endingZ)) + 4.0) def justTwoLogs(self, value): return self.flint.arb(value).log().log() @@ -730,8 +733,10 @@ class DiveMathSupportFlintCustom(DiveMathSupportFlint): def mandelbrot(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + #print("mandelbrot maxIter: %s" % (str(maxIter))) (answer, lastZ, remainingPrecision) = c.our_steps_mandelbrot(escapeRadius, maxIter) #print("answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + #print("answer: %s, remainingPrecision: %s" % (str(answer), str(remainingPrecision))) return(answer, lastZ) def mandelbrot_check_precision(self, c, escapeRadius, maxIter): @@ -744,8 +749,10 @@ def mandelbrot_check_precision(self, c, escapeRadius, maxIter): def mandelbrot_beginning(self, c, escapeRadius, maxIter): """ Slightly more efficient for LOW maxIter values """ #print("mandelbrot_beginning center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) + #print("mandelbrot_beginning radius: %s maxIter: %s" % (str(escapeRadius), str(maxIter))) (answer, lastZ, remainingPrecision) = c.our_mandelbrot(escapeRadius, maxIter) #print("beginning answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) + #print("beginning answer: %s, remainingPrecision: %s" % (str(answer), str(remainingPrecision))) return(answer, lastZ) def mandelbrot_beginning_check_precision(self, c, escapeRadius, maxIter): diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py index 75e460f..c0a6c47 100644 --- a/mandelbrot_solo.py +++ b/mandelbrot_solo.py @@ -48,6 +48,7 @@ def calculate_results(self): mesh_array = self.dive_mesh.generateMesh() math_support = self.dive_mesh.mathSupport + #mandelbrot_function = np.vectorize(math_support.mandelbrot_beginning) mandelbrot_function = np.vectorize(math_support.mandelbrot) (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) diff --git a/smooth.py b/smooth.py index 7dc61bf..e1200df 100644 --- a/smooth.py +++ b/smooth.py @@ -38,7 +38,8 @@ def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_pa super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) self.algorithm_name = 'smooth' - self.color = extra_params.get('color', None) + #self.color = extra_params.get('color', None) + self.color = extra_params.get('color', (.1,.2,.3)) def calculate_results(self): mesh_array = self.dive_mesh.generateMesh() @@ -57,6 +58,10 @@ def calculate_results(self): self.cache_frame.frame_info.raw_values = pixel_values_2d self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed + #print(pixel_values_2d) + #print("\n\n") + #print(pixel_values_2d_smoothed) + #print("\n\n") return def generate_image(self): @@ -76,6 +81,7 @@ def generate_image(self): # NOTE: This is the difference - not using palette, but # instead, a (kinda locally) calculated range color = self.map_value_to_color(pixel_values_2d[x,y]) + #print("will print color: (%d,%d,%d)" % (color[0], color[1], color[2])) # Plot the point draw.point([x, y], color) @@ -89,15 +95,10 @@ def generate_image(self): return im def map_value_to_color(self, val): - - magnification = 1. / self.dive_mesh.imagMeshGenerator.baseWidth - if magnification <= 100: - magnification = 100 - - denom = float(self.dive_mesh.mathSupport.justTwoLogs(magnification)) - if denom <= 0.0 or math.isnan(denom): + if math.isnan(val): return (0,0,0) - elif self.color: + + if self.color: # (yellow blue 0,.6,1.0) c1 = 1 + math.cos(3.0 + val*0.15 + self.color[0]) c2 = 1 + math.cos(3.0 + val*0.15 + self.color[1]) @@ -106,20 +107,20 @@ def map_value_to_color(self, val): if c1 <= 0 or math.isnan(c1): c1int = 0 else: - c1int = int(255.*((c1/4.) * 3.) / denom) + c1int = int(255.*((c1/4.) * 3.) / 1.5) if c2 <= 0 or math.isnan(c2): c2int = 0 else: - c2int = int(255.*((c2/4.) * 3.) / denom) + c2int = int(255.*((c2/4.) * 3.) / 1.5) if c3 <= 0 or math.isnan(c3): c3int = 0 else: - c3int = int(255.*((c3/4.) * 3.) / denom) + c3int = int(255.*((c3/4.) * 3.) / 1.5) return (c1int,c2int,c3int) else: - #magnification = 1. / self.context.cmplx_width - cint = int((val * 3.) / denom) + c1 = 1 + math.cos(3.0 + val*0.15) + cint = int(255.*((c1/4.) * 3.) / 1.5) return (cint,cint,cint) From b0e10f6f0072dadec44687db7bc1f64621e24fca Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Fri, 13 Aug 2021 14:18:21 -0700 Subject: [PATCH 30/44] Re-implemented a Python Decimal MathSupport, without great results. Fixed one-off errors across mandelbrot implementations. Added a timing comparison script to help study different solution sets. --- batch_build.sh | 10 +- divemesh.py | 21 ++++ fractal.py | 4 +- fractalmath.py | 247 ++++++++++++++++++++++++++++++++++++------- test_fractalmath.py | 30 ++++-- timing_comparison.py | 88 +++++++++++++++ 6 files changed, 349 insertions(+), 51 deletions(-) create mode 100644 timing_comparison.py diff --git a/batch_build.sh b/batch_build.sh index b567d87..40e4bce 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # Example of embarassingly parallel start/stop range commands that all write # out tiffs that can be assembled with 'compile_video.py' after finishing. @@ -26,10 +28,12 @@ #processCount=12 # Tried 12 again, think it's very slow. -processCount=4 +processCount=6 -startFrame=610 -lastNumber=646 +# 30 frames per batch, 4 batches, = 120 frames, going a little extra +# 30 frames per process, let's say 6 processes? = 180 frames +startFrame=750 +lastNumber=930 stride=$(((lastNumber-startFrame)/processCount)) diff --git a/divemesh.py b/divemesh.py index 22e658a..7bcac13 100644 --- a/divemesh.py +++ b/divemesh.py @@ -92,6 +92,27 @@ def __init__(self, mathSupport, varyingAxis): raise ValueError("varyingAxis must be one of (%s)" % ", ".join(axisOptions)) self.varyingAxis = varyingAxis +# def __getstate__(self): +# pickleInfo = self.__dict__.copy() +# pickleInfo['mathSupport'] = type(self.mathSupport).__name__ +# +# #for currKey, currValue in pickleInfo.items(): +# # pickleInfo[currKey] = str(currValue) +# +# return pickleInfo +# +# def __setstate__(self, state): +# mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, +# "DiveMathSupportFlint":fm.DiveMathSupportFlint, +# "DiveMathSupport":fm.DiveMathSupport} +# +# mathSupportClassName = state['mathSupport'] +# +# state['mathSupport'] = mathSupportClasses[mathSupportClassName]() +# +# self.__dict__.update(state) + + def generateForDiveMesh(self): raise NotImplementedError("generateForDiveMesh() must be overridden in a MeshGenerator subclass") diff --git a/fractal.py b/fractal.py index e5c9575..7c69927 100644 --- a/fractal.py +++ b/fractal.py @@ -1235,9 +1235,11 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.max_iter = 1024 # covers ~e-48 or so #fractal_ctx.max_iter = 2048 #fractal_ctx.max_iter = 2048 * 15 # ~30k, ok at e-180? + #fractal_ctx.max_iter = int(2048 * 15.5) # ~31k, ok at e-225 + fractal_ctx.max_iter = int(2048 * 16) # ~31k, ok at e-225 #fractal_ctx.max_iter = 2048 * 20 # ~41k, ok at e-391 #fractal_ctx.max_iter = 2048 * 40 # ~82k, ok at e-497 - fractal_ctx.max_iter = 2048 * 34 # ~70k, ok at e-504 + #fractal_ctx.max_iter = 2048 * 34 # ~70k, ok at e-504 #fractal_ctx.max_iter = 2048 * 71 # ~145k, ok at e-752 #fractal_ctx.max_iter = 2048 * 140 # ~286k iter, ok at e-1204 #fractal_ctx.max_iter = 2048 * 300 # ~614k iter, ok at e-1505 diff --git a/fractalmath.py b/fractalmath.py index 5c759bf..7198a53 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -9,28 +9,6 @@ import numpy as np -#FLINT_HIGH_PRECISION_SIZE = 16 # 53 is how many bits are in float64 -#FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 -#3.32 bits per digit, on average -#2200 was therefore, ~662 digits, got 54 frames down at .5 scaling - -#FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol -#FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol -#FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol -FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) -#FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) -#FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) - -# For debugging, looks like we're bottoming out somewhere around e-11 -# So, only really need ~20 digits for this test -# Blowing out frame 30, which is ~28? -#FLINT_HIGH_PRECISION_SIZE = int(50 * 3.32) -# Native blew out somewhere e-13 to e-15 -#FLINT_HIGH_PRECISION_SIZE = int(200 * 3.32) - - -GMP_HIGH_PRECISION_SIZE=53 - class DiveMathSupport: """ Toolbox for math functions that need to be type-aware, so we @@ -50,6 +28,10 @@ class DiveMathSupport: def __init__(self): self.precisionType = 'native' + def setPrecision(self, newPrecision): + """ Seems meaningless for pure python impelmentation, but seems like calling this shouldn't force a crash either? """ + raise NotImplementedError("setPrecision is meaningless for base class, and must be implemented in DiveMathSupport sublcasses") + def createComplex(self, *args): """ Compatible complex types will return values for .real() and .imag() @@ -262,9 +244,14 @@ def mandelbrot(self, c, escapeRadius, maxIter): """ z = complex(0,0) n = 0 - while abs(z) <= escapeRadius and n < maxIter: + + for currIter in range(maxIter + 1): + n = currIter + + if abs(z) > escapeRadius: + break + z = z*z + c - n += 1 return (n, z) @@ -288,9 +275,14 @@ def julia(self, c, z0, escapeRadius, maxIter): """ z = z0 n = 0 - while abs(z) <= escapeRadius and n < maxIter: + + for currIter in range(maxIter + 1): + n = currIter + + if abs(z) > escapeRadius: + break + z = z*z + c - n += 1 return (n, z) @@ -340,14 +332,19 @@ def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): dz = complex(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia absOfZ = 0.0 # Will re-use the final result for smoothing n = 0 - while absOfZ < escapeRadius and n < maxIter: + + for currIter in range(maxIter + 1): + n = currIter + + if absOfZ > escapeRadius: + break + # Z' -> 2·Z·Z' + 1 dz = 2.0 * (z*dz) + 1 # Z -> Z² + c z = z*z + c absOfZ = abs(z) - n += 1 if n == maxIter: return (n, 0.0) @@ -394,6 +391,8 @@ def clamp(self, num, min_value, max_value): return max(min(num, max_value), min_value) def orig_mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): + """ This seems to be old and a bit buggy. At the least it's off by one + for iters, because it doesn't update n and range quite the same? """ c2 = c.real*c.real + c.imag*c.imag # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm if 256.0*c2*c2 - 96.0*c2 + 32.0*c.real - 3.0 < 0.0: @@ -442,9 +441,34 @@ def __init__(self): super().__init__() self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. - self.flint.ctx.prec = FLINT_HIGH_PRECISION_SIZE # Sets flint's precision (in bits) + + #FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 + #3.32 bits per digit, on average + #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling + + #FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol + #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol + #FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol + #FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) + #FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) + #FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) + + # For debugging, looks like we're bottoming out somewhere around e-11 + # So, only really need ~20 digits for this test + # Blowing out frame 30, which is ~28? + #FLINT_HIGH_PRECISION_SIZE = int(50 * 3.32) + # Native blew out somewhere e-13 to e-15 + #FLINT_HIGH_PRECISION_SIZE = int(200 * 3.32) + + self.defaultPrecisionSize = int(500 * 3.32) + self.flint.ctx.prec = self.defaultPrecisionSize # Sets flint's precision (in bits) self.precisionType = 'flint' + def setPrecision(self, newPrecision): + oldPrecision = self.flint.ctx.prec + self.flint.ctx.prec = newPrecision + return oldPrecision + def createComplex(self, *args): """ Flint complex type doesn't accept a string for instantiation. @@ -625,9 +649,14 @@ def mandelbrot(self, c, escapeRadius, maxIter): """ z = self.flint.acb(0,0) n = 0 - while float(z.abs_lower()) <= escapeRadius and n < maxIter: + + for currIter in range(maxIter + 1): + n = currIter + + if float(z.abs_lower()) > escapeRadius: + break + z = z*z + c - n += 1 return (n, z) @@ -665,14 +694,19 @@ def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): dz = self.flint.acb(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia absOfZ = 0.0 # Will re-use the final result for smoothing n = 0 - while float(absOfZ) < escapeRadius and n < maxIter: + + for currIter in range(maxIter + 1): + n = currIter + + if float(absOfZ) > escapeRadius: + break + # Z' -> 2·Z·Z' + 1 dz = 2.0 * (z*dz) + 1 # Z -> Z² + c z = z*z + c absOfZ = z.abs_lower() - n += 1 dzMag = dz.abs_lower() if n == maxIter or dzMag == 0.0: @@ -774,9 +808,15 @@ def __init__(self): super().__init__() self.gmp = __import__('gmpy2') # Only imports if you instantiate this DiveMathSupport subclass. - self.gmp.get_context().precision = GMP_HIGH_PRECISION_SIZE + self.defaultPrecisionSize = 53 + self.gmp.get_context().precision = self.defaultPrecisionSize self.precisionType = 'gmp' + def setPrecision(self, newPrecision): + oldPrecision = self.gmp.get_context().precision + self.gmp.get_context().precision = newPrecision + return oldPrecision + def createComplex(self, *args): """ gmpy2.mpc() accepts one of: @@ -836,9 +876,14 @@ def mandelbrot(self, c, escapeRadius, maxIter): # Because norm() doesn't work for python3-gmp2 v 2.1.0a4 #while float(self.gmp.sqrt(self.gmp.square(z.real) + self.gmp.square(z.imag))) <= escapeRadius and n < maxIter: # looks like abs() gets to the right place, even though there's no explicit abs_lower() in libgmp? - while abs(z) <= escapeRadius and n < maxIter: + for currIter in range(maxIter + 1): + n = currIter + + if abs(z) > escapeRadius: + break + z = z*z + c - n += 1 + return (n, z) def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): @@ -858,4 +903,134 @@ def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): return float(endingIter + 1 - self.gmp.log10(self.gmp.log2(self.gmp.norm(endingZ))) / math.log(2.0)) #return float(endingIter + 1 - self.gmp.log10(self.gmp.log10(self.gmp.sqrt(self.gmp.square(endingZ.real) + self.gmp.square(endingZ.imag))))) +class DecimalComplex: + def __init__(self, realValue, imagValue): + super().__init__() + self.real = realValue + self.imag = imagValue + + def __repr__(self): + return u"DecimalComplex(%s, %s)" % (str(self.real), str(self.imag)) + +class DiveMathSupportDecimal(DiveMathSupport): + def __init__(self): + super().__init__() + + self.decimal = __import__('decimal') # Only imports if you instantiate this DiveMathSupport subclass. + self.defaultPrecisionSize = 16 + self.decimal.getcontext().prec = self.defaultPrecisionSize + self.precisionType = 'decimal' + + def setPrecision(self, newPrecision): + oldPrecision = self.decimal.getcontext().prec + self.decimal.getcontext().prec = newPrecision + return oldPrecision + + def createComplex(self, *args): + if len(args) == 2: + return DecimalComplex(self.decimal.Decimal(args[0]), self.decimal.Decimal(args[1])) + elif len(args) == 1: + if isinstance(args[0], str): + partsString = args[0] + + # Trim off the leading sign + firstIsPositive = True + if partsString.startswith('+'): + partsString = partsString[1:] + elif partsString.startswith('-'): + firstIsPositive = False + partsString = partsString[1:] + + # Trim off the trailing letter + lastIsImag = False + if partsString.endswith('j'): + lastIsImag = True + partsString = partsString[:-1] + + # Remaining string might have an internal sign. + # If there's no internal sign, then the whole remaining + # string is either the real or the complex + positiveParts = partsString.split('+') + negativeParts = partsString.split('-') + + realIsPositive = True + imagIsPositive = True + realPart = "" + imagPart = "" + if len(positiveParts) == 2: + realIsPositive = firstIsPositive + realPart = positiveParts[0] + imagIsPositive = True + imagPart = positiveParts[1] + elif len(negativeParts) == 2: + realIsPositive = firstIsPositive + realPart = negativeParts[0] + imagIsPositive = False + imagPart = negativeParts[1] + elif len(positiveParts) == 1 and len(negativeParts) == 1: + # No internal + or -, so it should just be a number + if lastIsImag == True: + imagPart = partsString + imagIsPositive = firstIsPositive + else: + realPart = partsString + realIsPositive = firstIsPositive + else: + raise ValueError("String parameter not identifiably a complex number, in createComplex()") + + preparedReal = '0.0' + preparedImag = '0.0' + if realPart != "": + if realIsPositive == True: + preparedReal = realPart + else: + preparedReal = "-%s" % realPart + + if imagPart != "": + if imagIsPositive == True: + preparedImag = imagPart + else: + preparedImag = "-%s" % imagPart + + return DecimalComplex(self.decimal.Decimal(preparedReal), self.decimal.Decimal(preparedImag)) + else: + if isinstance(args[0], complex): + return DecimalComplex(self.decimal.Decimal(args[0].real), self.decimal.Decimal(args[0].imag)) + else: + return DecimalComplex(self.decimal.Decimal(args[0]),self.decimal.Decimal(0))# Just a constant, so make a 0-imaginary value + elif len(args) == 0: + return DecimalComplex(self.decimal.Decimal(0),self.decimal.Decimal(0)) + else: + raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") + + def mandelbrot(self, c, escapeRadius, maxIter): + """ + Step-wise impementation, sacrificing some theoretical precision for fewer + multiplications + """ + radSquared = escapeRadius * escapeRadius + + z = DecimalComplex(self.decimal.Decimal(0), self.decimal.Decimal(0)) + n = 0 + + zrSquared = self.decimal.Decimal(0) + ziSquared = self.decimal.Decimal(0) + + for currIter in range(maxIter + 1): + n = currIter + + currMagnitude = zrSquared + ziSquared + if currMagnitude > radSquared: + break + + partSum = z.real + z.imag + z.imag = partSum * partSum - zrSquared - ziSquared + c.imag + z.real = zrSquared - ziSquared + c.real + + zrSquared = z.real * z.real + ziSquared = z.imag * z.imag + + return (n, z) + + diff --git a/test_fractalmath.py b/test_fractalmath.py index b498a7d..5de6858 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -47,7 +47,7 @@ def test_mandelbrot(self): radius = 2.0 maxIterations = 100 - placesToMatch = 9 + placesToMatch = 8 # The answer for this particular point, is supposed to be 133, but we're # capping iterations to 100 here. @@ -56,8 +56,8 @@ def test_mandelbrot(self): #print(str(iterations)) #print(str(lastZee)) self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(0.07974379578605828, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.03921502776351098, float(lastZee.imag), placesToMatch) # when maxIterations == 100 # Slightly adjusted center, just to get a different answer center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') @@ -71,7 +71,7 @@ def test_mandelbrot(self): # More iterations, fewer decimal places required to match for comparison # 3 digits seems to be near the low-end for correctness of the iteration count? - maxIterations = 110 + maxIterations = 110 placesToMatch = 3 # The answer for this particular point, is supposed to be 133, but we're @@ -81,9 +81,17 @@ def test_mandelbrot(self): #print(str(iterations)) #print(str(lastZee)) self.assertEqual(110, iterations) - self.assertAlmostEqual(0.04427332710633869, float(lastZee.real), placesToMatch) # when maxIterations == 110 - self.assertAlmostEqual(0.053034798128938195, float(lastZee.imag), placesToMatch) # when maxIterations == 110 + self.assertAlmostEqual(-1.7702357414195125, float(lastZee.real), placesToMatch) # when maxIterations == 110 + self.assertAlmostEqual(0.00893290183116224, float(lastZee.imag), placesToMatch) # when maxIterations == 110 +class TestMathSupportDecimal(TestMathSupport): + @classmethod + def setUpClass(cls): + cls.mathSupport = fm.DiveMathSupportDecimal() + # Set prec to what gets the closest value for comparing to + # native python. It's a little arbitrary, even knowing the + # prec value *should* be 53 to match precision. + cls.mathSupport.setPrecision(16) class TestMathSupportFlint(TestMathSupport): @classmethod @@ -116,7 +124,7 @@ def test_differentMandelbrots(self): Since 2 of these (#3 and #4) belong to the custom math support, we make sure their values match each other here. Could be more flexible and remember the answer, but oh well. """ - placesToMatch = 9 + placesToMatch = 8 radius = 2.0 maxIterations = 100 @@ -128,8 +136,8 @@ def test_differentMandelbrots(self): #print(str(iterations)) #print("lastZee as string: %s" % str(lastZee)) self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(0.0797437953537099, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.03921502719242046, float(lastZee.imag), placesToMatch) # when maxIterations == 100 # Slightly adjusted center, just to get a different answer center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') @@ -146,8 +154,8 @@ def test_differentMandelbrots(self): #print(str(iterations)) #print(str(lastZee)) self.assertEqual(100, iterations) - self.assertAlmostEqual(1.3599199256223002, float(lastZee.real), placesToMatch) # when maxIterations == 100 - self.assertAlmostEqual(-0.0159758949202937, float(lastZee.imag), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(0.0797437953537099, float(lastZee.real), placesToMatch) # when maxIterations == 100 + self.assertAlmostEqual(-0.03921502719242046, float(lastZee.imag), placesToMatch) # when maxIterations == 100 # Slightly adjusted center, just to get a different answer center = self.mathSupport.createComplex('-1.7693831791-0.5042368479j') diff --git a/timing_comparison.py b/timing_comparison.py new file mode 100644 index 0000000..754ca13 --- /dev/null +++ b/timing_comparison.py @@ -0,0 +1,88 @@ +import flint +import math +import timeit +from fractalmath import * + +############################### +# All these pairings of precision to count are based on the center +# of the Edge of Infinity point +############################### + +decimalPlaces = 16 +mandelbrotMaxIter = 255 +timerRepeatCount = 10000 + +#decimalPlaces = 100 +#mandelbrotMaxIter = 7000 +#timerRepeatCount = 1000 + +#decimalPlaces = 500 +#mandelbrotMaxIter = 35000 +#timerRepeatCount = 100 + +#decimalPlaces = 1500 +#mandelbrotMaxIter = 290000 +#timerRepeatCount = 1 + +#decimalPlaces = 2200 +#mandelbrotMaxIter = 2700000 +#timerRepeatCount = 1 + + +#centerString = '-1.7693831791+0.0042368479j' + +centerString = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884+0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690j' + + + +pyMathSupport = DiveMathSupport() + +flintMathSupport = DiveMathSupportFlint() +flintMathSupport.setPrecision(int(decimalPlaces*3.32)) + +flintCustomMathSupport = DiveMathSupportFlintCustom() +flintCustomMathSupport.setPrecision(int(decimalPlaces*3.32)) + +decMathSupport = DiveMathSupportDecimal() +decMathSupport.setPrecision(decimalPlaces) + +def pyMandelbrotter(): + theCenter = pyMathSupport.createComplex(centerString) + radius = 2.0 + return pyMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) + +def flintMandelbrotter(): + theCenter = flintMathSupport.createComplex(centerString) + radius = 2.0 + return flintMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) + +def flintCustomMandelbrotter(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) + +def flintBeginningMandelbrotter(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.mandelbrot_beginning(theCenter, radius, mandelbrotMaxIter) + +def decMandelbrotter(): + theCenter = decMathSupport.createComplex(centerString) + radius = 2.0 + return decMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) + +def main(): + print("pure python (incorrect when iter > 100)") + print(timeit.Timer(pyMandelbrotter).timeit(number= timerRepeatCount)) + print("flint") + print(timeit.Timer(flintMandelbrotter).timeit(number= timerRepeatCount)) + print("flint custom") + print(timeit.Timer(flintCustomMandelbrotter).timeit(number= timerRepeatCount)) + print("flint low magnification") + print(timeit.Timer(flintBeginningMandelbrotter).timeit(number= timerRepeatCount)) + print("decimal") + print(timeit.Timer(decMandelbrotter).timeit(number= timerRepeatCount)) + +if __name__ == '__main__': + main() + From 631c5a3fbeb95f960999273eb0c34cac8e809789 Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Sun, 15 Aug 2021 01:25:04 -0700 Subject: [PATCH 31/44] Attempting to add usable pickling for DiveMesh classes. --- batch_build.sh | 4 +- divemesh.py | 115 +++++++++++++++++++----- fractalmath.py | 206 +++++++++++++++++++++++++++++++------------ test_divemesh.py | 136 ++++++++++++++++++++++++++++ test_fractalmath.py | 66 +++++++++++++- timing_comparison.py | 4 +- 6 files changed, 447 insertions(+), 84 deletions(-) create mode 100644 test_divemesh.py diff --git a/batch_build.sh b/batch_build.sh index 40e4bce..6767114 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -28,12 +28,12 @@ #processCount=12 # Tried 12 again, think it's very slow. -processCount=6 +processCount=7 # 30 frames per batch, 4 batches, = 120 frames, going a little extra # 30 frames per process, let's say 6 processes? = 180 frames startFrame=750 -lastNumber=930 +lastNumber=778 stride=$(((lastNumber-startFrame)/processCount)) diff --git a/divemesh.py b/divemesh.py index 7bcac13..cef6630 100644 --- a/divemesh.py +++ b/divemesh.py @@ -55,7 +55,53 @@ def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, # pass an extra parameter here and there. self.mathSupport = mathSupport - self.extraParams = extraParams + # Currently, extraParams is NOT pickled + # because we don't know which types the values are + self.extraParams = extraParams + + def __getstate__(self): + pickleInfo = self.__dict__.copy() + # Currently, extraParams is NOT pickled + # because we don't know which types the values are + del(pickleInfo['extraParams']) + + #pickleInfo['meshWidth'] = str(pickleInfo['meshWidth']) + #pickleInfo['meshHeight'] = str(pickleInfo['meshHeight']) + pickleInfo['center'] = str(pickleInfo['center']) + + # Going to encode both the class name of the MathSupport, and + # the 'precision' it was apparently set at, making a string + # like "DiveMathSupportFlint:2048". + mathSupportString = type(self.mathSupport).__name__ + ":" + str(self.mathSupport.precision()) + pickleInfo['mathSupport'] = mathSupportString + + return pickleInfo + + def __setstate__(self, state): + """ + NOTE: A new MathSupport sublass is instantiated during un-pickling. + This can be a problem if you're relying on specific precision settings, + because the only MathSupport configuration that's handled here + is setting the precision from the encode classname:precision string. + + It *is* important to set the precision before loading any numbers, or + else the numbers may be clipped to lower precision than what they + were saved at. + """ + mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, + "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupport":fm.DiveMathSupport} + + (mathSupportClassName, precisionString) = state['mathSupport'].split(':') + #print("mathSupport reads as: %s" % mathSupportClassName) + + state['mathSupport'] = mathSupportClasses[mathSupportClassName]() + state['mathSupport'].setPrecision(int(precisionString)) + #print("mathSupport is: %s" % str(state['mathSupport'])) + + state['center'] = state['mathSupport'].createComplex(state['center']) + + self.__dict__.update(state) def generateMesh(self): realMesh = self.realMeshGenerator.generateForDiveMesh(self) @@ -92,26 +138,38 @@ def __init__(self, mathSupport, varyingAxis): raise ValueError("varyingAxis must be one of (%s)" % ", ".join(axisOptions)) self.varyingAxis = varyingAxis -# def __getstate__(self): -# pickleInfo = self.__dict__.copy() -# pickleInfo['mathSupport'] = type(self.mathSupport).__name__ -# -# #for currKey, currValue in pickleInfo.items(): -# # pickleInfo[currKey] = str(currValue) -# -# return pickleInfo -# -# def __setstate__(self, state): -# mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, -# "DiveMathSupportFlint":fm.DiveMathSupportFlint, -# "DiveMathSupport":fm.DiveMathSupport} -# -# mathSupportClassName = state['mathSupport'] -# -# state['mathSupport'] = mathSupportClasses[mathSupportClassName]() -# -# self.__dict__.update(state) - + def __getstate__(self): + pickleInfo = self.__dict__.copy() + # Going to encode both the class name of the MathSupport, and + # the 'precision' it was apparently set at, making a string + # like "DiveMathSupportFlint:2048". + mathSupportString = type(self.mathSupport).__name__ + ":" + str(self.mathSupport.precision()) + pickleInfo['mathSupport'] = mathSupportString + + return pickleInfo + + def __setstate__(self, state): + """ + NOTE: A new MathSupport sublass is instantiated during un-pickling. + This can be a problem if you're relying on specific precision settings, + because the only MathSupport configuration that's handled here + is setting the precision from the encode classname:precision string. + + It *is* important to set the precision before loading any numbers, or + else the numbers may be clipped to lower precision than what they + were saved at. + """ + mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, + "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupport":fm.DiveMathSupport} + + (mathSupportClassName, precisionString) = state['mathSupport'].split(':') + #print("mathSupport reads as: %s" % mathSupportClassName) + + state['mathSupport'] = mathSupportClasses[mathSupportClassName]() + state['mathSupport'].setPrecision(int(precisionString)) + #print("mathSupport is: %s" % str(state['mathSupport'])) + self.__dict__.update(state) def generateForDiveMesh(self): raise NotImplementedError("generateForDiveMesh() must be overridden in a MeshGenerator subclass") @@ -131,6 +189,21 @@ def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth): self.valuesCenter = valuesCenter self.baseWidth = baseWidth + def __getstate__(self): + pickleInfo = MeshGenerator.__getstate__(self) + + pickleInfo['valuesCenter'] = str(pickleInfo['valuesCenter']) + pickleInfo['baseWidth'] = str(pickleInfo['baseWidth']) + + return pickleInfo + + def __setstate__(self, state): + super().__setstate__(state) + #print("mathSupport exists as: %s" % str(self.mathSupport)) + # Should probably be updating __dict__ instead? + self.valuesCenter = self.mathSupport.createFloat(self.valuesCenter) + self.baseWidth = self.mathSupport.createFloat(self.baseWidth) + def generateForDiveMesh(self, diveMesh): """ e.g. diff --git a/fractalmath.py b/fractalmath.py index 7198a53..8622861 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -30,7 +30,11 @@ def __init__(self): def setPrecision(self, newPrecision): """ Seems meaningless for pure python impelmentation, but seems like calling this shouldn't force a crash either? """ - raise NotImplementedError("setPrecision is meaningless for base class, and must be implemented in DiveMathSupport sublcasses") + pass; + #raise NotImplementedError("setPrecision is meaningless for base class, and must be implemented in DiveMathSupport sublcasses") + + def precision(self): + return 16 # Heh - pytnon's native float64, right? def createComplex(self, *args): """ @@ -42,6 +46,8 @@ def createComplex(self, *args): # detect that, jam them into floats if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str): return complex(float(args[0]), float(args[1])) + #elif len(args) == 1 and isinstance(args[0], str): + # # Strip parens out of the definition string? else: return complex(*args) @@ -469,8 +475,15 @@ def setPrecision(self, newPrecision): self.flint.ctx.prec = newPrecision return oldPrecision + def precision(self): + return self.flint.ctx.prec + def createComplex(self, *args): """ + Flint complex only accepts a flint-style square-bracket complex + string for instantiation. So unless we detect that, we do some + extra string conversions here, to be flexible and consistent. + Flint complex type doesn't accept a string for instantiation. So, we do a string conversion here, to be flexible @@ -483,65 +496,19 @@ def createComplex(self, *args): if isinstance(args[0], str): partsString = args[0] - # Trim off the leading sign - firstIsPositive = True - if partsString.startswith('+'): - partsString = partsString[1:] - elif partsString.startswith('-'): - firstIsPositive = False - partsString = partsString[1:] - - # Trim off the trailing letter - lastIsImag = False - if partsString.endswith('j'): - lastIsImag = True - partsString = partsString[:-1] - - # Remaining string might have an internal sign. - # If there's no internal sign, then the whole remaining - # string is either the real or the complex - positiveParts = partsString.split('+') - negativeParts = partsString.split('-') - - realIsPositive = True - imagIsPositive = True - realPart = "" - imagPart = "" - if len(positiveParts) == 2: - realIsPositive = firstIsPositive - realPart = positiveParts[0] - imagIsPositive = True - imagPart = positiveParts[1] - elif len(negativeParts) == 2: - realIsPositive = firstIsPositive - realPart = negativeParts[0] - imagIsPositive = False - imagPart = negativeParts[1] - elif len(positiveParts) == 1 and len(negativeParts) == 1: - # No internal + or -, so it should just be a number - if lastIsImag == True: - imagPart = partsString - imagIsPositive = firstIsPositive - else: - realPart = partsString - realIsPositive = firstIsPositive - else: - raise ValueError("String parameter not identifiably a complex number, in createComplex()") - preparedReal = '0.0' preparedImag = '0.0' - if realPart != "": - if realIsPositive == True: - preparedReal = realPart - else: - preparedReal = "-%s" % realPart - - if imagPart != "": - if imagIsPositive == True: - preparedImag = imagPart - else: - preparedImag = "-%s" % imagPart + #print("Looking at \"%s\"" % partsString) + if partsString.startswith('['): + # Flint-style 'acb' complex string detected + (preparedReal, preparedImag) = self.separateFlintComplexString(partsString) + else: + (preparedReal, preparedImag) = self.separateOrdinaryComplexString(partsString) + + #print("real: %s" % preparedReal) + #print("imag: %s" % preparedImag) + return self.flint.acb(preparedReal, preparedImag) else: if isinstance(args[0], complex): @@ -553,6 +520,120 @@ def createComplex(self, *args): else: raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") + + def separateFlintComplexString(self, paramPartsString): + partsString = paramPartsString + + lastIsImag = False + if partsString.endswith('j'): + lastIsImag = True + partsString = partsString[:-1] + + # Remaining string might have an internal sign. + # If there's no internal sign, then the whole remaining + # string is either the real or the complex + positiveParts = partsString.split(' + ') + negativeParts = partsString.split(' - ') + + imagIsPositive = True + realPart = "" + imagPart = "" + if len(positiveParts) == 2: + realPart = positiveParts[0] + imagIsPositive = True + imagPart = positiveParts[1] + elif len(negativeParts) == 2: + realPart = negativeParts[0] + imagIsPositive = False + imagPart = negativeParts[1] + elif len(positiveParts) == 1 and len(negativeParts) == 1: + # No internal + or -, so it should just be a number + if lastIsImag == True: + realPart = '0.0' + imagPart = partsString + else: + realPart = partsString + imagPart = '0.0' + else: + raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()->separateFlintComplexString()" % paramPartsString) + + realPart = realPart.strip() + imagPart = imagPart.strip() + + # When imag is negative, need to insert the negative + # inside of the brackets. + if imagPart != '0.0' and imagIsPositive == False: + imagPart = imagPart[:1] + "-" + imagPart[1:] + + return (realPart, imagPart) + + def separateOrdinaryComplexString(self, partsString): + # 'Normal' complex with or without parens + # Trim off surrounding parens, if present + if partsString.startswith('('): + partsString = partsString[1:] + if partsString.endswith(')'): + partsString = partsString[:-1] + + # Trim off the leading sign + firstIsPositive = True + if partsString.startswith('+'): + partsString = partsString[1:] + elif partsString.startswith('-'): + firstIsPositive = False + partsString = partsString[1:] + + # Trim off the trailing letter + lastIsImag = False + if partsString.endswith('j'): + lastIsImag = True + partsString = partsString[:-1] + + # Remaining string might have an internal sign. + # If there's no internal sign, then the whole remaining + # string is either the real or the complex + positiveParts = partsString.split('+') + negativeParts = partsString.split('-') + + realIsPositive = True + imagIsPositive = True + realPart = "" + imagPart = "" + if len(positiveParts) == 2: + realIsPositive = firstIsPositive + realPart = positiveParts[0] + imagIsPositive = True + imagPart = positiveParts[1] + elif len(negativeParts) == 2: + realIsPositive = firstIsPositive + realPart = negativeParts[0] + imagIsPositive = False + imagPart = negativeParts[1] + elif len(positiveParts) == 1 and len(negativeParts) == 1: + # No internal + or -, so it should just be a number + if lastIsImag == True: + imagPart = partsString + imagIsPositive = firstIsPositive + else: + realPart = partsString + realIsPositive = firstIsPositive + else: + raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()" % args[0]) + + if realPart != "": + if realIsPositive == True: + preparedReal = realPart + else: + preparedReal = "-%s" % realPart + + if imagPart != "": + if imagIsPositive == True: + preparedImag = imagPart + else: + preparedImag = "-%s" % imagPart + + return (preparedReal, preparedImag) + def createFloat(self, floatValue): return self.flint.arb(floatValue) @@ -926,6 +1007,9 @@ def setPrecision(self, newPrecision): self.decimal.getcontext().prec = newPrecision return oldPrecision + def precision(self): + return self.decimal.getcontext().prec + def createComplex(self, *args): if len(args) == 2: return DecimalComplex(self.decimal.Decimal(args[0]), self.decimal.Decimal(args[1])) @@ -933,6 +1017,12 @@ def createComplex(self, *args): if isinstance(args[0], str): partsString = args[0] + # Trim off surrounding parens, if present + if partsString.startswith('('): + partsString = partsString[1:] + if partsString.endswith(')'): + partsString = partsString[:-1] + # Trim off the leading sign firstIsPositive = True if partsString.startswith('+'): @@ -976,7 +1066,7 @@ def createComplex(self, *args): realPart = partsString realIsPositive = firstIsPositive else: - raise ValueError("String parameter not identifiably a complex number, in createComplex()") + raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()" % args[0]) preparedReal = '0.0' preparedImag = '0.0' diff --git a/test_divemesh.py b/test_divemesh.py new file mode 100644 index 0000000..4b54560 --- /dev/null +++ b/test_divemesh.py @@ -0,0 +1,136 @@ + +import unittest + +import pickle + +import fractalmath as fm +from divemesh import * + +class TestDiveMesh(unittest.TestCase): + pyMathSupport = None + flintMathSupport = None + + @classmethod + def setUpClass(cls): + cls.pyMathSupport = fm.DiveMathSupport() + cls.flintMathSupport = fm.DiveMathSupportFlint() + + @classmethod + def tearDownClass(cls): + cls.pyMathSupport = None + cls.flintMathSupport = None + + def test_pythonGeneratorPickle(self): + centerFloatString = '-1.769383179195515018213' + baseWidthString = '2.0' + + pyCenter = self.pyMathSupport.createFloat(centerFloatString) + pyWidth = self.pyMathSupport.createFloat(baseWidthString) + + uniformGen = MeshGeneratorUniform(self.pyMathSupport, 'width', pyCenter, pyWidth) + + pickleValue = pickle.dumps(uniformGen) + otherGen = pickle.loads(pickleValue) + + self.assertEqual(uniformGen.valuesCenter, otherGen.valuesCenter) + self.assertEqual(uniformGen.baseWidth, otherGen.baseWidth) + + def test_flintGeneratorPickle(self): + # Maybe better to use a bracket-arb string here? But doesn't matter? + centerFloatString = '-1.769383179195515018213' + baseWidthString = '2.0' + + flintCenter = self.flintMathSupport.createFloat(centerFloatString) + flintWidth = self.flintMathSupport.createFloat(baseWidthString) + + uniformGen = MeshGeneratorUniform(self.flintMathSupport, 'width', flintCenter, flintWidth) + + pickleValue = pickle.dumps(uniformGen) + otherGen = pickle.loads(pickleValue) + + self.assertEqual(float(uniformGen.valuesCenter), float(otherGen.valuesCenter)) + self.assertEqual(float(uniformGen.baseWidth), float(otherGen.baseWidth)) + + def test_pythonDiveMeshPickle(self): + centerWidthString = '-1.769383179195515018213' + centerHeightString = '0.00423684791873677221' + + baseRealWidthString = '5.0' + baseImagWidthString = '3.0' + + mathSupport = self.pyMathSupport + + realCenter = mathSupport.createFloat(centerWidthString) + realWidth = mathSupport.createFloat(baseRealWidthString) + realGen = MeshGeneratorUniform(mathSupport, 'width', realCenter, realWidth) + + imagCenter = mathSupport.createFloat(centerHeightString) + imagWidth = mathSupport.createFloat(baseImagWidthString) + imagGen = MeshGeneratorUniform(mathSupport, 'height', imagCenter, imagWidth) + + centerComplexString = '-1.769383179195515018213+0.00423684791873677221j' + pyComplex = mathSupport.createComplex(centerComplexString) + + meshWidth = 320 + meshHeight = 240 + + diveMesh = DiveMesh(meshWidth, meshHeight, pyComplex, realGen, imagGen, mathSupport) + + pickleValue = pickle.dumps(diveMesh) + loadedMesh = pickle.loads(pickleValue) + + self.assertEqual(int(diveMesh.meshWidth), int(loadedMesh.meshWidth)) + self.assertEqual(int(diveMesh.meshHeight), int(loadedMesh.meshHeight)) + + self.assertEqual(float(diveMesh.center.real), float(loadedMesh.center.real)) + self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) + self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) + + #print("realGen: \"%s\"" % str(diveMesh.realMeshGenerator)) + #print("imagGen: \"%s\"" % str(diveMesh.imagMeshGenerator)) + #print("after realGen: \"%s\"" % str(loadedMesh.realMeshGenerator)) + #print("after imagGen: \"%s\"" % str(loadedMesh.imagMeshGenerator)) + + def test_flintDiveMeshPickle(self): + # Maybe better to use a bracket-arb string here? But doesn't matter? + centerWidthString = '-1.769383179195515018213' + centerHeightString = '0.00423684791873677221' + + baseRealWidthString = '5.0' + baseImagWidthString = '3.0' + + mathSupport = self.flintMathSupport + + realCenter = mathSupport.createFloat(centerWidthString) + realWidth = mathSupport.createFloat(baseRealWidthString) + realGen = MeshGeneratorUniform(mathSupport, 'width', realCenter, realWidth) + + imagCenter = mathSupport.createFloat(centerHeightString) + imagWidth = mathSupport.createFloat(baseImagWidthString) + imagGen = MeshGeneratorUniform(mathSupport, 'height', imagCenter, imagWidth) + + centerComplexString = '-1.769383179195515018213+0.00423684791873677221j' + pyComplex = mathSupport.createComplex(centerComplexString) + + meshWidth = 320 + meshHeight = 240 + + diveMesh = DiveMesh(meshWidth, meshHeight, pyComplex, realGen, imagGen, mathSupport) + + pickleValue = pickle.dumps(diveMesh) + loadedMesh = pickle.loads(pickleValue) + + self.assertEqual(int(diveMesh.meshWidth), int(loadedMesh.meshWidth)) + self.assertEqual(int(diveMesh.meshHeight), int(loadedMesh.meshHeight)) + + self.assertEqual(float(diveMesh.center.real), float(loadedMesh.center.real)) + self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) + self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) + + #print("realGen: \"%s\"" % str(diveMesh.realMeshGenerator)) + #print("imagGen: \"%s\"" % str(diveMesh.imagMeshGenerator)) + #print("after realGen: \"%s\"" % str(loadedMesh.realMeshGenerator)) + #print("after imagGen: \"%s\"" % str(loadedMesh.imagMeshGenerator)) + +if __name__ == '__main__': + unittest.main() diff --git a/test_fractalmath.py b/test_fractalmath.py index 5de6858..62ad7de 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -14,6 +14,30 @@ def setUpClass(cls): def tearDownClass(cls): cls.mathSupport = None + def test_instantiations(self): + """ + String-based number instantiations. + Short enough for all implementations. + """ + floatString = '-1.7693831791' + complexString = '-1.7693831791+0.0042368479j' + complexParenString = '(-1.7693831791+0.0042368479j)' + + complexRealString = '-1.7693831791' + complexImagString = '0.0042368479' + + firstFloat = self.mathSupport.createFloat(floatString) + self.assertIsNotNone(firstFloat) + + firstComplex = self.mathSupport.createComplex(complexString) + self.assertIsNotNone(firstComplex) + + parenComplex = self.mathSupport.createComplex(complexParenString) + self.assertIsNotNone(parenComplex) + + partwiseComplex = self.mathSupport.createComplex(complexRealString, complexImagString) + self.assertIsNotNone(partwiseComplex) + def test_scaleValue(self): """ Extra casts of returns to float() so assertAlmostEqual works """ startValue = 5.0 @@ -102,7 +126,47 @@ def setUpClass(cls): # prec value *should* be 53 to match precision. cls.mathSupport.flint.ctx.prec = 53 -class TestMathSupportFlintCustom(TestMathSupport): + def test_instantiations(self): + """ + String-based number instantiations. + Short enough for most all precisions. + """ + floatString = '-1.7693831791' + firstFloat = self.mathSupport.createFloat(floatString) + self.assertIsNotNone(firstFloat) + + complexString = '-1.7693831791+0.0042368479j' + firstComplex = self.mathSupport.createComplex(complexString) + self.assertIsNotNone(firstComplex) + + complexParenString = '(-1.7693831791+0.0042368479j)' + parenComplex = self.mathSupport.createComplex(complexParenString) + self.assertIsNotNone(parenComplex) + + complexRealString = '-1.7693831791' + complexImagString = '0.0042368479' + partwiseComplex = self.mathSupport.createComplex(complexRealString, complexImagString) + self.assertIsNotNone(partwiseComplex) + + complexFlintBracketString = '[-1.76938317910000 +/- 3.53e-16] + [0.00423684790000000 +/- 1.68e-18]j' + bracketComplex = self.mathSupport.createComplex(complexFlintBracketString) + self.assertIsNotNone(bracketComplex) + + complexFlintBracketStringNeg = '[-1.76938317910000 +/- 3.53e-16] - [0.00423684790000000 +/- 1.68e-18]j' + bracketNegativeComplex = self.mathSupport.createComplex(complexFlintBracketStringNeg) + self.assertIsNotNone(bracketNegativeComplex) + + complexFlintBracketRealOnly = '[-1.76938317910000 +/- 3.53e-16]' + bracketRealOnlyComplex = self.mathSupport.createComplex(complexFlintBracketRealOnly) + self.assertIsNotNone(bracketRealOnlyComplex) + #print("realOnly: %s" % complexFlintBracketRealOnly) + + complexFlintBracketImagOnly = '[0.00423684790000000 +/- 1.68e-18]j' + bracketImagOnlyComplex = self.mathSupport.createComplex(complexFlintBracketImagOnly) + self.assertIsNotNone(bracketImagOnlyComplex) + #print("imagOnly: %s" % complexFlintBracketImagOnly) + +class TestMathSupportFlintCustom(TestMathSupportFlint): @classmethod def setUpClass(cls): cls.mathSupport = fm.DiveMathSupportFlintCustom() diff --git a/timing_comparison.py b/timing_comparison.py index 754ca13..4ad0300 100644 --- a/timing_comparison.py +++ b/timing_comparison.py @@ -29,9 +29,9 @@ #timerRepeatCount = 1 -#centerString = '-1.7693831791+0.0042368479j' +centerString = '-1.7693831791+0.0042368479j' -centerString = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884+0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690j' +#centerString = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884+0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690j' From 0cad6ebe91f08428780f15430e0f12382b98262a Mon Sep 17 00:00:00 2001 From: clint Date: Mon, 16 Aug 2021 17:08:18 -0700 Subject: [PATCH 32/44] First attempts at building mesh_explore.py, depending on the pickling of DiveMesh, and on parameterizations into fractal.py. --- batch_build.sh | 60 +--------- fractal.py | 18 ++- fractalcache.py | 7 ++ fractalmath.py | 71 ++++++++---- mesh_explore.py | 288 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+), 83 deletions(-) create mode 100644 mesh_explore.py diff --git a/batch_build.sh b/batch_build.sh index 6767114..450a9bb 100755 --- a/batch_build.sh +++ b/batch_build.sh @@ -32,8 +32,8 @@ processCount=7 # 30 frames per batch, 4 batches, = 120 frames, going a little extra # 30 frames per process, let's say 6 processes? = 180 frames -startFrame=750 -lastNumber=778 +startFrame=0 +lastNumber=21 stride=$(((lastNumber-startFrame)/processCount)) @@ -56,62 +56,6 @@ for currPID in ${pidList[*]}; do done -#echo "Batch 4" -#date -# -#startFrame=700 -#lastNumber=800 -# -#stride=$(((lastNumber-startFrame)/processCount)) -# -##stride=45 -##lastNumber=359 -#batchCount=0 -#pidList=() -# -#until [ $startFrame -ge $lastNumber ] -#do -# echo startFrame: $startFrame & -# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& -# pidList[${batchCount}]=$! -# ((startFrame=startFrame+stride)) -# ((batchCount=batchCount+1)) -#done -# -## wait for all pids -#for currPID in ${pidList[*]}; do -# wait $currPID -# echo "Done" -#done -# -# -#echo "Batch 5" -#date -# -#startFrame=800 -#lastNumber=900 -# -#stride=$(((lastNumber-startFrame)/processCount)) -# -#batchCount=0 -#pidList=() -# -#until [ $startFrame -ge $lastNumber ] -#do -# echo startFrame: $startFrame & -# python3.9 fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-start-frame=${startFrame} --clip-frame-count=${stride}& -# pidList[${batchCount}]=$! -# ((startFrame=startFrame+stride)) -# ((batchCount=batchCount+1)) -#done -# -## wait for all pids -#for currPID in ${pidList[*]}; do -# wait $currPID -# echo "Done" -#done - - # AFTER all the processes finish python3.9 compile_video.py --dir='demo1_cache/image_frames/flint/mandelbrot_solo' --out='compiled.mp4' diff --git a/fractal.py b/fractal.py index 7c69927..c460f40 100644 --- a/fractal.py +++ b/fractal.py @@ -1100,6 +1100,7 @@ def set_demo1_params(fractal_ctx, view_ctx): center_real_str = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884' center_imag_str = '0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690' + # Shorter, for debugging # center_real_str = '-1.76938317919551501821384728608547378290574726365475143746552821652788819126475645883616344638952966730448582578182030315748749123842171940312824619511374752125508480620857874547728033032251679986623911241845427430171292144236397931692967543941816568313013426227935414237685724357839108499720568695273052075081914417347810617942906997531749111337143517341661174565202727561591789320429089324651026717908784146646282137559906504607383722834707778703064588828982026040017443489083888449628870745058537' # center_imag_str = '0.004236847918736772214926507171367997076682670917403757279459435650112344000805545157302430995023636506313532683359652571823004948055387363061275248149392923559308928343920507967248879049219866660455766269469006661034940149047143237255869797899085206566832026580640241153003788267897863946416220353410551029004563057237186845272103773258463079175126287746720056933262328069538227967558325171888734791243614309894854955011240963294216828273306935321715053674555266373827069885834569156846732024622119' @@ -1110,8 +1111,8 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) fractal_ctx.project_name = 'demo1' - #fractal_ctx.scaling_factor = .8 - fractal_ctx.scaling_factor = .5 + fractal_ctx.scaling_factor = .8 + #fractal_ctx.scaling_factor = .5 fractal_ctx.write_video = False @@ -1233,10 +1234,10 @@ def set_demo1_params(fractal_ctx, view_ctx): #fractal_ctx.max_iter = 255 #fractal_ctx.max_iter = 512 # covers ~e-34 or so #fractal_ctx.max_iter = 1024 # covers ~e-48 or so - #fractal_ctx.max_iter = 2048 + fractal_ctx.max_iter = 2048 #fractal_ctx.max_iter = 2048 * 15 # ~30k, ok at e-180? #fractal_ctx.max_iter = int(2048 * 15.5) # ~31k, ok at e-225 - fractal_ctx.max_iter = int(2048 * 16) # ~31k, ok at e-225 + #fractal_ctx.max_iter = int(2048 * 16) # ~31k, ok at e-225 #fractal_ctx.max_iter = 2048 * 20 # ~41k, ok at e-391 #fractal_ctx.max_iter = 2048 * 40 # ~82k, ok at e-497 #fractal_ctx.max_iter = 2048 * 34 # ~70k, ok at e-504 @@ -1258,20 +1259,25 @@ def set_demo1_params(fractal_ctx, view_ctx): fractal_ctx.build_cache=False # FPS still isn't set quite right, but we'll get it there eventually. - view_ctx.fps = 23.976 / 8.0 #view_ctx.fps = 23.976 / 4.0 #view_ctx.fps = 23.976 / 2.0 #view_ctx.fps = 23.976 #view_ctx.fps = 29.97 / 2.0 + #view_ctx.fps = 23.976 / 8.0 #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) #view_ctx.duration = 4.0 #view_ctx.duration = 540.0 - view_ctx.duration = 2300.0 # A bit more than EoI's dive (6682), this is ~6890 frames, if a .5 zoom factor at 23,976/8 fps. #view_ctx.duration = 1500.0 #view_ctx.duration = 30.0 #view_ctx.duration = 2.0 #view_ctx.duration = 0.25 + #view_ctx.duration = 2300.0 # A bit more than EoI's dive (6682), this is ~6890 frames, if a .5 zoom factor at 23.976/8 fps. + + + # + view_ctx.fps = 23.976 / 2.0 + view_ctx.duration = 180.0 # Just enough to explore? def set_julia_walk_demo1_params(fractal_ctx, view_ctx): print("+ Running in demo mode - loading default julia walk params") diff --git a/fractalcache.py b/fractalcache.py index 8085370..92a8f44 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -169,6 +169,9 @@ def create_image_file_name(self): def create_image_metadata_file_name(self): return u"%s.pik" % self.create_image_file_name() + def create_mesh_file_name(self): + return u"%s.mesh.pik" % self.create_image_file_name() + def write_image_to_file(self, image): self.create_image_subpath(mkdir_if_needed=True) # Just for side-effect folder creation @@ -181,6 +184,10 @@ def write_image_to_file(self, image): frame_meta.raw_histogram = self.frame_info.raw_histogram pickle.dump(frame_meta,fd) + mesh_filename = self.create_mesh_file_name() + with open(mesh_filename, 'wb') as mesh_handle: + pickle.dump(self.dive_mesh, mesh_handle) + return filename diff --git a/fractalmath.py b/fractalmath.py index 8622861..a77ca6f 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -9,6 +9,28 @@ import numpy as np +#FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 +#3.32 bits per digit, on average +#2200 was therefore, ~662 digits, got 54 frames down at .5 scaling + +#FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) + +# For debugging, looks like we're bottoming out somewhere around e-11 +# So, only really need ~20 digits for this test +# Blowing out frame 30, which is ~28? +#FLINT_HIGH_PRECISION_SIZE = int(50 * 3.32) +# Native blew out somewhere e-13 to e-15 +#FLINT_HIGH_PRECISION_SIZE = int(200 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(60 * 3.32) +#FLINT_HIGH_PRECISION_SIZE = int(30 * 3.32) +FLINT_HIGH_PRECISION_SIZE = int(16 * 3.32) + class DiveMathSupport: """ Toolbox for math functions that need to be type-aware, so we @@ -54,6 +76,13 @@ def createComplex(self, *args): def createFloat(self, floatValue): return float(floatValue) + def stringFromFloat(self, paramFloat): + return str(paramFloat) + + def shorterStringFromFloat(self, paramFloat, places): + # I guess for native, just ignore the truncation request? + return self.stringFromFloat(paramFloat) + def floor(self, value): return math.floor(value) @@ -448,25 +477,7 @@ def __init__(self): self.flint = __import__('flint') # Only imports if you instantiate this DiveMathSupport subclass. - #FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 - #3.32 bits per digit, on average - #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling - - #FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol - #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol - #FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol - #FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) - #FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) - #FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) - - # For debugging, looks like we're bottoming out somewhere around e-11 - # So, only really need ~20 digits for this test - # Blowing out frame 30, which is ~28? - #FLINT_HIGH_PRECISION_SIZE = int(50 * 3.32) - # Native blew out somewhere e-13 to e-15 - #FLINT_HIGH_PRECISION_SIZE = int(200 * 3.32) - - self.defaultPrecisionSize = int(500 * 3.32) + self.defaultPrecisionSize = FLINT_HIGH_PRECISION_SIZE self.flint.ctx.prec = self.defaultPrecisionSize # Sets flint's precision (in bits) self.precisionType = 'flint' @@ -620,6 +631,9 @@ def separateOrdinaryComplexString(self, partsString): else: raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()" % args[0]) + preparedReal = "" + preparedImag = "" + if realPart != "": if realIsPositive == True: preparedReal = realPart @@ -637,6 +651,14 @@ def separateOrdinaryComplexString(self, partsString): def createFloat(self, floatValue): return self.flint.arb(floatValue) + def stringFromFloat(self, paramFloat): + # Trying to clear out the error ball?! + return self.flint.arb(paramFloat.mid(), 0).str() + + def shorterStringFromFloat(self, paramFloat, places): + # Trying to clear out the error ball?! + return self.flint.arb(paramFloat.mid(), 0).str(places) + def floor(self, value): return self.flint.arb(value).floor() @@ -652,7 +674,10 @@ def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterati """ zoomAsArb = self.flint.arb(overallZoomFactor) iterationsAsArb = self.flint.arb(iterations) + #calculatedValue = startValue * pow(zoomAsArb, iterationsAsArb) + #return self.flint.arb(calculatedValue.mid(), 0) # Trying to clear out the error ball?! return startValue * pow(zoomAsArb, iterationsAsArb) + def interpolateLogTo(self, startX, startY, endX, endY, targetX): @@ -727,6 +752,8 @@ def mandelbrot(self, c, escapeRadius, maxIter): Could just call julia with a zero start here, but seems wiser to not have one extra function call in the core of the calculation? + (In retrospect, the extra call isn't worth tabulating at higher + iteration counts) """ z = self.flint.acb(0,0) n = 0 @@ -738,6 +765,12 @@ def mandelbrot(self, c, escapeRadius, maxIter): break z = z*z + c + + # Forcibly clearing the error component of the arb. + # Custom flint implementation could be more graceful with this. + zRealNoErr = self.flint.arb(z.real.mid(), 0) + zImagNoErr = self.flint.arb(z.imag.mid(), 0) + z = self.flint.acb(zRealNoErr, zImagNoErr) return (n, z) diff --git a/mesh_explore.py b/mesh_explore.py new file mode 100644 index 0000000..969b7b0 --- /dev/null +++ b/mesh_explore.py @@ -0,0 +1,288 @@ +# -- +# File: mesh_explore.py +# +# Viewer to explore results generated by fractal.py. +# Command-line (and --demo) parameters set how the image is generated. +# +# This means that when fractal.py runs out of frames, it might be +# able to render, that there will be an error in this exploration. +# If you run out of frames, then adjust the value in fractal.py for +# 'duration'. This might happen after adjusting 'fps' as well, except +# that changing 'fps' will ALSO change the frame number that a +# feature appears at. +# +# In addition, to achieve higher depths, you need to adjust 2 values +# as you explore: +# 1.) max_iter (e.g. line ~1550 in fractal.py sets for --demo) +# 2.) FLINT_HIGH_PRECISION_SIZE (e.g. line ~31 in fractalmath.py) +# +# If the image goes all white, it's likely you need higher iteration count. +# If the image goes splotchy, it's likely you need higher precision. +# If the image goes pixellated, then you definitely need higher precision. +# +# -- + +import getopt +import os +import pickle + +import subprocess + +from divemesh import * +from fractal import * +from fractalmath import * + +import matplotlib as mpl +from matplotlib import pyplot as plt + +def parse_options(): + params = {} + + argv = sys.argv[1:] + opts, args = getopt.getopt(argv, "f:c:", + ["frame=", + "center=", + ]) + + for opt, arg in opts: + if opt in ['f', '--frame']: + params['frameNumber'] = int(arg) + elif opt in ['-c', '--center']: + params['centerString'] = arg + + # Eventually, we should just be able to point to a project file + # (as a pickle), and this ui script will read that, and not have + # to create all its own commands, paths, and parameters, though + # we should probably build our own 'exploration' cache separately. + # + #### Custom flint, just mandelbrot + params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-frame-count=1' + params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/mandelbrot_solo" + # + #### Standard Flint, just mandelbrot + #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-frame-count=1' + #params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/mandelbrot_solo" + # + #### Custom Flint, smooth + #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=smooth --demo --burn --flintcustom --clip-frame-count=1' + #params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/smooth" + # + #### Native calc, smooth + # NOTE: When using 'native', complex should be formatted with + # parens like "(0.1+0.04j)" + #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=smooth --demo --burn --clip-frame-count=1' + #params['wholeCacheFolder'] = "demo1_cache/image_frames/native/smooth" + # + #### Native calc, just mandelbrot + #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --clip-frame-count=1' + #params['wholeCacheFolder'] = "demo1_cache/image_frames/native/mandelbrot_solo" + + params['frameIncrement'] = 1 + return params + +def nextClicked(event): + global params + origFrameNumber = params.get('frameNumber', 0) + frameNumber = origFrameNumber + params.get('frameIncrement', 1) + params['frameNumber'] = frameNumber + + updateForFrameNumber(frameNumber) + +def prevClicked(event): + global params + origFrameNumber = params.get('frameNumber', 0) + frameNumber = origFrameNumber - params.get('frameIncrement', 1) + if frameNumber < 0: + frameNumber = 0 + params['frameNumber'] = frameNumber + + updateForFrameNumber(frameNumber) + +def updateForFrameNumber(frameNumber): + global uiElements + clickedLocus = uiElements.get('lastClickedLocus', None) + if clickedLocus == None: + return + + runFractalCall(frameNumber, str(clickedLocus)) + + imageFileTitle = "%d.tiff" % frameNumber + imageFileName = os.path.join(params['wholeCacheFolder'], imageFileTitle) + imageData = mpl.image.imread(imageFileName) + + clickableImage = uiElements['clickableImage'] + clickableImage.set_data(imageData) + + meshFileTitle = "%s.mesh.pik" % imageFileTitle + meshFileName = os.path.join(params['wholeCacheFolder'], meshFileTitle) + with open(meshFileName, 'rb') as meshHandle: + diveMesh = pickle.load(meshHandle) + + uiElements['diveMesh'] = diveMesh + + updateTitle() + plt.draw() + +def runFractalCall(frameNumber, centerString): + global params + fractalCallString = "%s --clip-start-frame=%d" % (params['fractalCommandBase'], frameNumber) + if centerString != None: + fractalCallString += " --center='%s'" % (centerString) + + print("Calling: %s" % fractalCallString) + subprocess.call([fractalCallString], shell=True) + print("(Call finished.)") + print("Exploration invocation for this point:") + print("python3.9 ./mesh_explore.py --frame=%d --center='%s'" % (frameNumber, centerString)) + print("") + +def plusClicked(event): + global params + origFrameIncrement = params.get('frameIncrement', 1) + frameIncrement = origFrameIncrement + 1 + params['frameIncrement'] = int(frameIncrement) + + updateAdvanceText() + plt.draw() + +def minusClicked(event): + global params + origFrameIncrement = params.get('frameIncrement', 1) + frameIncrement = origFrameIncrement + if frameIncrement != 1: + frameIncrement -= 1 + params['frameIncrement'] = int(frameIncrement) + + updateAdvanceText() + plt.draw() + +def updateAdvanceText(): + global uiElements + advanceText = uiElements['advanceText'] + + global params + frameIncrement = params.get('frameIncrement', 1) + if frameIncrement == 1: + newText = "+ 1 frame" + else: + newText = "+ %d frames" % frameIncrement + + advanceText.set_text(newText) + +def updateTitle(): + global uiElements + diveMesh = uiElements['diveMesh'] + + widthString = diveMesh.mathSupport.shorterStringFromFloat(diveMesh.imagMeshGenerator.baseWidth, 10) + plt.suptitle("%s wide" % widthString) + +def onclick(event): + global uiElements + + # event.xdata and event.ydata are floats, but we want pixel ints + #print(event) + if event.xdata is None or event.ydata is None: + return + + # Only clicks in the image can change the focus. + clickableImage = uiElements['clickableImage'] + if event.inaxes != clickableImage.axes: + return + + clickX = int(event.xdata) + clickY = int(event.ydata) + + diveMesh = uiElements['diveMesh'] + meshData = diveMesh.generateMesh() + #print("click (%s,%s)" % (str(clickX), str(clickY))) + clickedLocus = meshData[clickY, clickX] + # Extra steps to try to clear out the 'error' radius from arb. + clickedRealString = diveMesh.mathSupport.stringFromFloat(clickedLocus.real) + clickedImagString = diveMesh.mathSupport.stringFromFloat(clickedLocus.imag) + rebuiltClickedLocus = diveMesh.mathSupport.createComplex(clickedRealString, clickedImagString) + print(rebuiltClickedLocus) + uiElements['lastClickedLocus'] = rebuiltClickedLocus + +if __name__ == '__main__': + + global params + params = parse_options() + frameNumber = params.get('frameNumber', 0) + + centerString = params.get('centerString', None) + + # Run the whole frame on first call, so we can read the + # mesh dimensions from the result. Otherwise, we have to + # guess at a lot of things like image size. + runFractalCall(frameNumber, centerString) + + global uiElements + uiElements = {} + + # Start off with the parameterized locus as the center. + uiElements['lastClickedLocus'] = centerString + + imageFileTitle = "%d.tiff" % frameNumber + meshFileTitle = "%s.mesh.pik" % imageFileTitle + meshFileName = os.path.join(params['wholeCacheFolder'], meshFileTitle) + + with open(meshFileName, 'rb') as meshHandle: + diveMesh = pickle.load(meshHandle) + + uiElements['diveMesh'] = diveMesh + + mainFigure = plt.figure() + uiElements['mainFigure'] = mainFigure + + imageFileName = os.path.join(params['wholeCacheFolder'], imageFileTitle) + imageData = mpl.image.imread(imageFileName) + uiElements['clickableImage'] = plt.imshow(imageData) + + mainFigure.canvas.mpl_connect('button_press_event', onclick) + + buttonWidth = 0.1 + buttonHeight = 0.06 + + gutter = 0.01 + positionX = gutter + positionY = gutter + + button1Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + prevButton = mpl.widgets.Button(button1Axes, label="Prev") + prevButton.on_clicked(prevClicked) + uiElements['previousButton'] = prevButton + + positionX += buttonWidth + gutter + + button2Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + nextButton = mpl.widgets.Button(button2Axes, label="Next") + nextButton.on_clicked(nextClicked) + uiElements['nextButton'] = nextButton + + positionX += buttonWidth + gutter + + smallerButtonWidth = .04 + + button3Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) + plusButton = mpl.widgets.Button(button3Axes, label="+") + plusButton.on_clicked(plusClicked) + uiElements['plusButton'] = plusButton + + positionX += smallerButtonWidth + gutter + + button4Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) + minusButton = mpl.widgets.Button(button4Axes, label="-") + minusButton.on_clicked(minusClicked) + uiElements['minusButton'] = minusButton + + positionX += 0.8 # BUG: Not sure why adding another button width is wrong here + #positionX += smallerButtonWidth + gutter + + advanceText = plt.text(positionX, positionY, "+ frame", horizontalalignment='left') + uiElements['advanceText'] = advanceText + updateAdvanceText() + + updateTitle() + + plt.show() + From 129a5c9c69832ee8d6c046c8f37b3408bb6c93da Mon Sep 17 00:00:00 2001 From: clint Date: Tue, 17 Aug 2021 16:46:22 -0700 Subject: [PATCH 33/44] Adding thoughts about dive construction and use cases --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index 16c6b23..e373f82 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,60 @@ # mandl + +# Constructing A Dive + +## Exploration + +Establish at least two frames to dive between. + - real and imaginary starting widths + (real -> horiz, imag -> vert, or was it the other way?) + - real and imaginary ending widths + (second width is redundant if aspect ratio is constant) + - single center point, used as middle of both start and end frames + +While exploring, write out points of interest, creating a waypoint list. +The first and last waypoints are the default endpoints for an +editing project. + +## Editing + +Need at least 2 waypoints to establish the 'outer' parameters for +a dive animation. + +In terms of an epoch-based definition, these are the starting widths +and the appropriate combination of zoom factor, framerate, +and duration, that achieve the window widths at the start and the end. + +In terms of mesh exploration, these are two meshes that define the starting +and ending widths, and either a framerate or a duration. + +## Statistics + +For an edit, would like to have statistics gathered across the frames +that show: + - How many pixels are which values (counts and/or hists) + - Some kinds of entropy estimates for each frame + - Pattern matching results, against a library of shapes + +## Sync + +Timing of color palette and drawing algorithm type should be flexible +enough that we can sync it to a music track, or to our defined waypoints. + +Sound generations from a sequence might require timing adjustments for +the animation, to make a rhythm or a tone adjustment more consistent. +e.g. if we have a good start of a beat, but the 4th bar is messy, we could +rush or delay that phrase to make it fit more consistently. + +Sound sync will probably require a similar kind of adjustment, except the +satisfaction is backwards, where we take a rhythm or a tone definition, +and bend the temporal locations that we've targeted as waypoints to +line up with the beat or tone. + + + +# Notes on Architecture and Classes + FractalContext - Keeps and manages overall parameters. - Instantiates algorithm and precision-aware subclasses. From fe47f61f97d0b63a9f4817db66f086d302be58b4 Mon Sep 17 00:00:00 2001 From: clint Date: Mon, 23 Aug 2021 00:12:01 -0700 Subject: [PATCH 34/44] Large restructuring, only --exploration rendering working at the moment --- algo.py | 197 +++++++------ divemesh.py | 19 +- fractal.py | 644 ++++++++++++++++++++++++++++++------------- fractalmath.py | 3 + fractalpalette.py | 6 +- julia.py | 4 +- mandelbrot.py | 178 ------------ mandelbrot_smooth.py | 105 +++++++ mandelbrot_solo.py | 68 ++--- mandeldistance.py | 118 +++----- mesh_explore.py | 9 + smooth.py | 4 +- test_divemesh.py | 51 ++-- 13 files changed, 779 insertions(+), 627 deletions(-) delete mode 100644 mandelbrot.py create mode 100644 mandelbrot_smooth.py diff --git a/algo.py b/algo.py index 672371d..6e4b315 100644 --- a/algo.py +++ b/algo.py @@ -1,4 +1,8 @@ + +import os +import pickle + import fractalcache as fc import fractalpalette as fp import divemesh as mesh @@ -16,109 +20,132 @@ class Algo(object): This means the algo is responsible for generating and caching its own intermediate results. - Frame generation happens in two phases, with intermediate hooks available. - - beginning hook - - generate results - - pre-image hook - - generate image - - ending hook + The sequence in run() is the core of the processing architecture. + + Trying something out: presuming that Algos will all write out + intermediate files, without extra parameters to turn that behavior + off. This is only really visible in this base class in mesh_setup(), + in which you can't turn off the mesh pickle file writing. + + In general, because this is a customizable sequence processor, + subclasses should NOT call superclass implementations, because they're + probably providing more specific behavior. """ @staticmethod - def parse_options(opts): + def options_list(): """ Algorithm implementations can fill a dictionary with key/value pairs to pass algorithm-specific parameters from the command line into the per-frame algorithm instantiation + + To do this, the list of available options are returned here, and + then iteration over the getopts values is done in parse_options() """ + return [] + + @staticmethod + def parse_options(opts): return {} - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): self.algorithm_name = None self.dive_mesh = dive_mesh self.frame_number = frame_number + self.output_folder_name = output_folder_name - self.project_folder_name = project_folder_name - self.shared_cache_path = shared_cache_path - - self.build_cache = build_cache - self.invalidate_cache = invalidate_cache - - self.cache_frame = None + # Algo should fill in these values + self.mesh_array = None + self.counts_array = None + self.last_values_array = None + self.processed_array = None + self.output_image_file_name = None - def build_cache_frame(self): - raise NotImplementedError('Subclass must implement build_cache_frame()') - - def get_frame_metadata(self): + def get_metadata(self): return {'frame_number' : self.frame_number, 'fractal_type': self.algorithm_name, 'precision_type': self.dive_mesh.mathSupport.precisionType, - 'mesh_center': str(self.dive_mesh.center), + 'mesh_center': str(self.dive_mesh.getCenter()), 'complex_real_width' : str(self.dive_mesh.realMeshGenerator.baseWidth), - 'complex_imag_width' : str(self.dive_mesh.imagMeshGenerator.baseWidth), + 'complex_imag_width' : str(self.dive_mesh.imagMeshGenerator.baseWidth), 'mesh_is_uniform' : str(self.dive_mesh.isUniform())} + def run(self): + """ + Frame generation happens in phases, with intermediate hooks available. + """ + self.beginning_hook() + self.mesh_setup() + self.generate_counts() + self.pre_process_hook() + self.process_counts() + self.pre_image_hook() + self.generate_image() + self.ending_hook() + return self.output_image_file_name + def beginning_hook(self): pass - def generate_results(self): - """ - Load or calculate the frame data. - - Probably need to move this to a hierarchical subclass - eventually, but for now, the cache objects hold up to - 2 arrays and 2 histograms, so it can be used for all - our Algos. + def mesh_setup(self): + """ + Might not be important to split mesh array creation into + its owns step, but it does add flexibility to Algo subclasses + to handle this differently, or maybe to not save results out + to file. """ - self.cache_frame = self.build_cache_frame() - - if self.invalidate_cache == True: - self.cache_frame.remove_from_results_cache() + self.mesh_array = self.dive_mesh.generateMesh() - self.cache_frame.read_results_cache() + mesh_base_name = u"%d.mesh.pik" % self.frame_number + mesh_file_name = os.path.join(self.output_folder_name, mesh_base_name) + with open(mesh_file_name, 'wb') as mesh_handle: + pickle.dump(self.dive_mesh, mesh_handle) - if not self.cache_frame.frame_info.data_is_loaded(): - self.calculate_results() + def generate_counts(self): + """ Business end of getting results for a mesh """ + raise NotImplementedError('Subclass must implement generate_counts()') - # Fresly calculated results get saved if we're building the cache - if self.build_cache == True: - self.cache_frame.write_results_cache() + def pre_process_hook(self): + pass - return + def process_counts(self): + """ + The main thing process_counts() does is make sure that + 'processed_array' is loaded with information for generate_image() + to run with. - def calculate_results(self): - """ Business end of getting results for a mesh """ - raise NotImplementedError('Subclass must implement calculate_results()') + This base-class implementation just points one to the other. + """ + self.processed_array = self.counts_array def pre_image_hook(self): pass def generate_image(self): - pass - - def write_image_to_file(self, image): - return self.cache_frame.write_image_to_file(image) + """ + Apart from actually generating an image, another important thing + that generate_image() does is sets output_image_file_name to the + appropriate file name that we generated. + """ + raise NotImplementedError('Subclass must implement generate_image()') def ending_hook(self): pass -class EscapeFrameInfo(fc.FrameInfo): - def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): - super().__init__(mesh_width, mesh_height, center, complex_real_width, complex_imag_width, raw_values, raw_histogram, smooth_values, smooth_histogram) - - self.escape_r = escape_r - self.max_escape_iter = max_escape_iter - - def empty_copy(self): - """ Looks like storing strings of everything makes us pickle-proof? """ - return EscapeFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter)) +class EscapeAlgo(Algo): - def pickle_copy(self): - return EscapeFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + @staticmethod + def options_list(): + whole_list = Algo.options_list() + whole_list.extend(["escape-radius=", + "max-escape-iterations=", + "burn", + "palette=", + ]) -class EscapeAlgo(Algo): + return whole_list @staticmethod def parse_options(opts): @@ -129,8 +156,6 @@ def parse_options(opts): options['escape_radius'] = float(arg) elif opt in ['--max-escape-iterations']: options['max_escape_iterations'] = int(arg) - elif opt in ['--smooth']: - options['use_smoothing'] = True elif opt in ['--burn']: options['burn_in'] = True elif opt in ['--palette']: @@ -138,26 +163,15 @@ def parse_options(opts): return options - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) # Load, with optional default values self.escape_radius = extra_params.get('escape_radius', 2.0) self.max_escape_iterations = extra_params.get('max_escape_iterations', 255) - self.use_smoothing = extra_params.get('use_smoothing', False) self.burn_in = extra_params.get('burn_in', False) self.palette = extra_params.get('palette', fp.FractalPalette()) - def build_cache_frame(self): - frame_info = EscapeFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations) - return fc.Frame(project_folder_name=self.project_folder_name, shared_cache_path=self.shared_cache_path, algorithm_name=self.algorithm_name, dive_mesh=self.dive_mesh, frame_info=frame_info, frame_number=self.frame_number) - - def get_frame_metadata(self): - metadata = super().get_frame_metadata() - metadata['escape_radius'] = self.escape_radius - metadata['max_escape_iterations'] = self.max_escape_iterations - return metadata - def burn_text_to_drawing(self, burn_in_text, drawing): burn_in_location = (10,10) burn_in_margin = 5 @@ -166,20 +180,15 @@ def burn_text_to_drawing(self, burn_in_text, drawing): drawing.rectangle(((burn_in_location[0] - burn_in_margin, burn_in_location[1] - burn_in_margin), (burn_in_size[0] + burn_in_margin * 2, burn_in_size[1] + burn_in_margin * 2)), fill="black") drawing.text((burn_in_location[0]-2, burn_in_location[1] - 2), burn_in_text, 'white', burn_in_font) -class JuliaFrameInfo(EscapeFrameInfo): - def __init__(self, mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, julia_center, raw_values=None, raw_histogram=None, smooth_values=None, smooth_histogram=None): - super().__init__(mesh_width, mesh_height, center, complex_real_width, complex_imag_width, escape_r, max_escape_iter, raw_values, raw_histogram, smooth_values, smooth_histogram) - - self.julia_center = julia_center +class JuliaAlgo(EscapeAlgo): - def empty_copy(self): - """ Looks like storing strings of everything makes us pickle-proof? """ - return JuliaFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), str(self.julia_center)) + @staticmethod + def options_list(): + whole_list = EscapeAlgo.options_list() - def pickle_copy(self): - return JuliaFrameInfo(str(self.mesh_width), str(self.mesh_height), str(self.center), str(self.complex_real_width), str(self.complex_imag_width), str(self.escape_r), str(self.max_escape_iter), str(self.julia_center), self.raw_values, self.raw_histogram, self.smooth_values, self.smooth_histogram) + whole_list.extend(["julia-center="]) -class JuliaAlgo(EscapeAlgo): + return whole_list @staticmethod def parse_options(opts): @@ -193,24 +202,12 @@ def parse_options(opts): return options - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) #self.julia_center = extra_params.get('julia_center', self.dive_mesh.mathSupport.createComplex(0,0) self.julia_center = extra_params.get('julia_center', self.dive_mesh.mathSupport.createComplex(-.8,.145)) #print("settled on %s" % self.julia_center) - def build_cache_frame(self): - frame_info = JuliaFrameInfo(self.dive_mesh.meshWidth, self.dive_mesh.meshHeight, self.dive_mesh.center, self.dive_mesh.realMeshGenerator.baseWidth, self.dive_mesh.imagMeshGenerator.baseWidth, self.escape_radius, self.max_escape_iterations, self.julia_center) - return fc.Frame(project_folder_name=self.project_folder_name, shared_cache_path=self.shared_cache_path, algorithm_name=self.algorithm_name, dive_mesh=self.dive_mesh, frame_info=frame_info, frame_number=self.frame_number) - - def get_frame_metadata(self): - metadata = super().get_frame_metadata() - metadata['julia_center'] = self.julia_center - return metadata - - def beginning_hook(self): - pass - diff --git a/divemesh.py b/divemesh.py index cef6630..eab6563 100644 --- a/divemesh.py +++ b/divemesh.py @@ -34,13 +34,12 @@ class DiveMesh: # Then, the separate 2D meshes are combined to become the overall mesh values. # Finally, overall distortions are applied to the overall mesh values. """ - def __init__(self, width, height, center, realMeshGenerator, imagMeshGenerator, mathSupport, extraParams={}): + def __init__(self, width, height, realMeshGenerator, imagMeshGenerator, mathSupport, extraParams={}): # Trying not to apply castings to these types, to keep them the same as # the original parameters, which could make swapping out different # precision libraries simpler? self.meshWidth = width self.meshHeight = height - self.center = center self.realMeshGenerator = realMeshGenerator self.imagMeshGenerator = imagMeshGenerator @@ -67,7 +66,6 @@ def __getstate__(self): #pickleInfo['meshWidth'] = str(pickleInfo['meshWidth']) #pickleInfo['meshHeight'] = str(pickleInfo['meshHeight']) - pickleInfo['center'] = str(pickleInfo['center']) # Going to encode both the class name of the MathSupport, and # the 'precision' it was apparently set at, making a string @@ -99,10 +97,12 @@ def __setstate__(self, state): state['mathSupport'].setPrecision(int(precisionString)) #print("mathSupport is: %s" % str(state['mathSupport'])) - state['center'] = state['mathSupport'].createComplex(state['center']) - self.__dict__.update(state) + def getCenter(self): + """ Pretty common to want the mesh center, so assemble it from the generators. """ + return self.mathSupport.createComplex(self.realMeshGenerator.valuesCenter, self.imagMeshGenerator.valuesCenter) + def generateMesh(self): realMesh = self.realMeshGenerator.generateForDiveMesh(self) imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) @@ -115,9 +115,10 @@ def generateMesh(self): # The native python 'complex' type assigns into "object" type arrays without problems, # but not vice-versa, so use object type for everything. - for x in range(0, meshShape[0]): - for y in range(0, meshShape[1]): - combinedMesh[x,y] = self.mathSupport.createComplex(realMesh[x,y], imagMesh[x,y]) + # numpyArray.shape returns (rows, columns) + for y in range(0, meshShape[0]): + for x in range(0, meshShape[1]): + combinedMesh[y,x] = self.mathSupport.createComplex(realMesh[y,x], imagMesh[y,x]) return combinedMesh @@ -234,7 +235,7 @@ def generateForDiveMesh(self, diveMesh): def __repr__(self): return """\ -[MeshGeneratorUniform center:{vCenter} baseWidth:{vWidth} along axis:{vAxis}]\ +[MeshGeneratorUniform valuesCenter:{vCenter} baseWidth:{vWidth} along axis:{vAxis}]\ """.format(vCenter=self.valuesCenter, vWidth=self.baseWidth, vAxis=self.varyingAxis) # Not implemented yet: diff --git a/fractal.py b/fractal.py index c460f40..57ed30c 100644 --- a/fractal.py +++ b/fractal.py @@ -26,6 +26,7 @@ import os import pickle +import json # For params file reading and writing import multiprocessing # Can't actually make this work yet - gonna need pickling? @@ -48,8 +49,8 @@ from algo import Algo # Abstract base class import, because we rely on it. from julia import Julia -from mandelbrot import Mandelbrot from mandelbrot_solo import MandelbrotSolo +from mandelbrot_smooth import MandelbrotSmooth from mandeldistance import MandelDistance from smooth import Smooth @@ -98,8 +99,8 @@ def __init__(self, math_support=fm.DiveMathSupport()): self.invalidate_cache = False self.algorithm_map = {'julia' : Julia, - 'mandelbrot' : Mandelbrot, 'mandelbrot_solo' : MandelbrotSolo, + 'mandelbrot_smooth' : MandelbrotSmooth, 'mandeldistance' : MandelDistance, 'smooth': Smooth} self.algorithm_name = None @@ -128,21 +129,10 @@ def render_frame_number(self, frame_number): #print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) #print(".") # Just to keep the time-per-frame calculations from being overwritten in the terminal - frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh=dive_mesh, frame_number=display_frame_number, project_folder_name=self.project_name, shared_cache_path=self.shared_cache_path, build_cache=self.build_cache, invalidate_cache=self.invalidate_cache, extra_params=extra_params) + frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh=dive_mesh, frame_number=display_frame_number, output_folder_name=self.project_name, extra_params=extra_params) - frame_algorithm.beginning_hook() - - frame_algorithm.generate_results() - - frame_algorithm.pre_image_hook() - - frame_image = frame_algorithm.generate_image() - - image_file_name = frame_algorithm.write_image_to_file(frame_image) - - frame_algorithm.ending_hook() - - return image_file_name + frame_algorithm.run() + return frame_algorithm.output_image_file_name def __repr__(self): return """\ @@ -358,13 +348,23 @@ class DiveTimeline: There are currently only 3 'tracks' of keyframes (for complex center, window base sizes, and perspective). Keyframes currently all live on integer frame numbers. + + # TODO: Seems like algorithm should be a per-span property, instead of + # per-timeline, doesn't it? """ - def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, frameHeight, mathSupport, sharedCachePath): + @staticmethod + def algorithm_map(): + return {'julia' : Julia, + 'mandelbrot_solo' : MandelbrotSolo, + 'mandelbrot_smooth' : MandelbrotSmooth, + 'mandeldistance' : MandelDistance, + 'smooth': Smooth, + } + + def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, frameHeight, mathSupport): self.projectFolderName = projectFolderName - self.sharedCachePath = sharedCachePath - self.algorithm_name = algorithm_name self.framerate = float(framerate) @@ -500,7 +500,7 @@ def getMeshForFrame(self, frameNumber): for (leftKeyframe, rightKeyframe) in currKeyframePairs: extraFrameParams[currParamName] = self.interpolateFloatBetweenParameterKeyframes(localFrameNumber, leftKeyframe, rightKeyframe) - diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, meshCenterValue, realMeshGenerator, imagMeshGenerator, self.mathSupport, extraFrameParams) + diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, realMeshGenerator, imagMeshGenerator, self.mathSupport, extraFrameParams) #print (diveMesh) return diveMesh @@ -1359,140 +1359,328 @@ def set_snapshot_mode(fractal_ctx, view_ctx, snapshot_filename='snapshot.gif'): view_ctx.fps = 0 -def parse_options(fractal_ctx, view_ctx): - argv = sys.argv[1:] - - opts, args = getopt.getopt(argv, "pd:m:s:f:w:h:c:a:", - ["preview", - "algo=", - "flint", - "flintcustom", - "gmp", - "demo", - "demo-julia-walk", - "duration=", - "fps=", - "clip-start-frame=", - "clip-frame-count=", - "project-name=", - "image-frame-path=", - "shared-cache-path=", - "build-cache", - "invalidate-cache", - "banner", - "max-iter=", - "img-w=", - "img-h=", - "cmplx-w=", - "cmplx-h=", - "center=", - "scaling-factor=", - "snapshot=", - "gif=", - "mpeg=", - "verbose=", - "palette-test=", - #"color=", - "color", - "julia-center=", # Julia - "julia-list=", # Julia - "burn", # Hopefully all algorithms? - "escape_radius", # Mandelbrot, Julia - "max_escape_iterations", # Mandelbrot, Julia - "smooth", # Mandelbrot, Julia - ]) - - - # First-pass parameters handled so others can be responsive - # - Math support, so instantiations are properly typed - # - Algorithm name, so additional parameters can be read - for opt, arg in opts: - if opt in ['--gmp']: - fractal_ctx.math_support = fm.DiveMathSupportGmp() - elif opt in ['--flint']: - fractal_ctx.math_support = fm.DiveMathSupportFlint() - elif opt in ['--flintcustom']: - fractal_ctx.math_support = fm.DiveMathSupportFlintCustom() - elif opt in ['-a', '--algo']: - if str(arg) in fractal_ctx.algorithm_map: - fractal_ctx.algorithm_name = str(arg) - - if fractal_ctx.algorithm_name is None: - fractal_ctx.algorithm_name = 'mandelbrot' - - # Kinda a crazy invocation. Loads algorithm-specific parameters into - # a dictionary, based on that algorithm's static class parse function. - fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] = fractal_ctx.algorithm_map[fractal_ctx.algorithm_name].parse_options(opts) - # Theoretically possible we'll eventually want to run this for all - # possible algorithm types, but for now, just loading for the - # 'active' algorithm. - - for opt,arg in opts: - if opt in ['-p', '--preview']: - set_preview_mode(fractal_ctx, view_ctx) - elif opt in ['-s', '--snapshot']: - set_snapshot_mode(fractal_ctx, view_ctx, arg) - elif opt in ['--demo']: - set_demo1_params(fractal_ctx, view_ctx) - elif opt in ['--demo-julia-walk']: - set_julia_walk_demo1_params(fractal_ctx, view_ctx) - - palette = fp.FractalPalette() # Will get stashed for the algorithm to use +def make_project(params): + """ + Sets up a project directory structure, as long as the provided + name is legal enough and not already taken. + + It might be more pure to only create subdirs as they're needed + and used, but it's actually helpful to have a pre-defined structure + in place to rely on, even if the paths end up shifting around later + because of changes to settings. + """ + folder_name = params['make_project_name'] + print("Making project: \"%s\"" % folder_name) + if os.path.exists(folder_name): + print("NOT CREATING project:") + print(" File already exists where fractal project would be created.") + exit(0) + + project_params = { + "math_support": params['math_support'].precisionType, + "exploration_mesh_width": "160", + "exploration_mesh_height": "120", + "exploration_default_algo_name": "mandelbrot_solo", + "exploration_default_zoom_factor": "0.8", + "exploration_output_path": "exploration/output", + "exploration_markers_path": "exploration/markers", + "edit_markers_path": "edit/markers", + "edit_timelines_path": "edit/timelines", + "render_mesh_width": "1024", + "render_mesh_height": "768", + "render_fps": "23.976", + "render_output_path": "output", + "render_exports_path": "exports", + } + + subfolder_names = [project_params['exploration_output_path'], + project_params['exploration_markers_path'], + project_params['edit_markers_path'], + project_params['edit_timelines_path'], + project_params['render_output_path'], + project_params['render_exports_path'], + ] + + for curr_subfolder_name in subfolder_names: + os.makedirs(os.path.join(folder_name, curr_subfolder_name)) + + param_file_name = os.path.join(folder_name, 'params.json') + with open(param_file_name, 'wt') as param_handle: + param_handle.write(json.dumps(project_params, indent=4)) + param_handle.close() + + print("Project folder created at \"%s\"" % folder_name) + +def parse_options(): + """ + Handles parsing of command line parameters, including those specified + in Algo implementations. + + For all modes except "--make-project", also load the values + from params.json into params['project_params'] + """ +#def parse_options(fractal_ctx, view_ctx): + # argv = sys.argv[1:] + # opts, args = getopt.getopt(argv, "pd:m:s:f:w:h:c:a:", + # ["preview", + # "algo=", + # "flint", + # "flintcustom", + # "gmp", + # "demo", + # "demo-julia-walk", + # "duration=", + # "fps=", + # "clip-start-frame=", + # "clip-frame-count=", + # "project-name=", + # "image-frame-path=", + # "shared-cache-path=", + # "build-cache", + # "invalidate-cache", + # "banner", + # "max-iter=", + # "img-w=", + # "img-h=", + # "cmplx-w=", + # "cmplx-h=", + # "center=", + # "scaling-factor=", + # "snapshot=", + # "gif=", + # "mpeg=", + # "verbose=", + # "palette-test=", + # #"color=", + # "color", + # "julia-center=", # Julia + # "julia-list=", # Julia + # "burn", # Hopefully all algorithms? + # "escape_radius", # Mandelbrot, Julia + # "max_escape_iterations", # Mandelbrot, Julia + # "smooth", # Mandelbrot, Julia + # "make-project=", + ## "project=", + # "math-support=", + # ]) + + params = {} # Most everything here fills this dictionary + + options_list = ["math-support=", + "digits-precision=", + "project=", + # Mode params + "make-project=", + "exploration", + "timeline-name=", + # Exploration-only params + "expl-algo=", + "expl-real-width=", + "expl-imag-width=", + "expl-center=", + "expl-frame-number=", + # Timeline-only params + "batch-frame-numbers=", + ] + + # Add *all* the command line options that Algos recognize, even though + # I'd rather only load up for the active Algo. There's not a really + # clean way to first load the Algo, and second, trim out the algo-specific + # options, in a way that keeps getopt working simply. + algorithm_map = DiveTimeline.algorithm_map() + algorithm_extra_params = {} + for algorithm_name, algorithm_class in algorithm_map.items(): + options_list.extend(algorithm_class.options_list()) + # Make param list unique, just for readability + options_list = list(set(options_list)) + + opts, args = getopt.getopt(sys.argv[1:], "", options_list) + + # First-pass at params to set up MathSupport because lots of + # things depend on it for names and types. + math_support_classes = {'native': fm.DiveMathSupport, + 'flint': fm.DiveMathSupportFlint, + 'flintcustom': fm.DiveMathSupportFlintCustom, + # maybe not complete?# 'gmp': fm.DiveMathSupportGmp, + # maybe not complete?# 'decimal': fm.DiveMathSupportDecimal, + # definitely not built yet.# 'libbf': fm.DiveMathSupportLibbf, + } + math_support = math_support_classes['native']() # Creates an instance for opt, arg in opts: - if opt in ['-d', '--duration']: - view_ctx.duration = float(arg) - elif opt in ['-f', '--fps']: - view_ctx.fps = float(arg) - elif opt in ['--clip-start-frame']: - # For construct_simple_timeline, use of --clip-start-frame - # makes --clip-frame-count kinda required - fractal_ctx.clip_start_frame = int(arg) - elif opt in ['--clip-frame-count']: - fractal_ctx.clip_frame_count = int(arg) - elif opt in ['-m', '--max-iter']: - fractal_ctx.max_iter = int(arg) - elif opt in ['-w', '--img-w']: - fractal_ctx.img_width = int(arg) - elif opt in ['-h', '--img-h']: - fractal_ctx.img_height = int(arg) - elif opt in ['--cmplx-w']: - fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(arg) - elif opt in ['--cmplx-h']: - fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(arg) - elif opt in ['-c', '--center']: - fractal_ctx.cmplx_center= fractal_ctx.math_support.createComplex(arg) - elif opt in ['--scaling-factor']: - fractal_ctx.scaling_factor = float(arg) - elif opt in ['-z', '--zoom']: - fractal_ctx.set_zoom_level = int(arg) - - # Worth noting, julia-list is used only for Timeline construction, - # and isn't an intrisic thing to the Algo. - elif opt in ['--julia-list']: - raw_julia_list = eval(arg) # expects a list of complex numbers - if len(raw_julia_list) <= 1: - print("Error: List of complex numbers for Julia walk must be at least two points") - sys.exit(0) - julia_list = [] - for currCenter in raw_julia_list: - julia_list.append(fractal_ctx.math_support.create_complex(currCenter)) - fractal_ctx.julia_list = julia_list - elif opt in ['--palette-test']: - if str(arg) == "gauss": - palette.create_gauss_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp": - palette.create_exp_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp2": - palette.create_exp2_gradient((0,0,0),(128,128,128)) - elif str(arg) == "list": - palette.create_gradient_from_list() - else: - print("Error: --palette-test arg must be one of gauss|exp|list") - sys.exit(0) - palette.display() - sys.exit(0) -# elif opt in ['--color']: + if opt in ['--math-support']: + if arg in math_support_classes: + # Creates an instance + math_support = math_support_classes[arg]() + elif opt in ['--digits-precision']: + params['digits_precision'] = int(arg) + # Important to also set expected precision before parsing param values + support_precision = params.get('digits_precision', 16) # 16 == native + math_support.setPrecision(support_precision) + params['math_support'] = math_support + + # Second-pass at params, figures out which mode we're operating + # in, so enforcement of parameter consistency can be localized. + # + # 4 modes: make-project, exploration, timeline, batch-in-timeline + mode_count = 0 + for opt, arg in opts: + if opt in ['--make-project']: + params['mode'] = 'make_project' + params['make_project_name'] = arg + mode_count += 1 + elif opt in ['--exploration']: + params['mode'] = 'exploration' + mode_count += 1 + elif opt in ['--timeline-name']: + # 'timeline' mode may be overwritten later with more specificity + params['mode'] = 'timeline' + params['timeline_name'] = arg + mode_count += 1 + elif opt in ['--project']: + params['project_name'] = arg + + if mode_count != 1: + raise ValueError("Exactly one of '--make-project=', '--timeline-name=', or '--exploration' is required to set the running mode.") + + # Special case where we can do all the work here and be done. + if params['mode'] == 'make_project': + make_project(params) + exit(0) + + # We know we're in a project mode, if we made it this far, so + # require that a project has been specified. + if 'project_name' not in params: + raise ValueError("Specifying --project= is required") + + # Load project parameters out of the params file + param_file_name = os.path.join(params['project_name'], 'params.json') + with open(param_file_name, 'rt') as param_handle: + params['project_params'] = json.load(param_handle) + + # Fourth pass at parameters, gathering those that are + # mode-specific, with mode-specific enforcement + if params['mode'] == 'exploration': + # Exploration also reads parameters out of the project's params.json: + # exploration_mesh_width + # exloration_mesh_height + # exploration_output_path + for opt, arg in opts: + if opt in ['--expl-algo']: + params['expl_algo'] = arg + elif opt in ['--expl-real-width']: + params['expl_real_width'] = math_support.createFloat(arg) + elif opt in ['--expl-imag-width']: + params['expl_imag_width'] = math_support.createFloat(arg) + elif opt in ['--expl-center']: + params['expl_center'] = math_support.createComplex(arg) + elif opt in ['--expl-frame-number']: + params['expl_frame_number'] = int(arg) + + required_params = ["expl_algo", + "expl_real_width", + "expl_imag_width", + "expl_center", + "expl_frame_number", + ] + for param_name in required_params: + if param_name not in params: + raise ValueError("For exploration mode, \"%s\" is a required parameter (well, that name with hyphens, not underscores)." % param_name) + + # Kinda a crazy invocation. Loads algorithm-specific parameters into + # a dictionary, based on that algorithm's static class parse function. + expl_algorithm_name = params.get('expl_algo', 'mandelbrot_solo') + params['algorithm_extra_params'] = algorithm_map[expl_algorithm_name].parse_options(opts) + # Theoretically possible we'll eventually want to run this for all + # possible algorithm types, but for now, just loading for the + # 'active' algorithm. + + elif params['mode'] == 'timeline': + for opt, arg in opts: + if opt in ['--batch-frame-numbers']: + # We're in batch mode, instead of entire-timeline mode, + # so get more specific. + params['mode'] = 'batch_timeline' + params['batch_frame_numbers'] = arg + + return params + +# # First-pass parameters handled so others can be responsive +# # - Math support, so instantiations are properly typed +# # - Algorithm name, so additional parameters can be read +# for opt, arg in opts: +# if opt in ['--gmp']: +# fractal_ctx.math_support = fm.DiveMathSupportGmp() +# elif opt in ['--flint']: +# fractal_ctx.math_support = fm.DiveMathSupportFlint() +# elif opt in ['--flintcustom']: +# fractal_ctx.math_support = fm.DiveMathSupportFlintCustom() +# elif opt in ['-a', '--algo']: +# if str(arg) in fractal_ctx.algorithm_map: +# fractal_ctx.algorithm_name = str(arg) +# +# if fractal_ctx.algorithm_name is None: +# fractal_ctx.algorithm_name = 'mandelbrot' +# +# # Kinda a crazy invocation. Loads algorithm-specific parameters into +# # a dictionary, based on that algorithm's static class parse function. +# fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] = fractal_ctx.algorithm_map[fractal_ctx.algorithm_name].parse_options(opts) +# # Theoretically possible we'll eventually want to run this for all +# # possible algorithm types, but for now, just loading for the +# # 'active' algorithm. +# +# for opt,arg in opts: +# if opt in ['-p', '--preview']: +# set_preview_mode(fractal_ctx, view_ctx) +# elif opt in ['-s', '--snapshot']: +# set_snapshot_mode(fractal_ctx, view_ctx, arg) +# elif opt in ['--demo']: +# set_demo1_params(fractal_ctx, view_ctx) +# elif opt in ['--demo-julia-walk']: +# set_julia_walk_demo1_params(fractal_ctx, view_ctx) +# +# palette = fp.FractalPalette() # Will get stashed for the algorithm to use +# +# for opt, arg in opts: +# if opt in ['-d', '--duration']: +# view_ctx.duration = float(arg) +# elif opt in ['-f', '--fps']: +# view_ctx.fps = float(arg) +# elif opt in ['--clip-start-frame']: +# # For construct_simple_timeline, use of --clip-start-frame +# # makes --clip-frame-count kinda required +# fractal_ctx.clip_start_frame = int(arg) +# elif opt in ['--clip-frame-count']: +# fractal_ctx.clip_frame_count = int(arg) +# elif opt in ['-m', '--max-iter']: +# fractal_ctx.max_iter = int(arg) +# elif opt in ['-w', '--img-w']: +# fractal_ctx.img_width = int(arg) +# elif opt in ['-h', '--img-h']: +# fractal_ctx.img_height = int(arg) +# elif opt in ['--cmplx-w']: +# fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(arg) +# elif opt in ['--cmplx-h']: +# fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(arg) +# elif opt in ['-c', '--center']: +# fractal_ctx.cmplx_center= fractal_ctx.math_support.createComplex(arg) +# elif opt in ['--scaling-factor']: +# fractal_ctx.scaling_factor = float(arg) +# elif opt in ['-z', '--zoom']: +# fractal_ctx.set_zoom_level = int(arg) +# +# # Worth noting, julia-list is used only for Timeline construction, +# # and isn't an intrisic thing to the Algo. +# elif opt in ['--julia-list']: +# raw_julia_list = eval(arg) # expects a list of complex numbers +# if len(raw_julia_list) <= 1: +# print("Error: List of complex numbers for Julia walk must be at least two points") +# sys.exit(0) +# julia_list = [] +# for currCenter in raw_julia_list: +# julia_list.append(fractal_ctx.math_support.create_complex(currCenter)) +# fractal_ctx.julia_list = julia_list +# elif opt in ['--palette-test']: # if str(arg) == "gauss": # palette.create_gauss_gradient((255,255,255),(0,0,0)) # elif str(arg) == "exp": @@ -1504,47 +1692,127 @@ def parse_options(fractal_ctx, view_ctx): # else: # print("Error: --palette-test arg must be one of gauss|exp|list") # sys.exit(0) -# fractal_ctx.palette = palette - elif opt in ['--project-name']: - fractal_ctx.project_name = arg - elif opt in ['--shared-cache-path']: - fractal_ctx.shared_cache_path = arg - elif opt in ['--build-cache']: - fractal_ctx.build_cache = True - elif opt in ['--invalidate-cache']: - fractal_ctx.invalidate_cache = True - elif opt in ['--banner']: - view_ctx.banner = True - elif opt in ['--verbose']: - verbosity = int(arg) - if verbosity not in [0,1,2,3]: - print("Invalid verbosity level (%d) use range 0-3"%(verbosity)) - sys.exit(0) - fractal_ctx.verbose = verbosity - elif opt in ['--gif']: - if view_ctx.vfilename != None: - print("Error : Already specific media type %s"%(view_ctx.vfilename)) - sys.exit(0) - view_ctx.vfilename = arg - elif opt in ['--mpeg']: - if view_ctx.vfilename != None: - print("Error : Already specific media type %s"%(view_ctx.vfilename)) - sys.exit(0) - view_ctx.vfilename = arg +# palette.display() +# sys.exit(0) +## elif opt in ['--color']: +## if str(arg) == "gauss": +## palette.create_gauss_gradient((255,255,255),(0,0,0)) +## elif str(arg) == "exp": +## palette.create_exp_gradient((255,255,255),(0,0,0)) +## elif str(arg) == "exp2": +## palette.create_exp2_gradient((0,0,0),(128,128,128)) +## elif str(arg) == "list": +## palette.create_gradient_from_list() +## else: +## print("Error: --palette-test arg must be one of gauss|exp|list") +## sys.exit(0) +## fractal_ctx.palette = palette +# elif opt in ['--project-name']: +# fractal_ctx.project_name = arg +# elif opt in ['--shared-cache-path']: +# fractal_ctx.shared_cache_path = arg +# elif opt in ['--build-cache']: +# fractal_ctx.build_cache = True +# elif opt in ['--invalidate-cache']: +# fractal_ctx.invalidate_cache = True +# elif opt in ['--banner']: +# view_ctx.banner = True +# elif opt in ['--verbose']: +# verbosity = int(arg) +# if verbosity not in [0,1,2,3]: +# print("Invalid verbosity level (%d) use range 0-3"%(verbosity)) +# sys.exit(0) +# fractal_ctx.verbose = verbosity +# elif opt in ['--gif']: +# if view_ctx.vfilename != None: +# print("Error : Already specific media type %s"%(view_ctx.vfilename)) +# sys.exit(0) +# view_ctx.vfilename = arg +# elif opt in ['--mpeg']: +# if view_ctx.vfilename != None: +# print("Error : Already specific media type %s"%(view_ctx.vfilename)) +# sys.exit(0) +# view_ctx.vfilename = arg +# +# # Stash the palette as an extra parameter +# extra_params = fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] +# extra_params['palette'] = palette + +def run_exploration(params): + algorithm_name = params['expl_algo'] + project_params = params['project_params'] + project_folder_name = params['project_name'] + + timeline = DiveTimeline(projectFolderName=project_folder_name, algorithm_name=algorithm_name, framerate=23.976, frameWidth=project_params['exploration_mesh_width'], frameHeight=project_params['exploration_mesh_height'], mathSupport=params['math_support']) + real_width = params['expl_real_width'] + imag_width = params['expl_imag_width'] + center = params['expl_center'] + + span = timeline.addNewSpanAtEnd(1, center, real_width, imag_width, real_width, imag_width) + + extra_params = params['algorithm_extra_params'] + dive_mesh=timeline.getMeshForFrame(0) + extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? - # Stash the palette as an extra parameter - extra_params = fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] - extra_params['palette'] = palette + output_folder_name = os.path.join(project_folder_name, project_params['exploration_output_path']) + + # Following is a class instantiation, of a string-specified Algo class + algorithm_map = DiveTimeline.algorithm_map() + frame_algorithm = algorithm_map[algorithm_name](dive_mesh=dive_mesh, frame_number=params['expl_frame_number'], output_folder_name=output_folder_name, extra_params=extra_params) + + frame_algorithm.run() + + +# if algorithm_name == 'julia': +# # Probably better to check for instance-of Julia Algo? +# span = DiveTimelineSpan(timeline, 1) +# span.addNewWindowKeyframe(0, real_width, imag_width) +# span.addNewWindowKeyframe(1, real_width, imag_width) +# span.addNewUniformKeyframe(0) +# span.addNewUniformKeyframe(1) +# +# span.addNewCenterKeyframe(0, center) +# span.addNewCenterKeyframe(1, center) +# +## Let's try to rely on extra params to pass in the julia_center? +## Should work, since I don't have to interpolate it anywhere? +## +## julia_center = algo_params['julia_center'] +## span.addNewComplexParameterKeyframe(0, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') +## span.addNewComplexParameterKeyframe(1, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') +# +# timeline.timelineSpans.append(span) +# +# else: +# span = timeline.addNewSpanAtEnd(1, center, real_width, imag_width, real_width, imag_width) + + +def run_timeline(params): + pass + +def run_batch_timeline(params): + pass if __name__ == "__main__": print("++ fractal.py version %s" % (MANDL_VER)) - fractal_ctx = FractalContext() - view_ctx = MediaView(16, 16, fractal_ctx) + params = parse_options() + mode = params['mode'] + if mode == 'exploration': + run_exploration(params) + elif mode == 'timeline': + run_timeline(params) + elif mode == 'batch_timeline': + run_batch_timeline(params) + else: + raise ValueError("Run mode is unrecognized - abandoning run") - parse_options(fractal_ctx, view_ctx) - - view_ctx.setup() - view_ctx.run() + + #fractal_ctx = FractalContext() + #view_ctx = MediaView(16, 16, fractal_ctx) + #parse_options(fractal_ctx, view_ctx) + + #view_ctx.setup() + #view_ctx.run() diff --git a/fractalmath.py b/fractalmath.py index a77ca6f..7c800d1 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -322,6 +322,9 @@ def julia(self, c, z0, escapeRadius, maxIter): return (n, z) def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): + # TODO: seems like the order of params to smoothAfterCalculation are + # all needlessly scrambled up? + if endingIter == maxIter: return float(maxIter) else: diff --git a/fractalpalette.py b/fractalpalette.py index f2f53f6..df996be 100644 --- a/fractalpalette.py +++ b/fractalpalette.py @@ -58,7 +58,11 @@ def per_frame_reset(self): def map_value_to_color(self, m, smoothing=False): if len(self.palette) == 0: - c = 255 - int(255 * self.hues[math.floor(m)]) + if len(self.hues) == 0: + # This is the default 'distance estimate' palette + c = int(float(m)*255) + else: + c = 255 - int(255 * self.hues[math.floor(m)]) return (c, c, c) if smoothing: diff --git a/julia.py b/julia.py index c32ed0a..f596d09 100644 --- a/julia.py +++ b/julia.py @@ -3,9 +3,9 @@ from collections import defaultdict -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw -from algo import JuliaFrameInfo, JuliaAlgo +from algo import JuliaAlgo import fractalpalette as fp diff --git a/mandelbrot.py b/mandelbrot.py deleted file mode 100644 index 46a575e..0000000 --- a/mandelbrot.py +++ /dev/null @@ -1,178 +0,0 @@ -# -- -# File: mandelbrot.py -# -# Basic escape iteration method for calculating the mandlebrot set -# -# Code cribbed from all over the place ... notably : -# -# https://www.codingame.com/playgrounds/2358/how-to-plot-the-mandelbrot-set/mandelbrot-set -# http://linas.org/art-gallery/escape/escape.html -# -# Misiurewicz points also cribbed from all over -# -# https://mrob.com/pub/muency/misiurewiczpoint.html -# https://www.youtube.com/watch?v=u1pwtSBTnPU&t=274s -# -# -# MPs: -# -# 0.4244 + 0.200759i; - -import math -import numpy as np - -from collections import defaultdict - -from PIL import Image, ImageDraw, ImageFont - -from algo import EscapeFrameInfo, EscapeAlgo -import fractalpalette as fp - -# TODO: probably rename these to MandelbrotEscape and MandelbrotDistanceEstimate? - -class Mandelbrot(EscapeAlgo): - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) - - self.algorithm_name = 'mandelbrot' - #print(dive_mesh) - - def calculate_results(self): - mesh_array = self.dive_mesh.generateMesh() - math_support = self.dive_mesh.mathSupport - - mandelbrot_function = np.vectorize(math_support.mandelbrot) - (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) - - smoothing_function = np.vectorize(math_support.smoothAfterCalculation) - pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) - - hist = defaultdict(int) - hist_smoothed = defaultdict(int) - - for x in range(0, mesh_array.shape[0]): - for y in range(0, mesh_array.shape[1]): - # Not using mathSupport's floor() here, because it should just be a normal-scale float - if pixel_values_2d[x,y] < self.max_escape_iterations: - #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) - hist[math.floor(pixel_values_2d[x,y])] += 1 - if pixel_values_2d_smoothed[x,y] < self.max_escape_iterations: - hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 - -#### -# Sorry, didn't update this iterative version of the code when -# it got moved into algo, so it won't run like this. -#### -# pixel_values_2d = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.uint32) -# pixel_values_2d_smoothed = np.zeros((mesh.shape[0], mesh.shape[1]), dtype=np.float) -# hist = defaultdict(int) -# hist_smoothed = defaultdict(int) -# -# show_row_progress = True -# for x in range(0, mesh.shape[0]): -# for y in range(0, mesh.shape[1]): -# if self.fractalType == 'julia': -# (pixel_values_2d[x,y], lastZee) = self.mathSupport.julia(diveMesh.center, mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) -# else: # self.FractalType == 'mandelbrot' -# (pixel_values_2d[x,y], lastZee) = self.mathSupport.mandelbrot(mesh[x,y], diveMesh.escapeRadius, diveMesh.maxEscapeIterations) -# -# pixel_values_2d_smoothed[x,y] = self.mathSupport.smoothAfterCalculation(lastZee, pixel_values_2d[x,y], diveMesh.maxEscapeIterations) -# -# # Not using mathSupport's floor() here, because it should just be a normal-scale float -# if pixel_values_2d[x,y] < diveMesh.maxEscapeIterations: -# #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(self.mathSupport.floor(pixel_values_2d[x,y])))) -# hist[math.floor(pixel_values_2d[x,y])] += 1 -# if pixel_values_2d_smoothed[x,y] < diveMesh.maxEscapeIterations: -# hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 -# -# if show_row_progress == True: -# print("%d-" % x, end="") -# sys.stdout.flush() - - self.cache_frame.frame_info.raw_values = pixel_values_2d - self.cache_frame.frame_info.raw_histogram = hist - self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed - self.cache_frame.frame_info.smooth_histogram = hist_smoothed - - return - - #### - # Graveyard of failed attempts at further vectorizing this, maybe there's a clue in here - # somewhere... - #### - - # In search of efficient ways to apply the map, and getting stuck with various issues - # like pickling, which are keeping me from using multiprocessing.Pool - -# Seemed to go exponential run time for some bizarre reason -# # Probably not necessary, but lining up the 2-element subarray -# pixel_inputs_1d = pixel_values_2d.reshape((mesh.shape[0] * mesh.shape[1])) -# -# # Pretty sure this is mistakenly doing an n! pass, or something just as ridiculous. -# #print("shape of pixel_inputs_1d: %s" % str(pixel_inputs_1d.shape)) -# pixel_values_1d = np.array([self.mathSupport.mandelbrot(complex_value, diveMesh.escapeRadius, diveMesh.maxEscapeIterations, diveMesh.shouldSmooth) for complex_value in pixel_inputs_1d]) -# -# pixel_values_2d = pixel_values_1d.reshape((mesh.shape[0], mesh.shape[1])) -# #pixel_values_2d = np.squeeze(pixel_values_2d, axis=2) # Incantation to remove a sub-array level -# #print("shape of pixel_values_2d: %s" % str(pixel_values_2d.shape)) - - #nope - #theFunction = np.vectorize(self.mandelbrot_flint) - #pixel_values_1d = theFunction(pixel_inputs_1d) - - #pixel_inputs_1d = pixel_inputs.reshape(1,self.img_width * self.img_height) - - #pixel_values_1d = map(self.mandelbrot_flint, pixel_inputs_1d) - #pixel_values_1d = np.array(list(map(self.mandelbrot_flint, pixel_inputs_1d))) - #print("shape of pixel_values_1d: %s" % str(pixel_values_1d.shape)) - - # Can't pikcle... hmm - #mandelpool = multiprocessing.Pool(processes = 1) - #pixel_values_1d = mandelpool.map(self.mandelbrot_flint, pixel_inputs_1d) - #mandelpool.close() - #mandelpool.join() - - def pre_image_hook(self): - if self.use_smoothing == True: - self.palette.histogram = self.cache_frame.frame_info.smooth_histogram - else: - self.palette.histogram = self.cache_frame.frame_info.raw_histogram - - # TODO: Hey, calc_hues should get a different range of bins if - # smoothed values are floats! - self.palette.calc_hues(self.max_escape_iterations) - - def generate_image(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.use_smoothing == True: - pixel_values_2d = self.cache_frame.frame_info.smooth_values.T - else: - pixel_values_2d = self.cache_frame.frame_info.raw_values.T - #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - -# TODO: Really, width and height are all kinda incorrect here - -# gotta spend some TLC on the array shape and transpose. - (image_width, image_height) = pixel_values_2d.shape - im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) - - for x in range(0, image_width): - for y in range(0, image_height): - color = self.palette.map_value_to_color(pixel_values_2d[x,y]) - - # Plot the point - draw.point([x, y], color) - - if self.burn_in == True: - meta = self.get_frame_metadata() - if meta: - burn_in_text = u"%d" % (meta['frame_number']) - #burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - self.burn_text_to_drawing(burn_in_text, draw) - - return im - - def ending_hook(self): - self.palette.per_frame_reset() - diff --git a/mandelbrot_smooth.py b/mandelbrot_smooth.py new file mode 100644 index 0000000..5f109ef --- /dev/null +++ b/mandelbrot_smooth.py @@ -0,0 +1,105 @@ +# -- +# File: mandelbrot_smooth.py +# +# Subclass of MandelbrotSolo, which implements a process_counts() +# function that populates 'processed_array' with processed values, +# instead of defaulting to the counts array. +# +# -- + +import os + +import math +import numpy as np + +from PIL import Image, ImageDraw + +from mandelbrot_solo import MandelbrotSolo + +class MandelbrotSmooth(MandelbrotSolo): + +# TODO: Yeah, really should be a palette +# +# @staticmethod +# def options_list(): +# whole_list = MandelbrotSolo.options_list() +# whole_list.extend(["color="]) +# return whole_list +# +# @staticmethod +# def parse_options(opts): +# options = MandelbrotSolo.parse_options(opts) +# for opt,arg in opts: +# if opt in ['--color']: +# options['color'] = (.1,.2,.3) # dark +# #options['color'] = (.0,.6,1.0) # blue / yellow +# #options['color'] = (.0,.6,1.0) +# +# return options + + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) + + self.algorithm_name = 'mandelbrot_smooth' + + self.color = (.0,.6,1.0) # blue / yellow + #self.color = (.1,.2,.3) # dark + + def process_counts(self): + smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) + self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + + def generate_image(self): + (image_height, image_width) = self.processed_array.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + # Note: Image's width,height is backwards from numpy's size (rows, cols) + for x in range(0, image_width): + for y in range(0, image_height): + color = self.map_value_to_color(self.processed_array[y,x]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_metadata() + if meta: + burn_in_text = u"%d" % (meta['frame_number']) + self.burn_text_to_drawing(burn_in_text, draw) + + image_filename_base = u"%d.tiff" % self.frame_number + self.output_image_file_name = os.path.join(self.output_folder_name, image_filename_base) + im.save(self.output_image_file_name) + + def map_value_to_color(self, val): + # TODO: should make this a special rotating palette, + # or bound the values before doing a lookup to a palette. + if math.isnan(val): + return (0,0,0) + + if self.color: + # (yellow blue 0,.6,1.0) + c1 = 1 + math.cos(3.0 + val*0.15 + self.color[0]) + c2 = 1 + math.cos(3.0 + val*0.15 + self.color[1]) + c3 = 1 + math.cos(3.0 + val*0.15 + self.color[2]) + + if c1 <= 0 or math.isnan(c1): + c1int = 0 + else: + c1int = int(255.*((c1/4.) * 3.) / 1.5) + if c2 <= 0 or math.isnan(c2): + c2int = 0 + else: + c2int = int(255.*((c2/4.) * 3.) / 1.5) + if c3 <= 0 or math.isnan(c3): + c3int = 0 + else: + c3int = int(255.*((c3/4.) * 3.) / 1.5) + + return (c1int,c2int,c3int) + else: + c1 = 1 + math.cos(3.0 + val*0.15) + cint = int(255.*((c1/4.) * 3.) / 1.5) + return (cint,cint,cint) + diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py index c0a6c47..b12b650 100644 --- a/mandelbrot_solo.py +++ b/mandelbrot_solo.py @@ -18,81 +18,73 @@ # # 0.4244 + 0.200759i; +import os +import pickle + import math import numpy as np from collections import defaultdict -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw -from algo import EscapeFrameInfo, EscapeAlgo +from algo import EscapeAlgo import fractalpalette as fp - class MandelbrotSolo(EscapeAlgo): - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) self.algorithm_name = 'mandelbrot_solo' - def generate_results(self): - """ - Special no-cache implementation of the main results sequence - """ - self.cache_frame = self.build_cache_frame() - self.calculate_results() - - return - - def calculate_results(self): - mesh_array = self.dive_mesh.generateMesh() + def generate_counts(self): math_support = self.dive_mesh.mathSupport #mandelbrot_function = np.vectorize(math_support.mandelbrot_beginning) mandelbrot_function = np.vectorize(math_support.mandelbrot) - (pixel_values_2d, last_zees) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) + (self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) + + counts_name_base = u"%d.counts.pik" % self.frame_number + counts_file_name = os.path.join(self.output_folder_name, counts_name_base) + with open(counts_file_name, 'wb') as counts_handle: + pickle.dump(self.counts_array, counts_handle) + def pre_image_hook(self): hist = defaultdict(int) - for x in range(0, mesh_array.shape[0]): - for y in range(0, mesh_array.shape[1]): + # numpyArray.shape returns (rows, columns) + for y in range(0, self.mesh_array.shape[0]): + for x in range(0, self.mesh_array.shape[1]): # Not using mathSupport's floor() here, because it should just be a normal-scale float - if pixel_values_2d[x,y] < self.max_escape_iterations: - #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) - hist[math.floor(pixel_values_2d[x,y])] += 1 + if self.counts_array[y,x] < self.max_escape_iterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(counts_array[y,x]), str(math.floor(counts_array[y,x])))) + hist[math.floor(self.counts_array[y,x])] += 1 - self.cache_frame.frame_info.raw_values = pixel_values_2d - self.cache_frame.frame_info.raw_histogram = hist - - return - - def pre_image_hook(self): - self.palette.histogram = self.cache_frame.frame_info.raw_histogram + self.palette.histogram = hist self.palette.calc_hues(self.max_escape_iterations) def generate_image(self): - pixel_values_2d = self.cache_frame.frame_info.raw_values.T - -# TODO: Really, width and height are all kinda incorrect here - -# gotta spend some TLC on the array shape and transpose. - (image_width, image_height) = pixel_values_2d.shape + (image_height, image_width) = self.processed_array.shape im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) draw = ImageDraw.Draw(im) - + + # Note: Image's width,height is backwards from numpy's size (rows, cols) for x in range(0, image_width): for y in range(0, image_height): - color = self.palette.map_value_to_color(pixel_values_2d[x,y]) + color = self.palette.map_value_to_color(self.processed_array[y,x]) # Plot the point draw.point([x, y], color) if self.burn_in == True: - meta = self.get_frame_metadata() + meta = self.get_metadata() if meta: burn_in_text = u"%d" % (meta['frame_number']) self.burn_text_to_drawing(burn_in_text, draw) - return im + image_filename_base = u"%d.tiff" % self.frame_number + self.output_image_file_name = os.path.join(self.output_folder_name, image_filename_base) + im.save(self.output_image_file_name) def ending_hook(self): self.palette.per_frame_reset() diff --git a/mandeldistance.py b/mandeldistance.py index a468b52..c07b2a5 100644 --- a/mandeldistance.py +++ b/mandeldistance.py @@ -4,99 +4,43 @@ # # -- -from algo import Algo +import os +import pickle -import math import numpy as np -from collections import defaultdict +from PIL import Image, ImageDraw # Should be removable if palette performed the lookup -from PIL import Image, ImageDraw, ImageFont +from mandelbrot_solo import MandelbrotSolo -from algo import EscapeFrameInfo, EscapeAlgo -import fractalpalette as fp - - -def clamp(num, min_value, max_value): - return max(min(num, max_value), min_value) - - -class MandelDistance(EscapeAlgo): - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) +class MandelDistance(MandelbrotSolo): + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) self.algorithm_name = 'mandeldistance' - def calculate_results(self): - mesh_array = self.dive_mesh.generateMesh() - math_support = self.dive_mesh.mathSupport - - #mandelbrot_function = np.vectorize(math_support.orig_mandelbrotDistanceEstimate) -# mandelbrot_function = np.vectorize(math_support.mandelbrotDistanceEstimate) -# self.cache_frame.frame_info.raw_values = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) - - - mandelbrot_function = np.vectorize(math_support.mandelbrotDistanceEstimate) - (pixel_values_2d, distances) = mandelbrot_function(mesh_array, self.escape_radius, self.max_escape_iterations) - - rescaleFunction = np.vectorize(math_support.rescaleForRange) - (pixel_values_2d_smoothed) = rescaleFunction(distances, pixel_values_2d, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) - - self.cache_frame.frame_info.raw_values = pixel_values_2d - self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed - - return - - def generate_image(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.use_smoothing == True: - pixel_values_2d = self.cache_frame.frame_info.smooth_values.T - else: - pixel_values_2d = self.cache_frame.frame_info.raw_values.T - - ##pixel_values_2d = self.cache_frame.frame_info.raw_values.T - #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - - -# TODO: Really, width and height are all kinda incorrect here - -# gotta spend some TLC on the array shape and transpose. - (image_width, image_height) = pixel_values_2d.shape - im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) - - for x in range(0, image_width): - for y in range(0, image_height): - color = self.map_value_to_color(pixel_values_2d[x,y]) - - # Plot the point - draw.point([x, y], color) - - if self.burn_in == True: - meta = self.get_frame_metadata() - if meta: - burn_in_text = u"%d" % (meta['frame_number']) - self.burn_text_to_drawing(burn_in_text, draw) - - return im - - def map_value_to_color(self, raw_val): - cint = int(float(raw_val) * 255) - return (cint,cint,cint) - -# def map_value_to_color(self, raw_val): -# ## Was an error, but when val was negative, this blew up. -# val = float(raw_val) -# if val < 0.0: -# val = 0.0 -# zoo = .1 -# zoom_level = 1. / (self.dive_mesh.imagMeshGenerator.baseWidth) -# d = clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); -# #if math.isnan(d): -# # print("NAN val: %s zoom_level: %s width: %s" % (str(val), str(zoom_level), self.dive_mesh.imagMeshGenerator.baseWidth)) -# cint = int(float(d)*255) -# # Forced float() here because baseWidth is maybe a special math subtype -# -# return (cint,cint,cint) - + def generate_counts(self): + """ + Originally thought this could use 'normal' mandelbrot, but keeping + track of the derivative is important for the distance estimate, so + we use the math_support's distance esitmate function instead. + """ + mandelbrot_function = np.vectorize(self.dive_mesh.mathSupport.mandelbrotDistanceEstimate) + (self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) + + counts_name_base = u"%d.counts.pik" % self.frame_number + counts_file_name = os.path.join(self.output_folder_name, counts_name_base) + with open(counts_file_name, 'wb') as counts_handle: + pickle.dump(self.counts_array, counts_handle) + + def process_counts(self): + smoothing_function = np.vectorize(self.dive_mesh.mathSupport.rescaleForRange) + self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) + + def pre_image_hook(self): + """ + Don't actually want to update the palette right now for + calculating the distance estimate color. + """ + pass diff --git a/mesh_explore.py b/mesh_explore.py index 969b7b0..b679307 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -207,6 +207,15 @@ def onclick(event): global params params = parse_options() + (params, remainingOptions) = parse_options() + + print("First remaining: %s" % remainingOptions) + opts, secondRemaining = getopt.getopt(remainingOptions,["foo=", + ]) + print("Second remaining: %s" % secondRemaining) + + exit(0) + frameNumber = params.get('frameNumber', 0) centerString = params.get('centerString', None) diff --git a/smooth.py b/smooth.py index e1200df..3e37d88 100644 --- a/smooth.py +++ b/smooth.py @@ -14,9 +14,9 @@ import math import numpy as np -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw -from algo import Algo, EscapeFrameInfo, EscapeAlgo +from algo import Algo, EscapeAlgo class Smooth(EscapeAlgo): diff --git a/test_divemesh.py b/test_divemesh.py index 4b54560..e50b08e 100644 --- a/test_divemesh.py +++ b/test_divemesh.py @@ -20,8 +20,29 @@ def tearDownClass(cls): cls.pyMathSupport = None cls.flintMathSupport = None + def test_meshShape(self): + meshWidth = 5 + meshHeight = 3 + widthGen = MeshGeneratorUniform(self.pyMathSupport, 'width', 0.5, 0.5) + heightGen = MeshGeneratorUniform(self.pyMathSupport, 'height', 2.0, 4.0) + comparisonArrayString = """[[(0.25+0j) (0.375+0j) (0.5+0j) (0.625+0j) (0.75+0j)] + [(0.25+2j) (0.375+2j) (0.5+2j) (0.625+2j) (0.75+2j)] + [(0.25+4j) (0.375+4j) (0.5+4j) (0.625+4j) (0.75+4j)]]""" + + diveMesh = DiveMesh(meshWidth, meshHeight, widthGen, heightGen, self.pyMathSupport) + + meshArray = diveMesh.generateMesh() + meshShape = meshArray.shape + #print(meshArray) + + # numpy array.shape returns (rows, columns), which is (height, width) + self.assertEqual(meshShape[1], meshWidth) + self.assertEqual(meshShape[0], meshHeight) + + self.assertEqual(str(meshArray), comparisonArrayString) + def test_pythonGeneratorPickle(self): - centerFloatString = '-1.769383179195515018213' + centerFloatString = '-1.7693831791955' baseWidthString = '2.0' pyCenter = self.pyMathSupport.createFloat(centerFloatString) @@ -37,7 +58,7 @@ def test_pythonGeneratorPickle(self): def test_flintGeneratorPickle(self): # Maybe better to use a bracket-arb string here? But doesn't matter? - centerFloatString = '-1.769383179195515018213' + centerFloatString = '-1.7693831791955' baseWidthString = '2.0' flintCenter = self.flintMathSupport.createFloat(centerFloatString) @@ -52,8 +73,8 @@ def test_flintGeneratorPickle(self): self.assertEqual(float(uniformGen.baseWidth), float(otherGen.baseWidth)) def test_pythonDiveMeshPickle(self): - centerWidthString = '-1.769383179195515018213' - centerHeightString = '0.00423684791873677221' + centerWidthString = '-1.7693831791955' + centerHeightString = '0.0042368479187' baseRealWidthString = '5.0' baseImagWidthString = '3.0' @@ -68,13 +89,10 @@ def test_pythonDiveMeshPickle(self): imagWidth = mathSupport.createFloat(baseImagWidthString) imagGen = MeshGeneratorUniform(mathSupport, 'height', imagCenter, imagWidth) - centerComplexString = '-1.769383179195515018213+0.00423684791873677221j' - pyComplex = mathSupport.createComplex(centerComplexString) - meshWidth = 320 meshHeight = 240 - diveMesh = DiveMesh(meshWidth, meshHeight, pyComplex, realGen, imagGen, mathSupport) + diveMesh = DiveMesh(meshWidth, meshHeight, realGen, imagGen, mathSupport) pickleValue = pickle.dumps(diveMesh) loadedMesh = pickle.loads(pickleValue) @@ -82,10 +100,6 @@ def test_pythonDiveMeshPickle(self): self.assertEqual(int(diveMesh.meshWidth), int(loadedMesh.meshWidth)) self.assertEqual(int(diveMesh.meshHeight), int(loadedMesh.meshHeight)) - self.assertEqual(float(diveMesh.center.real), float(loadedMesh.center.real)) - self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) - self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) - #print("realGen: \"%s\"" % str(diveMesh.realMeshGenerator)) #print("imagGen: \"%s\"" % str(diveMesh.imagMeshGenerator)) #print("after realGen: \"%s\"" % str(loadedMesh.realMeshGenerator)) @@ -93,8 +107,8 @@ def test_pythonDiveMeshPickle(self): def test_flintDiveMeshPickle(self): # Maybe better to use a bracket-arb string here? But doesn't matter? - centerWidthString = '-1.769383179195515018213' - centerHeightString = '0.00423684791873677221' + centerWidthString = '-1.7693831791955' + centerHeightString = '0.0042368479187' baseRealWidthString = '5.0' baseImagWidthString = '3.0' @@ -109,13 +123,10 @@ def test_flintDiveMeshPickle(self): imagWidth = mathSupport.createFloat(baseImagWidthString) imagGen = MeshGeneratorUniform(mathSupport, 'height', imagCenter, imagWidth) - centerComplexString = '-1.769383179195515018213+0.00423684791873677221j' - pyComplex = mathSupport.createComplex(centerComplexString) - meshWidth = 320 meshHeight = 240 - diveMesh = DiveMesh(meshWidth, meshHeight, pyComplex, realGen, imagGen, mathSupport) + diveMesh = DiveMesh(meshWidth, meshHeight, realGen, imagGen, mathSupport) pickleValue = pickle.dumps(diveMesh) loadedMesh = pickle.loads(pickleValue) @@ -123,10 +134,6 @@ def test_flintDiveMeshPickle(self): self.assertEqual(int(diveMesh.meshWidth), int(loadedMesh.meshWidth)) self.assertEqual(int(diveMesh.meshHeight), int(loadedMesh.meshHeight)) - self.assertEqual(float(diveMesh.center.real), float(loadedMesh.center.real)) - self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) - self.assertEqual(float(diveMesh.center.imag), float(loadedMesh.center.imag)) - #print("realGen: \"%s\"" % str(diveMesh.realMeshGenerator)) #print("imagGen: \"%s\"" % str(diveMesh.imagMeshGenerator)) #print("after realGen: \"%s\"" % str(loadedMesh.realMeshGenerator)) From 69b87a28b8559e83ccdaeabef70b9e16ced7be8b Mon Sep 17 00:00:00 2001 From: clint Date: Tue, 24 Aug 2021 12:11:10 -0700 Subject: [PATCH 35/44] julia exploration, with and without smoothing, looks like its working now too. --- algo.py | 12 +++---- fractal.py | 15 +++++---- julia.py | 91 --------------------------------------------------- julia_solo.py | 80 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 103 deletions(-) delete mode 100644 julia.py create mode 100644 julia_solo.py diff --git a/algo.py b/algo.py index 6e4b315..42bb9ac 100644 --- a/algo.py +++ b/algo.py @@ -45,7 +45,7 @@ def options_list(): return [] @staticmethod - def parse_options(opts): + def load_options_with_math_support(opts, math_support): return {} def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): @@ -148,8 +148,8 @@ def options_list(): return whole_list @staticmethod - def parse_options(opts): - options = Algo.parse_options(opts) + def load_options_with_math_support(opts, math_support): + options = Algo.load_options_with_math_support(opts, math_support) for opt,arg in opts: if opt in ['--escape-radius']: @@ -191,14 +191,14 @@ def options_list(): return whole_list @staticmethod - def parse_options(opts): + def load_options_with_math_support(opts, math_support): # Considered loading this with default values, but didn't # want to compete with the defaults in __init__()? - options = EscapeAlgo.parse_options(opts) + options = EscapeAlgo.load_options_with_math_support(opts, math_support) for opt,arg in opts: if opt in ['--julia-center']: - options['julia_center'] = arg + options['julia_center'] = math_support.createComplex(arg) return options diff --git a/fractal.py b/fractal.py index 57ed30c..f8efbdd 100644 --- a/fractal.py +++ b/fractal.py @@ -48,11 +48,13 @@ import divemesh as mesh from algo import Algo # Abstract base class import, because we rely on it. -from julia import Julia from mandelbrot_solo import MandelbrotSolo from mandelbrot_smooth import MandelbrotSmooth from mandeldistance import MandelDistance -from smooth import Smooth +from julia_solo import JuliaSolo +from julia_smooth import JuliaSmooth + +#from smooth import Smooth MANDL_VER = "0.1" @@ -355,11 +357,12 @@ class DiveTimeline: @staticmethod def algorithm_map(): - return {'julia' : Julia, - 'mandelbrot_solo' : MandelbrotSolo, + return {'mandelbrot_solo' : MandelbrotSolo, 'mandelbrot_smooth' : MandelbrotSmooth, 'mandeldistance' : MandelDistance, - 'smooth': Smooth, + 'julia_solo' : JuliaSolo, + 'julia_smooth' : JuliaSmooth, + #'smooth': Smooth, } def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, frameHeight, mathSupport): @@ -1590,7 +1593,7 @@ def parse_options(): # Kinda a crazy invocation. Loads algorithm-specific parameters into # a dictionary, based on that algorithm's static class parse function. expl_algorithm_name = params.get('expl_algo', 'mandelbrot_solo') - params['algorithm_extra_params'] = algorithm_map[expl_algorithm_name].parse_options(opts) + params['algorithm_extra_params'] = algorithm_map[expl_algorithm_name].load_options_with_math_support(opts, math_support) # Theoretically possible we'll eventually want to run this for all # possible algorithm types, but for now, just loading for the # 'active' algorithm. diff --git a/julia.py b/julia.py deleted file mode 100644 index f596d09..0000000 --- a/julia.py +++ /dev/null @@ -1,91 +0,0 @@ -import math -import numpy as np - -from collections import defaultdict - -from PIL import Image, ImageDraw - -from algo import JuliaAlgo - -import fractalpalette as fp - -class Julia(JuliaAlgo): - - def __init__(self, dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params={}): - super().__init__(dive_mesh, frame_number, project_folder_name, shared_cache_path, build_cache, invalidate_cache, extra_params) - - self.algorithm_name = 'julia' - - def calculate_results(self): - mesh_array = self.dive_mesh.generateMesh() - math_support = self.dive_mesh.mathSupport - - julia_function = np.vectorize(math_support.julia) - (pixel_values_2d, last_zees) = julia_function(self.julia_center, mesh_array, self.escape_radius, self.max_escape_iterations) - - smoothing_function = np.vectorize(math_support.smoothAfterCalculation) - pixel_values_2d_smoothed = smoothing_function(last_zees, pixel_values_2d, self.max_escape_iterations, self.escape_radius) - - hist = defaultdict(int) - hist_smoothed = defaultdict(int) - - for x in range(0, mesh_array.shape[0]): - for y in range(0, mesh_array.shape[1]): - # Not using mathSupport's floor() here, because it should just be a normal-scale float - if pixel_values_2d[x,y] < self.max_escape_iterations: - #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(pixel_values_2d[x,y]), str(math_support.floor(pixel_values_2d[x,y])))) - hist[math.floor(pixel_values_2d[x,y])] += 1 - if pixel_values_2d_smoothed[x,y] < self.max_escape_iterations: - hist_smoothed[math.floor(pixel_values_2d_smoothed[x,y])] += 1 - - self.cache_frame.frame_info.raw_values = pixel_values_2d - self.cache_frame.frame_info.raw_histogram = hist - self.cache_frame.frame_info.smooth_values = pixel_values_2d_smoothed - self.cache_frame.frame_info.smooth_histogram = hist_smoothed - - def pre_image_hook(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.use_smoothing == True: - self.palette.histogram = self.cache_frame.frame_info.smooth_histogram - else: - self.palette.histogram = self.cache_frame.frame_info.raw_histogram - - # TODO: Hey, calc_hues should get a different range of bins if - # smoothed values are floats! - self.palette.calc_hues(self.max_escape_iterations) - - def generate_image(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.use_smoothing == True: - pixel_values_2d = self.cache_frame.frame_info.smooth_values.T - else: - pixel_values_2d = self.cache_frame.frame_info.raw_values.T - #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - -# TODO: Really, width and height are all kinda incorrect here - -# gotta spend some TLC on the array shape and transpose. - (image_width, image_height) = pixel_values_2d.shape - im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) - - for x in range(0, image_width): - for y in range(0, image_height): - color = self.palette.map_value_to_color(pixel_values_2d[x,y]) - - # Plot the point - draw.point([x, y], color) - - if self.burn_in == True: - meta = self.get_frame_metadata() - if meta: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - self.burn_text_to_drawing(burn_in_text, draw) - - return im - - def ending_hook(self): - self.palette.per_frame_reset() - - diff --git a/julia_solo.py b/julia_solo.py new file mode 100644 index 0000000..b9c1949 --- /dev/null +++ b/julia_solo.py @@ -0,0 +1,80 @@ +# -- +# File: julia_solo.py +# +# +# Interesting julia-center points: +# -.8+.145j +# +# -- + +import os +import pickle + +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw + +from algo import JuliaAlgo +import fractalpalette as fp + +class JuliaSolo(JuliaAlgo): + + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) + + self.algorithm_name = 'julia_solo' + + def generate_counts(self): + math_support = self.dive_mesh.mathSupport + + julia_function = np.vectorize(math_support.julia) + (self.counts_array, self.last_values_array) = julia_function(self.julia_center, self.mesh_array, self.escape_radius, self.max_escape_iterations) + + counts_name_base = u"%d.counts.pik" % self.frame_number + counts_file_name = os.path.join(self.output_folder_name, counts_name_base) + with open(counts_file_name, 'wb') as counts_handle: + pickle.dump(self.counts_array, counts_handle) + + def pre_image_hook(self): + hist = defaultdict(int) + + # numpyArray.shape returns (rows, columns) + for y in range(0, self.mesh_array.shape[0]): + for x in range(0, self.mesh_array.shape[1]): + # Not using mathSupport's floor() here, because it should just be a normal-scale float + if self.counts_array[y,x] < self.max_escape_iterations: + #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(counts_array[y,x]), str(math.floor(counts_array[y,x])))) + hist[math.floor(self.counts_array[y,x])] += 1 + + self.palette.histogram = hist + self.palette.calc_hues(self.max_escape_iterations) + + def generate_image(self): + (image_height, image_width) = self.processed_array.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + # Note: Image's width,height is backwards from numpy's size (rows, cols) + for x in range(0, image_width): + for y in range(0, image_height): + color = self.palette.map_value_to_color(self.processed_array[y,x]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_metadata() + if meta: + burn_in_text = u"%d" % (meta['frame_number']) + self.burn_text_to_drawing(burn_in_text, draw) + + image_filename_base = u"%d.tiff" % self.frame_number + self.output_image_file_name = os.path.join(self.output_folder_name, image_filename_base) + im.save(self.output_image_file_name) + + def ending_hook(self): + self.palette.per_frame_reset() + From 7f2a30dab6148580a83b8fb63202d29d16594f68 Mon Sep 17 00:00:00 2001 From: clint Date: Thu, 9 Sep 2021 13:59:45 -0700 Subject: [PATCH 36/44] Likely non-functional, but checking in so I can switch platforms for testing. Millisecond-based timeline. Timeline files persisted as JSON. Support scripts for markers and timelines and frame batches. --- algo.py | 2 +- batch_files_for_timeline.py | 95 ++ compile_video.py | 1 + divemesh.py | 201 +++- fractal.py | 1988 ++++++++++------------------------- fractalmath.py | 175 ++- fractalpalette.py | 51 + julia_smooth.py | 119 +++ mandelbrot_smooth.py | 67 +- mandelbrot_solo.py | 5 + markers_to_timeline.py | 201 ++++ mesh_explore.py | 432 +++++--- strip_comments.py | 12 + test_fractalmath.py | 50 + 14 files changed, 1747 insertions(+), 1652 deletions(-) create mode 100644 batch_files_for_timeline.py create mode 100644 julia_smooth.py create mode 100644 markers_to_timeline.py create mode 100644 strip_comments.py diff --git a/algo.py b/algo.py index 42bb9ac..9993685 100644 --- a/algo.py +++ b/algo.py @@ -168,7 +168,7 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) # Load, with optional default values self.escape_radius = extra_params.get('escape_radius', 2.0) - self.max_escape_iterations = extra_params.get('max_escape_iterations', 255) + self.max_escape_iterations = int(extra_params.get('max_escape_iterations', 255)) self.burn_in = extra_params.get('burn_in', False) self.palette = extra_params.get('palette', fp.FractalPalette()) diff --git a/batch_files_for_timeline.py b/batch_files_for_timeline.py new file mode 100644 index 0000000..b83a4ce --- /dev/null +++ b/batch_files_for_timeline.py @@ -0,0 +1,95 @@ + +import os, sys, getopt, math + +import numpy as np + +from collections import defaultdict + +from fractal import * + +def parse_options(): + params = {} # Most everything here fills this dictionary + + options_list = ["project=", + # Mode params + "timeline-name=", + "batch-count=", + "sequential", # Default is cycling (so earliest frames happen first) + ] + + opts, args = getopt.getopt(sys.argv[1:], "", options_list) + + for opt, arg in opts: + if opt in ['--project']: + params['project_name'] = arg + elif opt in ['--timeline-name']: + params['timeline_name'] = arg + elif opt in ['--batch-count']: + params['batch_count'] = int(arg) + elif opt in ['--sequential']: + params['group_type'] = 'sequential' + + if 'project_name' not in params: + raise ValueError("Specifying --project= is required") + if 'timeline_name' not in params: + raise ValueError("Specifying --timeline-name= is required") + if 'batch_count' not in params: + raise ValueError("Specifying --batch-count= is required") + + if 'group_type' not in params: + params['group_type'] = 'cycling' + + # Load project parameters out of the params file + param_file_name = os.path.join(params['project_name'], 'params.json') + with open(param_file_name, 'rt') as param_handle: + params['project_params'] = json.load(param_handle) + + return params + +def write_batches(params): + project_params = params['project_params'] + project_folder_name = params['project_name'] + timeline_name = params['timeline_name'] + + timeline_file_name = os.path.join(params['project_name'], params['project_params']['edit_timelines_path'], params['timeline_name']) + timeline = load_timeline_from_file(timeline_file_name, params) + + main_span = timeline.getMainSpan() + frame_count = timeline.getFramesInDuration(main_span.duration) + #print(f"duration: {main_span.duration}") + print(f"frame count: {frame_count}") + + batch_folder_base = f"{params['timeline_name']}_batches" + batch_folder_name = os.path.join(project_folder_name, params['project_params']['edit_timelines_path'], batch_folder_base) + + if not os.path.exists(batch_folder_name): + os.makedirs(batch_folder_name) + + batch_lists = defaultdict(list) + + if params['group_type'] == 'sequential': + batch_lists = np.array_split(range(frame_count), params['batch_count']) + else: # params['group_type'] == 'cycling': + for curr_frame_number in range(frame_count): + currIndex = frame_count % params['batch_count'] + batch_lists[curr_frame_number % params['batch_count']].append(curr_frame_number) + + for curr_batch_id in range(len(batch_lists)): + #print(f"{batch_lists[curr_batch_id]}") + + batch_file_name = os.path.join(batch_folder_name, f"batch_{curr_batch_id}.txt") + with open(batch_file_name, 'wt') as batch_handle: + for curr_frame in batch_lists[curr_batch_id]: + batch_handle.write(f"{curr_frame}\n") + + return batch_lists + +if __name__ == "__main__": + print("++ cbatch_files_for_timeline.py") + + params = parse_options() + + batch_lists = write_batches(params) + + print(f"Done - wrote {len(batch_lists)} batch files") + diff --git a/compile_video.py b/compile_video.py index 156bc65..9950127 100644 --- a/compile_video.py +++ b/compile_video.py @@ -80,6 +80,7 @@ def get_image_sequence_in_directory(params): file_names.append(curr_file_name) return file_names + if __name__ == "__main__": print("++ compile_video.py") diff --git a/divemesh.py b/divemesh.py index eab6563..7e8c75c 100644 --- a/divemesh.py +++ b/divemesh.py @@ -62,17 +62,15 @@ def __getstate__(self): pickleInfo = self.__dict__.copy() # Currently, extraParams is NOT pickled # because we don't know which types the values are - del(pickleInfo['extraParams']) - - #pickleInfo['meshWidth'] = str(pickleInfo['meshWidth']) - #pickleInfo['meshHeight'] = str(pickleInfo['meshHeight']) + if 'extraParams' in pickleInfo: + del(pickleInfo['extraParams']) # Going to encode both the class name of the MathSupport, and # the 'precision' it was apparently set at, making a string # like "DiveMathSupportFlint:2048". mathSupportString = type(self.mathSupport).__name__ + ":" + str(self.mathSupport.precision()) pickleInfo['mathSupport'] = mathSupportString - + return pickleInfo def __setstate__(self, state): @@ -80,7 +78,7 @@ def __setstate__(self, state): NOTE: A new MathSupport sublass is instantiated during un-pickling. This can be a problem if you're relying on specific precision settings, because the only MathSupport configuration that's handled here - is setting the precision from the encode classname:precision string. + is setting the precision from the encoded 'classname:precision' string. It *is* important to set the precision before loading any numbers, or else the numbers may be clipped to lower precision than what they @@ -104,6 +102,7 @@ def getCenter(self): return self.mathSupport.createComplex(self.realMeshGenerator.valuesCenter, self.imagMeshGenerator.valuesCenter) def generateMesh(self): + #print(f"Mesh math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") realMesh = self.realMeshGenerator.generateForDiveMesh(self) imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) @@ -111,15 +110,17 @@ def generateMesh(self): raise ValueError("Real sub-mesh (%s) and Imaginary sub-mesh (%s) shapes don't match." % (realMesh.shape, imagMesh.shape)) meshShape = realMesh.shape + # The native python 'complex' type assigns into "object" type arrays + # without problems, but not vice-versa, so use object type for everything. combinedMesh = np.zeros(meshShape, dtype=object) - # The native python 'complex' type assigns into "object" type arrays without problems, - # but not vice-versa, so use object type for everything. - # numpyArray.shape returns (rows, columns) for y in range(0, meshShape[0]): for x in range(0, meshShape[1]): combinedMesh[y,x] = self.mathSupport.createComplex(realMesh[y,x], imagMesh[y,x]) + #print("mesh:") + #print(combinedMesh) + return combinedMesh def isUniform(self): @@ -187,6 +188,8 @@ class MeshGeneratorUniform(MeshGenerator): """ def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth): super().__init__(mathSupport, varyingAxis) + #print(f"making MeshGeneratorUniform with {mathSupport.digitsPrecision()} at center {valuesCenter}") + self.valuesCenter = valuesCenter self.baseWidth = baseWidth @@ -201,6 +204,7 @@ def __getstate__(self): def __setstate__(self, state): super().__setstate__(state) #print("mathSupport exists as: %s" % str(self.mathSupport)) + #print(f"__setstate__ math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") # Should probably be updating __dict__ instead? self.valuesCenter = self.mathSupport.createFloat(self.valuesCenter) self.baseWidth = self.mathSupport.createFloat(self.baseWidth) @@ -218,15 +222,18 @@ def generateForDiveMesh(self, diveMesh): """ mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + #print(f"generator math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") + if self.varyingAxis == 'width': #calculate start/end... Probably need to be subtype aware for this... discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshWidth) - #print("W baseWidth: %s meshWidth: %s" % (str(self.baseWidth), str(diveMesh.meshWidth))) + #print(f"W center: {self.valuesCenter} baseWidth: {str(self.baseWidth)} meshWidth: {str(diveMesh.meshWidth)}") #print("W: %s" % discretizedValues) + mesh[0:] = discretizedValues # Assign the one-row discretization to every row of the mesh else: # self.varyingAxis == 'height' discretizedValues = self.mathSupport.createLinspaceAroundValuesCenter(self.valuesCenter, self.baseWidth, diveMesh.meshHeight) - #print("H baseWidth: %s meshHeight: %s" % (str(self.baseWidth), str(diveMesh.meshHeight))) + #print(f"H center: {self.valuesCenter} baseWidth: {str(self.baseWidth)} meshHeight: {str(diveMesh.meshHeight)}") #print("H: %s" % discretizedValues) # Assign the one-row discretization (as a column) to every column of the mesh mesh[0:] = discretizedValues[:,np.newaxis] @@ -246,50 +253,159 @@ def __repr__(self): # like camera behavior, such as lens barrel distortion. class MeshGeneratorTilt(MeshGenerator): - def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth, tiltFactor): + def __init__(self, mathSupport, varyingAxis, valuesCenter, baseWidth, tiltAngle, tiltMaxRatio=10.0): """ Tilt is symmetric about the baseWidth. - Negative values aren't treated as negative factors, but instead as reversal - of the scaling direction (e.g. -2 means {.5,2.0}, and 2 => {2.0,.5}) - This means tilt values should not be between {-1.0,1.0}. - Originally thought this would need 2 axis parameters, but for now, requiring - the tilt axis to be the same as varying axis seems to make sense. + Using an "angle" here isn't really correct, but does seem to be an + intuitive and controllable analog, which allows an interpolation + across zero. With most (simpler) direct multiplication approaches, + the range from {-1.0,1.0} does strange things with continutity. + + Ranges from {-45,45} are acceptable (for now?). Didn't go with more + 'ordinary' ranges like 90 or 180, partly as a reminder that this + deviation isn't actually an angle, but instead maps a number across + the range defined by tiltMaxRatio. + + The tiltMaxRatio is the most that the first/last line is allowed to + differ from the baseWidth. + + Originally thought this would need 2 axis parameters, but for + now, requiring the tilt axis to be the same as varying axis seems + to make sense. Because the tilt factor is spread across the mesh rows, there might be some strong aliasing (across dive frames) if there's an even number - of rows or columns? I could imagine a back-and-forth wiggle developing if the - scales and factors line up the right way. - - Axis discretization happens for every frame, on 2 axes (across complex range, and across real range). - Mesh generation is the combination of these 2 axes (perhaps plus further post-processing modifications). - - A stretched axis (wider range), compared to the current frame's baseline, is akin to - calculating previous steps. For example, if you happen to stretch the axis as much as the previous - frame transition's zoom factor, then you're sorta recalculating the previous frame's axis again. - Similarly, a squished axis (narrower range), is akin to calculating future steps. + of rows or columns? I could imagine a back-and-forth wiggle developing + if the scales and factors line up the right way. + + Axis discretization happens for every frame, on 2 axes + (across complex range, and across real range). + Mesh generation is the combination of these 2 axes + (perhaps plus further post-processing modifications). + + A stretched axis (wider range), compared to the current frame's + baseline, is akin to calculating previous steps. For example, + if you happen to stretch the axis as much as the previous frame + transition's zoom factor, then you're sorta recalculating the + previous frame's axis again. + Similarly, a squished axis (narrower range), is akin to calculating + future steps. """ super().__init__(mathSupport, varyingAxis) self.valuesCenter = valuesCenter self.baseWidth = baseWidth - self.tiltFactor = tiltFactor + self.maxAbsTiltAngle = 45 + self.tiltAngle = tiltAngle - def generateForDiveMesh(self, diveMesh): - mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + self.tiltMaxRatio = tiltMaxRatio + self.safetyCheckTiltMaxRatio() + + def __getstate__(self): + pickleInfo = MeshGenerator.__getstate__(self) + + pickleInfo['valuesCenter'] = str(pickleInfo['valuesCenter']) + pickleInfo['baseWidth'] = str(pickleInfo['baseWidth']) + + return pickleInfo + def __setstate__(self, state): + super().__setstate__(state) + #print("mathSupport exists as: %s" % str(self.mathSupport)) + #print(f"__setstate__ math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") + # Should probably be updating __dict__ instead? + self.valuesCenter = self.mathSupport.createFloat(self.valuesCenter) + self.baseWidth = self.mathSupport.createFloat(self.baseWidth) + + def clampMaxAngle(self, paramAngle): + answerAngle = paramAngle + if answerAngle > self.maxAbsTiltAngle: + answerAngle = maxAbsTiltAngle + elif abs(answerAngle) > self.maxAbsTiltAngle: + answerAngle = -1 * self.maxAbsTiltAngle + return answerAngle + + def safetyCheckTiltMaxRatio(self): + """ + Can't be between {-1.0,1.0}, and we use abs() later, so + causes an error when max ratio is less than 1.0. + """ + if self.tiltMaxRatio < 1.0: + raise ValueError("ERROR - MeshGeneratorTilt can't have tiltMaxRatio under 1.0.") + + def getStartAndEndWidths(self): # Calculating only one side from the tiltFactor, then using the delta from # the original as the other side's size. This keeps our center in the linear # center of the ranges, instead of shifting it some amount dependent on the factor. # TODO: pretty sure this star/tend calculation needs to have its math done by # the MathSupport too, to keep type requirements localized there? - startWidth = 1.0 / self.tiltFactor * self.baseWidth - startDelta = self.baseWidth - startWidth - endWidth = self.baseWidth + startDelta - if self.tiltFactor < 0.0: - endWidth = 1.0 / self.tiltFactor * -1 * self.baseWidth + + self.safetyCheckTiltMaxRatio() + + safeAngle = self.clampMaxAngle(self.tiltAngle) + print(f"tilt {self.tiltAngle} => safe {safeAngle}") + + if abs(safeAngle) < 0.01: + # Effectively zero, or probably should be? + return (self.baseWidth, self.baseWidth) + + anglePercent = safeAngle / self.maxAbsTiltAngle + tiltRatioRange = self.tiltMaxRatio - 1.0 + + print(f"anglePercent {anglePercent}\ntiltRatioRange {tiltRatioRange}") + + if anglePercent > 0.0: + tiltRatio = 1.0 + (tiltRatioRange * anglePercent) + print(f"positive tiltRatio {tiltRatio}") + #startWidth = 1.0 / tiltRatio * self.baseWidth + startWidth = tiltRatio * self.baseWidth + startDelta = self.baseWidth - startWidth + print(f"positive startDelta {startDelta}") + endWidth = self.baseWidth + startDelta + return (startWidth, endWidth) + else: + tiltRatio = 1.0 + (tiltRatioRange * abs(anglePercent)) + print(f"negative tiltRatio {tiltRatio}") + endWidth = tiltRatio * self.baseWidth endDelta = self.baseWidth - endWidth + print(f"negative endDelta {endDelta}") startWidth = self.baseWidth + endDelta + return (startWidth, endWidth) + +# if anglePercent > 0.0: +# tiltRatio = 1.0 + (tiltRatioRange * anglePercent) +# print(f"positive tiltRatio {tiltRatio}") +# #startWidth = 1.0 / tiltRatio * self.baseWidth +# startWidth = 1.0 / tiltRatio * self.baseWidth +# startDelta = self.baseWidth - startWidth +# print(f"positive startDelta {startDelta}") +# endWidth = self.baseWidth + startDelta +# return (startWidth, endWidth) +# else: +# tiltRatio = (tiltRatioRange * anglePercent) +# print(f"negative tiltRatio {tiltRatio}") +# endWidth = 1.0 / tiltRatio * -1.0 * self.baseWidth +# endDelta = self.baseWidth - endWidth +# print(f"negative endDelta {endDelta}") +# startWidth = self.baseWidth + endDelta +# return (startWidth, endWidth) + +# Old version, right enough logic +# startWidth = 1.0 / self.tiltFactor * self.baseWidth +# startDelta = self.baseWidth - startWidth +# endWidth = self.baseWidth + startDelta +# if self.tiltFactor < 0.0: +# endWidth = 1.0 / self.tiltFactor * -1 * self.baseWidth +# endDelta = self.baseWidth - endWidth +# startWidth = self.baseWidth + endDelta + + + def generateForDiveMesh(self, diveMesh): + mesh = np.zeros((diveMesh.meshHeight, diveMesh.meshWidth), dtype=object) + + (startWidth, endWidth) = self.getStartAndEndWidths() + print(f"mesh startWidth: {startWidth} endWidth: {endWidth}") if self.varyingAxis == 'width': # Values vary along the width axis, and the tiltFactor is applied to the @@ -312,3 +428,18 @@ def __repr__(self): return """\ [MeshGeneratorTilt center:{vCenter} baseWidth:{vWidth} tiltFactor:{tilt} along axis:'{vAxis}']\ """.format(vCenter=self.valuesCenter, vWidth=self.baseWidth, tilt=self.tiltFactor, vAxis=self.varyingAxis) + +class MeshMarker: + """ + Rather than overloading DiveMesh's extra_params, or stashing stuff + in the mesh, this thin-ish wrapper is intended to hold the 'extra' + values and variables from exploration/layout so we can keep + everything persistent in one area. + """ + def __init__(self, diveMesh, markerNumber, algorithmName, maxEscapeIterations): + self.diveMesh = diveMesh + self.markerNumber = markerNumber + self.algorithmName = algorithmName + self.maxEscapeIterations = maxEscapeIterations + + diff --git a/fractal.py b/fractal.py index f8efbdd..dc303fa 100644 --- a/fractal.py +++ b/fractal.py @@ -54,303 +54,23 @@ from julia_solo import JuliaSolo from julia_smooth import JuliaSmooth -#from smooth import Smooth - MANDL_VER = "0.1" -class FractalContext: - """ - The context for a single dive - """ - - def __init__(self, math_support=fm.DiveMathSupport()): - self.math_support = math_support - - self.img_width = 0 # int : Wide of Image in pixels - self.img_height = 0 # int - - self.cmplx_width = self.math_support.createFloat(0.0) - self.cmplx_height = self.math_support.createFloat(0.0) - self.cmplx_center = self.math_support.createComplex(0.0) # center of image in complex plane - - self.max_iter = 0 # int max iterations before bailing - self.escape_rad = 2. # float radius mod Z hits before it "escapes" - - self.scaling_factor = 0.0 # float amount to zoom each epoch - - self.set_zoom_level = 0 # Zoom in prior to the dive - self.clip_start_frame = -1 - self.clip_frame_count = 1 - self.write_video = True - - self.smoothing = False # bool turn on color smoothing - self.snapshot = False # Generate a single, high res shotb - - self.julia_list = None # Used just for timeline construction - - self.palette = None - - # Shifting from the FractalContext being the oracle of frame information, to the Timeline being the oracle. - # Rather than keeping 'current frame' info in the context, we just keep the timeline, and - # query it for frame-specific parameters to render with. - self.timeline = None - - self.project_name = 'default_project' - self.shared_cache_path = 'shared_cache' - self.build_cache = False - self.invalidate_cache = False - - self.algorithm_map = {'julia' : Julia, - 'mandelbrot_solo' : MandelbrotSolo, - 'mandelbrot_smooth' : MandelbrotSmooth, - 'mandeldistance' : MandelDistance, - 'smooth': Smooth} - self.algorithm_name = None - self.algorithm_extra_params = {} # Keeps command-line params for later use - - self.verbose = 0 # how much to print about progress - - def render_frame_number(self, frame_number): - extra_params = {} - if self.algorithm_name in self.algorithm_extra_params: - extra_params = self.algorithm_extra_params[self.algorithm_name] - - #dive_mesh realizes more info is needed, so stashes it into its extraParams - # Gotta retrieve that in calculateResults, right? - # So, extra_params needs to be looked at, and algo params need to be set by the values, right? - # Algorithm gets instantiated with all its parts in place... - # We've gotta allow mesh's extra params to add to and override the overall algorithm's extra params? - - dive_mesh = self.timeline.getMeshForFrame(frame_number) - #print("extra params from mesh: %s" % str(dive_mesh.extraParams)) - extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? - - display_frame_number = frame_number - if self.clip_start_frame != -1: - display_frame_number += self.clip_start_frame - - #print("extra params for \"%s\" instantiation: %s" % (self.algorithm_name, str(extra_params))) - #print(".") # Just to keep the time-per-frame calculations from being overwritten in the terminal - frame_algorithm = self.algorithm_map[self.algorithm_name](dive_mesh=dive_mesh, frame_number=display_frame_number, output_folder_name=self.project_name, extra_params=extra_params) - - frame_algorithm.run() - return frame_algorithm.output_image_file_name - - def __repr__(self): - return """\ -[FractalContext Img W:{w:d} Img H:{h:d} Cmplx W:{cw:s} -Cmplx H:{ch:s} Complx Center:{cc:s} Scaling:{s:f} Max iter:{mx:d}]\ -""".format( - w=self.img_width,h=self.img_height,cw=str(self.cmplx_width),ch=str(self.cmplx_height), - cc=str(self.cmplx_center),s=self.scaling_factor,mx=self.max_iter); - -class MediaView: - """ - Handle displaying to gif / mp4 / screen etc. - """ - def make_frame(self, t): - return np.array(self.ctx.render_frame_number(self.frame_number_from_time(t))) - - def __init__(self, duration, fps, ctx): - self.duration = duration - self.fps = fps - self.ctx = ctx - self.banner = False - self.vfilename = None - - def frame_number_from_time(self, t): - return math.floor(self.fps * t) - - def time_from_frame_number(self, frame_number): - return frame_number / self.fps - - def intro_banner(self): - # Generate a text clip - w,h = self.ctx.img_width, self.ctx.img_height - banner_text = u"%dx%d center %s duration=%d fps=%d" %\ - (w, h, str(self.ctx.cmplx_center), self.duration, self.fps) - - - txt = mpy.TextClip(banner_text, font='Amiri-regular', - color='white',fontsize=12) - - txt_col = txt.on_color(size=(w + txt.w,txt.h+6), - color=(0,0,0), pos=(1,'center'), col_opacity=0.6) - - txt_mov = txt_col.set_position((0,h-txt_col.h)).set_duration(4) - - return mpy.CompositeVideoClip([self.clip,txt_mov]).subclip(0,self.duration) - - def create_snapshot(self): - - if not self.vfilename: - self.vfilename = "snapshot.gif" - - self.ctx.next_epoch(-1,self.vfilename) - - - # -- - # Do any setup needed prior to running the calculatiion loop - # -- - - def setup(self): - - print(self) - #print(self.ctx) - - -# def runTimeline(self, timeline): -# movieClip = mpy.VideoClip(self.make_frame, - - def run(self): - - if self.ctx.snapshot == True: - self.create_snapshot() - return - - self.ctx.timeline = self.construct_simple_timeline() - # Duration may be less than overall, if this is a sub-clip, so - # figure out what our REAL duration is. - frame_file_names = [] - timeline_frame_count = self.ctx.timeline.getTotalSpanFrameCount() - for curr_frame_number in range(timeline_frame_count): - frame_file_names.append(self.ctx.render_frame_number(curr_frame_number)) - - timeline_duration = self.time_from_frame_number(timeline_frame_count) - - self.clip = mpy.ImageSequenceClip(frame_file_names, fps = self.ctx.timeline.framerate) - - if self.ctx.write_video == True: - if self.banner: - self.clip = self.intro_banner() - - # if not self.vfilename: - # self.clip.preview(fps=1) #fps 1 is really all that works - if self.vfilename.endswith(".gif"): - #print("fps: %s" % str(self.fps)) - #self.clip.write_gif(self.vfilename, fps=self.fps) - print("fps: %s" % str(self.ctx.timeline.framerate)) - self.clip.write_gif(self.vfilename, fps=self.ctx.timeline.framerate) - elif self.vfilename.endswith(".mp4"): - self.clip.write_videofile(self.vfilename, - fps=self.fps, - audio=False, - codec="mpeg4") - else: - print("Error: file extension not supported, must be gif or mp4") - sys.exit(0) - - - def construct_simple_timeline(self): - """ - Transitional. - - Basically, let's construct a timeline from one set of start/end points, - as defined by the current context. - """ - overall_zoom_factor = self.ctx.scaling_factor - overall_frame_count = self.duration * self.fps - # Integers only, but be generous and round up? - if math.floor(overall_frame_count) != overall_frame_count: - overall_frame_count = math.floor(overall_frame_count) + 1 - - clip_start_frame = 0 - last_frame_number = overall_frame_count - 1 - if self.ctx.clip_start_frame != -1: - # if start frame is defined, then rely on frame count - # also being valid - clip_start_frame = self.ctx.clip_start_frame; - last_frame_number = clip_start_frame + self.ctx.clip_frame_count - 1 - - # So we know the overall trajectory of window zoom, and we know - # the frames we're actually trying to render. - rendered_frame_count = last_frame_number - clip_start_frame + 1 - - print("clip_start_frame: %d rendered_frame_count: %d" % (clip_start_frame, rendered_frame_count)) - - if last_frame_number > overall_frame_count: - raise ValueError("Can't construct timeline of %d frames, starting at frame %d, for a sequence of only %d frames (%f seconds at %f fps) (%d)" % (rendered_frame_count, clip_start_frame, overall_frame_count, self.duration, self.fps, last_frame_number)) - - if clip_start_frame == 0: - start_width_real = self.ctx.cmplx_width - start_width_imag = self.ctx.cmplx_height - else: - # Start frame is 1 or greater, the exponent here should be okay. - start_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, clip_start_frame) - start_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, clip_start_frame) - - end_width_real = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_width, overall_zoom_factor, last_frame_number) - end_width_imag = self.ctx.math_support.scaleValueByFactorForIterations(self.ctx.cmplx_height, overall_zoom_factor, last_frame_number) - - print("Timeline ranges: {%s,%s} -> {%s,%s} in %d frames" % (str(start_width_real), str(start_width_imag), str(end_width_real), str(end_width_imag), rendered_frame_count)) - - timeline = DiveTimeline(projectFolderName=self.ctx.project_name, algorithm_name=self.ctx.algorithm_name, framerate=self.fps, frameWidth=self.ctx.img_width, frameHeight=self.ctx.img_height, mathSupport=self.ctx.math_support, sharedCachePath=self.ctx.shared_cache_path) - - if timeline.algorithm_name == 'julia': - # Just evenly divide the waypoints across the time for a simple timeline - keyframeCount = len(self.ctx.julia_list) - # 2 keyframes over 10 frames = 10 frames per keyframe - keyframeSpacing = math.floor(rendered_frame_count / (keyframeCount - 1)) - if keyframeSpacing < 1: - raise ValueError("Can't construct julia walk with more waypoints than animation frames") - - span = DiveTimelineSpan(timeline, rendered_frame_count) - span.addNewWindowKeyframe(0, start_width_real, start_width_imag) - span.addNewWindowKeyframe(rendered_frame_count - 1, end_width_real, end_width_imag) - span.addNewUniformKeyframe(0) - span.addNewUniformKeyframe(rendered_frame_count - 1) - - span.addNewCenterKeyframe(0, self.ctx.cmplx_center) - span.addNewCenterKeyframe(rendered_frame_count - 1, self.ctx.cmplx_center) - - currKeyframeFrameNumber = 0 - for currJuliaCenter in self.ctx.julia_list: - # Recognize when we're at the last item, and jump that keyframe to the final frame - if currKeyframeFrameNumber + keyframeSpacing > rendered_frame_count - 1: - currKeyframeNumber = rendered_frame_count - 1 - - span.addNewComplexParameterKeyframe(currKeyframeFrameNumber, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') - currKeyframeFrameNumber += keyframeSpacing - - timeline.timelineSpans.append(span) - - else: - #print("Trying to make span of %d frames" % frame_count) - span = timeline.addNewSpanAtEnd(rendered_frame_count, self.ctx.cmplx_center, start_width_real, start_width_imag, end_width_real, end_width_imag) - - #perspectiveFrame = math.floor(frame_count * .5) - #span.addNewTiltKeyframe(perspectiveFrame, 4.0, 1.0) - - return timeline - - def __repr__(self): - return """\ -[MediaView duration {du:f} FPS:{f:s} Output:{vf:s}]\ -""".format(du=self.duration,f=str(self.fps),vf=str(self.vfilename)) - - class DiveTimeline: """ - Representation of an edit timeline. This maps parameters to specific frame numbers. - A timeline also is the keeper of framerate for a frame sequence. (I think?) - - For now, the timeline also maintains the calculation cache for every frame, though - this may eventually be the responsibility of a 'Project' + Representation of an edit timeline. This maps parameters to specific + times, which can be used for generating all the frames of a sequence. + + The first span is a special 'main' span which defines the run time + of the timeline, regardless of the properties of any other spans. Overview of the sequencing classes ---------------------------------- DiveTimeline DiveTimelineSpan (Basis for setting keyframes) DiveSpanKeyframe - - DiveSpanCenterKeyframe - - DiveSpanWindowKeyframe - - DiveSpanUniformKeyframe - - DiveSpanTiltKeyframe + - DiveSpanCustomKeyframe (not implemented yet) - There are currently only 3 'tracks' of keyframes (for complex - center, window base sizes, and perspective). Keyframes - currently all live on integer frame numbers. - # TODO: Seems like algorithm should be a per-span property, instead of # per-timeline, doesn't it? """ @@ -365,10 +85,16 @@ def algorithm_map(): #'smooth': Smooth, } - def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, frameHeight, mathSupport): + @staticmethod + def build_from_json_and_params(json, params): + newTimeline = DiveTimeline(params['project_name'], json['algorithmName'], float(json['framerate']), int(json['frameWidth']), int(json['frameHeight']), None) + newTimeline.__setstate__(json) + return newTimeline + + def __init__(self, projectFolderName, algorithmName, framerate, frameWidth, frameHeight, mathSupport): self.projectFolderName = projectFolderName - self.algorithm_name = algorithm_name + self.algorithmName = algorithmName self.framerate = float(framerate) self.frameWidth = int(frameWidth) @@ -379,988 +105,443 @@ def __init__(self, projectFolderName, algorithm_name, framerate, frameWidth, fra # No definition made yet for edit gaps, so let's just enforce adjacency of ranges for now. self.timelineSpans = [] - def getTotalSpanFrameCount(self): - seenFrameCount = 0 - for currSpan in self.timelineSpans: - seenFrameCount += currSpan.frameCount - return seenFrameCount - - def addNewSpanAtEnd(self, frameCount, center, startWidthReal, startWidthImag, endWidthReal, endWidthImag): - """ - Constructs a new span, and adds it to the end of the existing span list - - Also adds center keyframes (that is, keyframes at the end of the span, which - set the values for the complex center of the image), and window width keyframes - to the start and end of the new span. - - Apparently also adding perspective keyframes too. - """ - span = DiveTimelineSpan(self, frameCount) - span.addNewCenterKeyframe(0, center, 'quadratic-to', 'quadratic-to') - span.addNewCenterKeyframe(frameCount - 1, center, 'quadratic-to', 'quadratic-to') - span.addNewWindowKeyframe(0, startWidthReal, startWidthImag) - span.addNewWindowKeyframe(frameCount - 1, endWidthReal, endWidthImag) + def getFramesInDuration(self, duration): + # Use 6 decimal places for rounding down, but otherwise, round up. + # 1 second * 24 frames / second = 24 frames + # .041 seconds * 24 frames / second = .984 frames (should be 1) + # .042 seconds * 24 frames / second = 1.008 frames (should be 2) + # .041666 * 24 frames / second = .999984 frames (should be 1) + # .04166666 * 24 frames / second = .99999984 frames (should be 1) + # .04166667 * 24 frames / second = 1.00000008 frames (should be 1) + # .0416667 * 24 frames / second = 1.0000008 frames (should be 2) + rawCount = (duration / 1000) * self.framerate + framesCount = int(rawCount) + remainderCount = rawCount - framesCount + + if remainderCount != 0.0 and round(remainderCount,6) != 0.0: + framesCount += 1 + + return framesCount + + def getTimeForFrameNumber(self, frameNumber): + # Want to return max time when within 1 frame, right? To be nice. + # When 9 frames, the frames are 0-8 + # That's 9 positions, with 8 deltas. + rawTime = frameNumber * 1000 * (1 / self.framerate) + return int(rawTime) + + def getMainSpan(self): + if len(self.timelineSpans) == 0: + return None + else: + return self.timelineSpans[0] - # Setting uniform perspective this way kinda taped on as a solution, but not yet - # sure how to more gracefully set up perspective keyframes. - span.addNewUniformKeyframe(0) - span.addNewUniformKeyframe(frameCount-1) + def getTotalSpanFrameCount(self): + # Duration + # Span durations are exclusive, so a duration of 100 means it covers 0-99. + mainSpan = self.getMainSpan() + if mainSpan != None: + return self.getFramesInDuration(mainSpan.duration) + else: + return 0 + def addNewSpan(self, time, duration): + span = DiveTimelineSpan(self, time, duration) self.timelineSpans.append(span) + return span - return span - - def getSpanForFrameNumber(self, frameNumber): - """ - Seems like I should cache or memoize this, to keep from searching for every frame, - or at least binary search it, but I'm allergic to optimizing before profiling, - so 'slow' search it is for now. - - 10 frames -> {0,9} - 3 frames -> {10,12} - 10 frames -> {13,22} - """ - nextSpanFirstFrame = 0 + def getSpansForTime(self, targetTime): + overlappingSpans = [] for currSpan in self.timelineSpans: - nextSpanFirstFrame += currSpan.frameCount - if frameNumber < nextSpanFirstFrame: - currSpan.lastObservedStartFrame = nextSpanFirstFrame - currSpan.frameCount - return currSpan - - return None # Went past the end without finding a valid span, so it's too high a frame number - - def getMeshForFrame(self, frameNumber): + if currSpan.time <= targetTime and (currSpan.time + currSpan.duration) >= targetTime: + overlappingSpans.append(currSpan) + return overlappingSpans + + def getMeshForTime(self, targetTime): """ Calculate a discretized 2d plane of complex points based on spans and keyframes in this timeline. - # Haven't revisited this logic since building the MeshGenerator objects... Order of operations: - 1.) base_complex_center - 2.) distortions on base_complex_center (probably never really want this, but hey, it makes sense here) - 3.) MeshGenerator distortions? - 4.) overall distortions on the calculated 2D mesh + 1.) base window definitions + 2.) distortions on generators + 3.) overall distortions on the calculated 2D mesh + """ + targetSpans = self.getSpansForTime(targetTime) + + # Enforce that the 'main' span (the first span) at least, was returned + if len(targetSpans) == 0 or targetSpans[0] != self.timelineSpans[0]: + raise IndexError("Time '%d' (milliseconds) is out of range for this timeline" % targetTime) + + # Build up the set of parameter values, as interpolated between nearest + # surrounding keyframes. + # Take one entire span at a time, before moving to the next span. + # Deny a second span setting an already existing parameter directly. + # To allow a following span to adjust the setting, its target must be + # named as 'parameterName_modifiyPlus' or 'parameterName_modifyTimes'. + plusSuffix = "_modifyPlus" + timesSuffix = "_modifyTimes" + + parameterValues = {} + for currSpan in targetSpans: + spanParamValues = currSpan.getParamValuesAtTime(targetTime, parameterValues) + for newParamName in spanParamValues: + #print(f"Looking at parameter name \"{newParamName}\"") + if newParamName in parameterValues: + raise ValueError(f"Spans can't overwrite existing parameters, so can't set the value of \"{newParamName}\" from \"{parameterValues[newParamName]}\" to \"{spanParamValues[newParamName]}\". You might be trying to modify an existing value, in which case it could be done with a parameter named \"{newParamName}_modifyPlus\".") + + elif newParamName.endswith(plusSuffix): + realParamName = newParamName[:-len(plusSuffix)] + #print(f"PLUS maps to \"{realParamName}\"") + if realParamName not in parameterValues: + print(f"WARNING - span's attempt to modify \"{realParamName}\" failed, because it wasn't previously defined") + else: + parameterValues[realParamName] += spanParamValues[newParamName] + elif newParamName.endswith(timesSuffix): + realParamName = newParamName[:-len(timesSuffix)] + #print(f"TIMES maps to \"{realParamName}\"") + if realParamName not in parameterValues: + print(f"WARNING - span's attempt to modify \"{realParamName}\" failed, because it wasn't previously defined") + else: + parameterValues[realParamName] *= spanParamValues[newParamName] + else: + #print(f"just assigning param \"{newParamName}\"") + parameterValues[newParamName] = spanParamValues[newParamName] + + # Use the parameter values needed for constructing the mesh window, + # and REMOVE them from the parameter list, leaving only 'extra' parameters. + # + # I suppose just trying to access non-existent params here will be enough + # to trigger a runtime warning if they weren't defined, so not adding + # any extra guards for now. + meshCenter = parameterValues['meshCenter'] + del(parameterValues['meshCenter']) + + meshRealWidth = parameterValues['meshRealWidth'] + del(parameterValues['meshRealWidth']) + + meshImagWidth = parameterValues['meshImagWidth'] + del(parameterValues['meshImagWidth']) + + # Not strictly required, and 0.0 tilt (uniform) is assumed if not specified. + meshRealTilt = 0.0 + meshImagTilt = 0.0 + if 'meshRealTilt' in parameterValues: + meshRealTilt = parameterValues['meshRealTilt'] + del(parameterValues['meshRealTilt']) + if 'meshImagTilt' in parameterValues: + meshImagTilt = parameterValues['meshImagTilt'] + del(parameterValues['meshImagTilt']) + + realMeshGenerator = None + imagMeshGenerator = None - Procedurally calculate the discretized 2D plane of complex points, based on all - the known keyframes and modifiers for a given frame number... + #print(f"parameters near the end of getMeshForTime(): \"{parameterValues}\"") - # complexCenter, complexWidth, imaginaryWidth - - """ - # First step is to figure out which span the target frame belongs to - targetSpan = self.getSpanForFrameNumber(frameNumber) - if not targetSpan: - raise IndexError("Frame number '%d' is out of range for this timeline" % frameNumber) - - # Within this span, find the closest upstream and closest downstream keyframes - # Pretty sure we require least 2 keyframes defined for every span (start and end), so - # this should work out. - localFrameNumber = frameNumber - targetSpan.lastObservedStartFrame - (previousCenterKeyframe, nextCenterKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('center', localFrameNumber) - #print("centers %s -> %s" % (str(previousCenterKeyframe.center), str(nextCenterKeyframe.center))) - - meshCenterValue = targetSpan.interpolateCenterValueBetweenKeyframes(localFrameNumber, previousCenterKeyframe, nextCenterKeyframe) - #print(" interpolatedCenter: %s" % meshCenterValue) - - (previousWindowKeyframe, nextWindowKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('window', localFrameNumber) - #print("windows %s,%s -> %s,%s" % (str(previousWindowKeyframe.realWidth), str(previousWindowKeyframe.imagWidth), str(nextWindowKeyframe.realWidth), str(nextWindowKeyframe.imagWidth))) - - (baseWidthReal, baseWidthImag) = targetSpan.interpolateWindowValuesBetweenKeyframes(localFrameNumber, previousWindowKeyframe, nextWindowKeyframe) - - #print(" interpolatedWidths: %s, %s" % (str(baseWidthReal), str(baseWidthImag))) - - (previousPerspectiveKeyframe, nextPerspectiveKeyframe) = targetSpan.getKeyframesClosestToFrameNumber('perspective', localFrameNumber) - - # More complicated with perspective keyframes, right? - # Which mesh generator do we use - Uniform or Tilt? - - # Might have to interpolate the (widthFactor,heightFactor) of a tilt keyframe... - # If both keyframes are uniform, then we don't actually interpolate - # Hacky isisntance, but whatcha gonna do? - previousIsUniform = isinstance(previousPerspectiveKeyframe, DiveSpanUniformKeyframe) - nextIsUniform = isinstance(nextPerspectiveKeyframe, DiveSpanUniformKeyframe) - if previousIsUniform and nextIsUniform: - # Might feel like "baseImagWidth" is a typo (because it's distributed vertically), but - # it's the 'imaginary width', even though we use it as the vertical element in the final mesh - realMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal) - imagMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag) + if meshRealTilt == 0.0: + realMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenter.real, baseWidth=meshRealWidth) else: - (widthTiltFactor, heightTiltFactor) = self.interpolateTiltFactorsBetweenPerspectiveKeyframes(localFrameNumber, previousPerspectiveKeyframe, nextPerspectiveKeyframe) - - # Tilt factor is the multiplier applied to the range. - realMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenterValue.real, baseWidth=baseWidthReal, tiltFactor=widthTiltFactor) - imagMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenterValue.imag, baseWidth=baseWidthImag, tiltFactor=heightTiltFactor) - - extraFrameParams = {} - parameterKeyframePairs = targetSpan.getParameterKeyframePairsClosestToFrameNumber('complex', localFrameNumber) - # parameterKeyframePairs['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] - for currParamName, currKeyframePairs in parameterKeyframePairs.items(): - for (leftKeyframe, rightKeyframe) in currKeyframePairs: - extraFrameParams[currParamName] = targetSpan.interpolateComplexBetweenParameterKeyframes(localFrameNumber, leftKeyframe, rightKeyframe) - - parameterKeyframePairs = targetSpan.getParameterKeyframePairsClosestToFrameNumber('float', localFrameNumber) - # parameterKeyframePairs['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] - for currParamName, currKeyframePairs in parameterKeyframePairs.items(): - for (leftKeyframe, rightKeyframe) in currKeyframePairs: - extraFrameParams[currParamName] = self.interpolateFloatBetweenParameterKeyframes(localFrameNumber, leftKeyframe, rightKeyframe) - - diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, realMeshGenerator, imagMeshGenerator, self.mathSupport, extraFrameParams) - #print (diveMesh) - return diveMesh + realMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenter.real, baseWidth=meshRealWidth, tiltAngle=float(meshRealTilt)) - def interpolateTiltFactorsBetweenPerspectiveKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): - """ - First part of this repeats some hacky isinstance stuff. + if meshImagTilt == 0.0: + imagMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenter.imag, baseWidth=meshImagWidth) + else: + imagMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='height', valuesCenter=meshCenter.imag, baseWidth=meshImagWidth, tiltAngle=float(meshImagTilt)) + + #print(f"Making mesh with realWidth {meshRealWidth} and imagWidth {meshImagWidth}") + diveMesh = mesh.DiveMesh(self.frameWidth, self.frameHeight, realMeshGenerator, imagMeshGenerator, self.mathSupport, parameterValues) - But this time, we know they BOTH won't be uniform, else we never would try to - interpolate between them. - """ - if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: - raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) - - # Hacky isisntance, but whatcha gonna do? - leftIsUniform = isinstance(leftKeyframe, DiveSpanUniformKeyframe) - rightIsUniform = isinstance(rightKeyframe, DiveSpanUniformKeyframe) - - leftFrameNumber = leftKeyframe.lastObservedFrameNumber - rightFrameNumber = rightKeyframe.lastObservedFrameNumber - - # Use 1.0 as default widthFactor and heightFactor for uniform keyframes. - if leftIsUniform: - transitionType = rightKeyframe.transitionIn - leftWidthValue = 1.0 - leftHeightValue = 1.0 - rightWidthValue = rightKeyframe.widthFactor - rightHeightValue = rightKeyframe.heightFactor - elif rightIsUniform: - transitionType = leftKeyframe.transitionOut - leftWidthValue = leftKeyframe.widthFactor - leftHeightValue = leftKeyframe.heightFactor - rightWidthValue = 1.0 - rightHeightValue = 1.0 - else: # both are not uniform - # TODO: probably should enforce same transition type or crash here, but - # I don't feel like it at the moment. - transitionType = leftKeyframe.transitionOut - leftWidthValue = leftKeyframe.widthFactor - leftHeightValue = leftKeyframe.heightFactor - rightWidthValue = rightKeyframe.widthFactor - rightHeightValue = rightKeyframe.heightFactor - - widthTiltFactor = self.mathSupport.interpolate(transitionType, leftFrameNumber, leftWidthValue, rightFrameNumber, rightWidthValue, frameNumber) - heightTiltFactor = self.mathSupport.interpolate(transitionType, leftFrameNumber, leftHeightValue, rightFrameNumber, rightHeightValue, frameNumber) - - return(widthTiltFactor, heightTiltFactor) - -# Maybe these belong in timeline span? -# def getFrameNumberForTimecode(self, timecode): -# return math.floor( -# def getTimecodeForFrameNumber(self, frame_number): + #print(diveMesh) + return diveMesh - def __repr__(self): - return """\ -[DiveTimeline Project:{proj} framerate:{f}]\ -""".format(proj=self.title,f=self.framerate) + def __getstate__(self): + """ Pickle encoding helper, generates simply encoded keyframes. """ + pickleInfo = self.__dict__.copy() + + # Not storing project folder in timeline file, because + # that's parameters and param file's responsibility + del(pickleInfo['projectFolderName']) + + # Going to encode both the class name of the MathSupport, and + # the 'precision' it was apparently set at, making a string + # like "DiveMathSupportFlint:2048". + mathSupportString = type(self.mathSupport).__name__ + ":" + str(self.mathSupport.precision()) + pickleInfo['mathSupport'] = mathSupportString + + convertedSpans = [] + for currSpan in self.timelineSpans: + convertedSpans.append(currSpan.__getstate__()) + pickleInfo['timelineSpans'] = convertedSpans -class DiveTimelineSpan: - """ - # ?(Can be used to observe/calculate n-1 zoom factors.)? - """ - def __init__(self, timeline, frameCount): - self.timeline = timeline - self.frameCount = int(frameCount) + return pickleInfo - # Only a single 'track' for each keyframe type to begin with, represented - # just as a keyframe lookup for each track. Being able to stack multiples of - # similar-typed keyframes will probably be helpful in the long run. - self.centerKeyframes = {} - self.windowKeyframes = {} - self.perspectiveKeyframes = {} + def __setstate__(self, state): + """ + (Just like in divemesh.py...) + A new MathSupport sublass is instantiated during un-pickling. + This can be a problem if you're relying on specific precision settings, + because the only MathSupport configuration that's handled here + is setting the precision from the encode classname:precision string. + + It *is* important to set the precision before loading any numbers, or + else the numbers may be clipped to lower precision than what they + were saved at. + """ + mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, + "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupport":fm.DiveMathSupport} - self.complexParameterKeyframes = defaultdict(dict) - self.floatParameterKeyframes = defaultdict(dict) - # parameterKeyframes['julia_center'][25] = complex(0,0) - # parameterKeyframes['julia_center'][40] = complex(0,0) + (mathSupportClassName, precisionString) = state['mathSupport'].split(':') + #print("mathSupport reads as: %s" % mathSupportClassName) - # Currently, not allowing keyframes to exist outside of the span, even though - # that is often helpful for defining pleasing transitions. - # Currently, also not allowing keyframes to exist at non-frame targets, which - # might lead to some alignment frustrations, because sub-frame calculations - # are probably kinda important. + self.mathSupport = mathSupportClasses[mathSupportClassName]() + self.mathSupport.setPrecision(int(precisionString)) + #print("mathSupport is: %s" % str(self.mathSupport)) + #print(f"timeline's mathSupport set to {self.mathSupport.digitsPrecision()} digits") - self.lastObservedStartFrame = 0 # To stash frame offset when extracted from Timeline + #self.projectFolderName = state['projectFolderName'] + self.algorithmName = state['algorithmName'] + self.framerate = float(state['framerate']) + self.frameWidth = int(state['frameWidth']) + self.frameHeight = int(state['frameHeight']) - #### - # TODO: All of these helper functions need to perform a modification of upstream and downsatream - # keyframes when forcing addition, to make sure the transition types are all in order. - # When creating a new keyframe, default is to inherit the lead-in and lead-out interpolators of the existing span. - # - # So, if it's: - # | (lin) | (default all linear) - # - # | +(unspec)K(unspec) | - # | (lin) K (lin) | - # - # | +(log-to)K(unspec) | - # | (log-to) K (lin) | - # - # | +(log-to)K(log-from) | - # | (log-to) K (log-from) | - # - # When trying to fill the range, both adjacent keyframes will agree (because it's a span property). - # I think this means when setting a keyframe, the upstream and downstream keyframes are ALWAYS SET - # to be consistent with the newly inserted keyframe. (either that, or create single-point-of-reference span structure?) - # - # BaseRangeSpan(startKey, endKey, type=lin) - # +Keyframe(log-from) => - # BaseRangeSpan(startKey, keyframe, type=lin) + BaseRangeSpan(keyframe, endkey, type=log-from) - # - # Now, some asshole wants to define a 'speed' for a section... - # This will drag some keyframes along, up to a point, where it can. - # It might even just warn if reverse is observed? - # - # Looks like we need special handling for when setting a keyframe on top of an existig keyframe, right? - # If existing keyframe at the new keyframe frane number, and new keyframe is unspecified, then - # keep the interpolators as-is. - - def addNewCenterKeyframe(self, frameNumber, centerValue, transitionIn='quadratic-to', transitionOut='quadratic-to'): - # TODO: should probably gracefully handle stomping an existing keyframe, right? - newKeyframe = DiveSpanCenterKeyframe(self, centerValue, transitionIn, transitionOut) - self.centerKeyframes[frameNumber] = newKeyframe - return newKeyframe + for storedSpanInfo in state['timelineSpans']: + newSpan = self.addNewSpan(storedSpanInfo['time'], storedSpanInfo['duration']) + newSpan.__setstate__(storedSpanInfo) - def addNewWindowKeyframe(self, frameNumber, realWidth, imagWidth, transitionIn='root-to', transitionOut='root-to'): - newKeyframe = DiveSpanWindowKeyframe(self, realWidth, imagWidth, transitionIn, transitionOut) - self.windowKeyframes[frameNumber] = newKeyframe - return newKeyframe - def addNewUniformKeyframe(self, frameNumber, transitionIn='quadratic-to', transitionOut='quadratic-to'): - newKeyframe = DiveSpanUniformKeyframe(self, transitionIn, transitionOut) - self.perspectiveKeyframes[frameNumber] = newKeyframe - return newKeyframe +class DiveTimelineSpan: + """ - def addNewTiltKeyframe(self, frameNumber, widthFactor, heightFactor, transitionIn='quadratic-to-from', transitionOut='quadratic-to-from'): - newKeyframe = DiveSpanTiltKeyframe(self, widthFactor, heightFactor, transitionIn, transitionOut) - self.perspectiveKeyframes[frameNumber] = newKeyframe - return newKeyframe + """ + def __init__(self, timeline, timePosition, duration): + self.timeline = timeline + self.time = int(timePosition) + self.duration = int(duration) - def addNewComplexParameterKeyframe(self, frameNumber, name, value, transitionIn='linear', transitionOut='linear'): - # TODO: Would like to be unable to assign keyframes with identical - # names to both the complex and float parameter sets. - newKeyframe = DiveSpanParameterKeyframe(self, value, transitionIn, transitionOut) - self.complexParameterKeyframes[name][frameNumber] = newKeyframe - # parameterKeyframes['julia_center'][40] = complex(0,0) - return newKeyframe + # Only a single 'track' for each keyframe type, so each named + # parameter can only have one keyframe for one time stamp within + # a single span. Spans can layer though. + self.parameterKeyframes = defaultdict(dict) + # parameterKeyframes['meshRealWidth'][2300] = keyframeObject - def addNewFloatParameterKeyframe(self, frameNumber, name, value, transitionIn='linear', transitionOut='linear'): - newKeyframe = DiveSpanParameterKeyframe(self, value, transitionIn, transitionOut) - self.floatParameterKeyframes[name][frameNumber] = newKeyframe - # parameterKeyframes['a_value'][40] = 1.234 - return newKeyframe - - def getKeyframesClosestToFrameNumber(self, keyframeType, frameNumber): + def getParamValuesAtTime(self, targetTime, currentParameters): """ - Returns a tuple of keyframes, which are the nearest left and right keyframes for the frameNumber. - When the frameNumber directly has a keyframe, the same keyframe is returned for both values. - - Important to remember to bless the 'lastObservedFrameNumber' into the keyframe. + The returned set of parameters are only the parameters from this + span, and do not automatically include values from the + currentParameters input. + + Current parameters are used only as additional information for calculations. """ - typeOptions = ['center', 'window', 'perspective'] - if keyframeType not in typeOptions: - raise ValueError("keyframeType must be one of (%s)" % ", ".join(typeOptions)) - - if frameNumber >= self.frameCount: - raise IndexError("Requested %s keyframe frame number '%d' is out of range for a span that's '%d' frames long" % (keyframeType, frameNumber, self.frameCount)) - - keyframeHash = None - if keyframeType == 'perspective': - keyframeHash = self.perspectiveKeyframes - elif keyframeType == 'window': - keyframeHash = self.windowKeyframes - else: # keyframeType == 'center': - keyframeHash = self.centerKeyframes - - # Direct hit - # (really should have non-integer locations for keyframes, shouldn't we?) - if frameNumber in keyframeHash: - targetKeyframe = keyframeHash[frameNumber] - targetKeyframe.lastObservedFrameNumber = frameNumber - return (targetKeyframe, targetKeyframe) - - previousKeyframe = None - nextKeyframe = None - for currFrameNumber in sorted(keyframeHash.keys()): - if currFrameNumber <= frameNumber: - previousKeyframe = keyframeHash[currFrameNumber] - previousKeyframe.lastObservedFrameNumber = currFrameNumber - if currFrameNumber > frameNumber: - nextKeyframe = keyframeHash[currFrameNumber] - nextKeyframe.lastObservedFrameNumber = currFrameNumber - break # Past the sorted range, so done looking - - return (previousKeyframe, nextKeyframe) - - def getParameterKeyframePairsClosestToFrameNumber(self, keyframeType, frameNumber): - typeOptions = ['float', 'complex'] - if keyframeType not in typeOptions: - raise ValueError("keyframeType must be one of (%s)" % ", ".join(typeOptions)) - - if frameNumber >= self.frameCount: - raise IndexError("Requested %s keyframe frame number '%d' is out of range for a span that's '%d' frames long" % (keyframeType, frameNumber, self.frameCount)) - - outerKeyframeHash = None - if keyframeType == 'complex': - outerKeyframeHash = self.complexParameterKeyframes - else: #keyframeType == 'float': - outerKeyframeHash = self.floatParameterKeyframes - - - answerKeyframes = defaultdict(list) - # answerKeyframes['paramName'] -> [(previousKeyframe, nextKeyframe) , (previousKeyframe, nextKeyframe)...] - - for currParamName, keyframeHash in outerKeyframeHash.items(): - #print("Looking at %s" % currParamName) + answerParameters = {} + + keyframesList = self.getKeyframesClosestToTime(targetTime) + + for (parameterName, previousKeyframe, nextKeyframe) in keyframesList: + # TODO: Would like to enforce matching transitions, rather than + # just using one keyframe's transition type. But, that kinda requires + # a more compassionate implementation of adding keyframes which validates + # surrounding types and values. + if isinstance(previousKeyframe, DiveSpanCustomKeyframe) and isinstance(nextKeyframe, DiveSpanCustomKeyframe): + # Keyframes are 'call-a-function' type + + # I guess, only the first keyframe's function is used? + # When keyframes were retrieved, their time position was stashed. + # Calculate how far along the target time is between the keyframes + targetPercentBetweenKeyframes = ((target - prev.time) / (next.time - prev.time)) + answerParameters[parameterName] = previousKeyframe.calculateValueForTime(targetTime, targetPercentBetweenKeyframes, currentParameters) + else: + # Keyframes are 'values-to-interpolate' type + answerParameters[parameterName] = self.interpolateBetweenKeyframes(targetTime, previousKeyframe, nextKeyframe) + + print(answerParameters) + + return answerParameters + + def getKeyframesClosestToTime(self, targetTime): + answerKeyframes = [] + # answerKeyframes[(paramName, previousKeyframe, nextKeyframe),...] + + # parameterKeyframes['meshRealWidth'][2300] = keyframeObject + for currParamName, keyframesByTime in self.parameterKeyframes.items(): + #print(f"Looking at {currParamName}") + # Direct hit - if frameNumber in keyframeHash: - targetKeyframe = keyframeHash[frameNumber] - targetKeyframe.lastObservedFrameNumber = frameNumber - answerKeyframes[currParamName].append((targetKeyframe, targetKeyframe)) + if targetTime in keyframesByTime: + targetKeyframe = keyframesByTime[targetTime] + targetKeyframe.lastObservedTime = targetTime + answerKeyframes.append((currParamName, targetKeyframe, targetKeyframe)) continue - + previousKeyframe = None nextKeyframe = None - for currFrameNumber in sorted(keyframeHash.keys()): - if currFrameNumber <= frameNumber: - previousKeyframe = keyframeHash[currFrameNumber] - previousKeyframe.lastObservedFrameNumber = currFrameNumber - if currFrameNumber > frameNumber: - nextKeyframe = keyframeHash[currFrameNumber] - nextKeyframe.lastObservedFrameNumber = currFrameNumber + for currTime in sorted(keyframesByTime.keys()): + if currTime <= targetTime: + previousKeyframe = keyframesByTime[currTime] + previousKeyframe.lastObservedTime = currTime + if currTime > targetTime: + nextKeyframe = keyframesByTime[currTime] + nextKeyframe.lastObservedTime = currTime break # Past the sorted range, so done looking if previousKeyframe != None and nextKeyframe != None: - answerKeyframes[currParamName].append((previousKeyframe, nextKeyframe)) + answerKeyframes.append((currParamName, previousKeyframe, nextKeyframe)) #print("found: %s" % str(answerKeyframes)) return answerKeyframes - def interpolateCenterValueBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): + def interpolateBetweenKeyframes(self, targetTime, leftKeyframe, rightKeyframe): """ - Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe + Relies heavily on the stashed/cached 'lastObservedTime' of a keyframe """ - #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) - - # Recognize when left and right are the same, and dont' calculate anything. - if leftKeyframe == rightKeyframe: - return leftKeyframe.center + print("interpolating %s -> %s at time %s" % (str(leftKeyframe), str(rightKeyframe), str(targetTime))) - if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: - raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) + if targetTime < leftKeyframe.lastObservedTime or targetTime > rightKeyframe.lastObservedTime: + raise IndexError("Time '%d' isn't between 2 keyframes at '%d' and '%d'" % (targetTime, leftKeyframe.lastObservedTime, rightKeyframe.lastObservedTime)) - # May want to consider 'close' rather than equal for the center equivalence check. - if leftKeyframe.center == rightKeyframe.center: - return leftKeyframe.center - - # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn - if leftKeyframe.transitionOut != rightKeyframe.transitionIn: - raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) - transitionType = leftKeyframe.transitionOut - - # Python scopes seep like this, right? Just use the value later? - # - # And I kept all these separate, because I'm prety sure there will be more - # interpolation-specific parameters needed when all's said and done. - interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.center.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.center.imag, frameNumber) - - return self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) - - def interpolateWindowValuesBetweenKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): - """ - Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe - """ - # Recognize when left and right are the same, and dont' calculate anything. - if leftKeyframe == rightKeyframe: - return (leftKeyframe.realWidth, leftKeyframe.imagWidth) - - # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn - if leftKeyframe.transitionOut != rightKeyframe.transitionIn: - raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) - - transitionType = leftKeyframe.transitionOut - if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: - raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) - - interpolatedRealWidth = leftKeyframe.realWidth - interpolatedImagWidth = leftKeyframe.imagWidth - - # And I kept all these separate, because I'm prety sure there will be more - # interpolation-specific parameters needed when all's said and done. - interpolatedRealWidth = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.realWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.realWidth, frameNumber) - interpolatedImagWidth = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.imagWidth, rightKeyframe.lastObservedFrameNumber, rightKeyframe.imagWidth, frameNumber) - - #print("window interpolates from: (%s,%s) to: (%s,%s) as: (%s,%s)" % (leftKeyframe.realWidth, leftKeyframe.imagWidth, rightKeyframe.realWidth, rightKeyframe.imagWidth, interpolatedRealWidth, interpolatedImagWidth)) - - return (interpolatedRealWidth, interpolatedImagWidth) - - def interpolateComplexBetweenParameterKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): - """ - Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe - """ - #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) - - # Recognize when left and right are the same, and dont' calculate anything. - if leftKeyframe == rightKeyframe: - return leftKeyframe.value - - if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: - raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) - - # May want to consider 'close' rather than equal for the center equivalence check. - if leftKeyframe.value == rightKeyframe.value: - return leftKeyframe.value - - # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn - if leftKeyframe.transitionOut != rightKeyframe.transitionIn: - raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) - transitionType = leftKeyframe.transitionOut - - # Python scopes seep like this, right? Just use the value later? - # - # And I kept all these separate, because I'm prety sure there will be more - # interpolation-specific parameters needed when all's said and done. - interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.value.real, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value.real, frameNumber) - interpolatedImag = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedFrameNumber, leftKeyframe.value.imag, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value.imag, frameNumber) - - return self.timeline.mathSupport.createComplex(interpolatedReal, interpolatedImag) - - def interpolateFloatBetweenParameterKeyframes(self, frameNumber, leftKeyframe, rightKeyframe): - """ - Relies heavily on the stashed/cached 'lastObservedFrameNumber' value of a keyframe - """ - #print("interpolating %s -> %s at frame %s" % (str(leftKeyframe), str(rightKeyframe), str(frameNumber))) - - # Recognize when left and right are the same, and dont' calculate anything. - if leftKeyframe == rightKeyframe: - return leftKeyframe.value - - if frameNumber < leftKeyframe.lastObservedFrameNumber or frameNumber > rightKeyframe.lastObservedFrameNumber: - raise IndexError("Frame number '%d' isn't between 2 keyframes at '%d' and '%d'" % (frameNumber, leftKeyframe.lastObservedFrameNumber, rightKeyframe.lastObservedFrameNumber)) - - # May want to consider 'close' rather than equal for the center equivalence check. - if leftKeyframe.value == rightKeyframe.value: - return leftKeyframe.value - - # Enforce that left keyframe's transitionOut should match right keyframe's transitionIn - if leftKeyframe.transitionOut != rightKeyframe.transitionIn: - raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) + # Let's just be lenient for now? + ## Enforce that left keyframe's transitionOut should match right + ## keyframe's transitionIn + #if leftKeyframe.transitionOut != rightKeyframe.transitionIn: + # raise ValueError("Keyframe transition types mismatched for frame number '%d'" % frameNumber) transitionType = leftKeyframe.transitionOut + mathSupport = self.timeline.mathSupport + + if leftKeyframe.dataType == 'float': + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return leftKeyframe.value + + return mathSupport.interpolate(transitionType, leftKeyframe.lastObservedTime, leftKeyframe.value, rightKeyframe.lastObservedTime, rightKeyframe.value, targetTime) + elif leftKeyframe.dataType == 'complex': + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return leftKeyframe.value + interpolatedReal = mathSupport.interpolate(transitionType, leftKeyframe.lastObservedTime, leftKeyframe.value.real, rightKeyframe.lastObservedTime, rightKeyframe.value.real, targetTime) + interpolatedImag = mathSupport.interpolate(transitionType, leftKeyframe.lastObservedTime, leftKeyframe.value.imag, rightKeyframe.lastObservedTime, rightKeyframe.value.imag, targetTime) + return mathSupport.createComplex(interpolatedReal, interpolatedImag) + elif leftKeyframe.dataType == 'int': + # Recognize when left and right are the same, and dont' calculate anything. + if leftKeyframe == rightKeyframe: + return int(leftKeyframe.value) + + return int(float(mathSupport.interpolate(transitionType, leftKeyframe.lastObservedTime, leftKeyframe.value, rightKeyframe.lastObservedTime, rightKeyframe.value, targetTime))) + else: + raise ValueError(f"Can't perform interpolation for data type \"{leftKeyframe.dataType}\"") - # Python scopes seep like this, right? Just use the value later? - # - # And I kept all these separate, because I'm prety sure there will be more - # interpolation-specific parameters needed when all's said and done. - interpolatedReal = self.timeline.mathSupport.interpolate(transitionType, leftKeyframe.lastObservedKeyframeNumber, leftKeyframe.value, rightKeyframe.lastObservedFrameNumber, rightKeyframe.value, frameNumber) - return interpolatedReal + #### + # TODO: All of these helper functions need to perform a modification + # of upstream and downstream keyframes when forcing addition, to make + # sure the transition types are all in order. + # When creating a new keyframe, default is to inherit the lead-in and + # lead-out interpolators of the existing span. + # + # So, if it's: + # | (lin) | (default all linear) + # + # | +(unspec)K(unspec) | + # | (lin) K (lin) | + # + # | +(log-to)K(unspec) | + # | (log-to) K (lin) | + # + # | +(log-to)K(log-from) | + # | (log-to) K (log-from) | + # + def addNewParameterKeyframe(self, targetTime, dataType, name, value, transitionIn='linear', transitionOut='linear'): + newKeyframe = DiveSpanKeyframe(self, dataType, value, transitionIn, transitionOut) + self.parameterKeyframes[name][targetTime] = newKeyframe + return newKeyframe - def __repr__(self): - return """\ -[DiveTimelineSpan {framecount} frames]\ -""".format(framecount=self.frameCount) + def renderKeyframesAsStringList(self): + # First shovel all values into the array, then sort by time + # and re-assign the time to also be a string. + answerList = [] + for currParamName, keyframesByTime in self.parameterKeyframes.items(): + for currTime, currKeyframe in keyframesByTime.items(): + # TODO: should have at least 2 types of keyframes, one for + # values, and one for functions, but starting with JUST values... + answerList.append([currTime, currParamName, currKeyframe.dataType, str(currKeyframe.value), currKeyframe.transitionIn, currKeyframe.transitionOut]) + + answerList = sorted(answerList) + for currItem in answerList: + currItem[0] = str(currItem[0]) + + return answerList + + def setKeyframesFromStringList(self, keyframesList): + # Completely overwrite any existing keyframes + self.parameterKeyframes = defaultdict(dict) + + mathSupport = self.timeline.mathSupport + # TODO: Again, should have at least 2 types of keyframes, one for + # values, and one for functions, but starting with JUST values... + for (timeString, paramName, dataType, valueString, transitionIn, transitionOut) in keyframesList: + loadedValue = None + if dataType == 'float': + loadedValue = mathSupport.createFloat(valueString) + elif dataType == 'complex': + loadedValue = mathSupport.createComplex(valueString) + elif dataType == 'int': + loadedValue = int(valueString) + + self.parameterKeyframes[paramName][int(float(timeString))] = DiveSpanKeyframe(self, dataType, loadedValue, transitionIn, transitionOut) + + def __getstate__(self): + """ Pickle encoding helper, generates all string arrays and hashes. """ + spanState = {} + spanState['time'] = str(self.time) + spanState['duration'] = str(self.duration) + spanState['keyframes'] = self.renderKeyframesAsStringList() + return spanState + + def __setstate__(self, state): + self.time = int(state['time']) + self.duration = int(state['duration']) + self.setKeyframesFromStringList(state['keyframes']) class DiveSpanKeyframe: - def __init__(self, span, transitionIn='quadratic-to', transitionOut='quadratic-from'): - self.span = span + def __init__(self, span, dataType, value, transitionIn='quadratic-to', transitionOut='quadratic-from'): + + dataTypeOptions = ['float', 'complex', 'int', 'no_type'] + # Probably would like int and bool too? + if dataType not in dataTypeOptions: + raise ValueError("dataType must be one of (%s)" % ", ".join(dataTypeOptions)) - transitionOptions = ['quadratic-to', 'quadratic-from', 'quadratic-to-from', 'log-to', 'root-to', 'linear'] +# log-from doesn't work yet... + transitionOptions = ['quadratic-to', 'quadratic-from', 'quadratic-to-from', 'log-to', 'root-to', 'root-from', 'root-to-ease-in', 'root-to-ease-out', 'root-from-ease-in', 'root-from-ease-out', 'linear', 'step'] if transitionIn not in transitionOptions: raise ValueError("transitionIn must be one of (%s)" % ", ".join(transitionOptions)) if transitionOut not in transitionOptions: raise ValueError("transitionOut must be one of (%s)" % ", ".join(transitionOptions)) - + + self.span = span + self.dataType = dataType + self.value = value self.transitionIn = transitionIn self.transitionOut = transitionOut - self.lastObservedFrameNumber = 0 # For stashing frame numbers in + self.lastObservedTime = 0 # For stashing times in def __repr__(self): - return """\ -[DiveSpanKeyframe, {inType} -> frame {frame} -> {outType}]\ -""".format(inType=self.transitionIn, frame=self.lastObservedFrameNumber, outType=self.transitionOut) - -class DiveSpanCenterKeyframe(DiveSpanKeyframe): - def __init__(self, span, center, transitionIn='quadratic-to', transitionOut='quadratic-from'): - super().__init__(span, transitionIn, transitionOut) - self.center = center - -class DiveSpanWindowKeyframe(DiveSpanKeyframe): - def __init__(self, span, realWidth, imagWidth, transitionIn='root-to', transitionOut='root-to'): - super().__init__(span, transitionIn, transitionOut) - self.realWidth = realWidth - self.imagWidth = imagWidth - -class DiveSpanUniformKeyframe(DiveSpanKeyframe): - def __init__(self, span, transitionIn='quadratic-to', transitionOut='quadratic-from'): - super().__init__(span, transitionIn, transitionOut) - -class DiveSpanTiltKeyframe(DiveSpanKeyframe): - def __init__(self, span, widthFactor, heightFactor, transitionIn='quadratic-to', transitionOut='quadratic-from'): - super().__init__(span, transitionIn, transitionOut) - self.widthFactor = widthFactor - self.heightFactor = heightFactor - -class DiveSpanParameterKeyframe(DiveSpanKeyframe): - def __init__(self, span, value, transitionIn='quadratic-to', transitionOut='quadratic-from'): - super().__init__(span, transitionIn, transitionOut) - self.value = value - -#### -# Big bunch of 'still thinking about' comments here. Probably should have kept them on my own branch. -### -# -# Conceptual priority of items in a timeline goes something like... -# -# Frame Numbers -# Frames are the base grid, so nothing can 'shift' a frame number to a higher or lower value. -# -# DiveSpan -# Spans define the start and end base values (pre-modifications) of a dive animation -# -# DiveSpanCenterKeyframe and DiveSpanWindowKeyframe -# Within spans, target values to hit for a frame -# -# I think that's the end of the highest-order items. Modifications up to this point have to -# all be complete and calculated before the next items can be calculated/analyzed. -# -# For window rages, whether set as keyframes, or interpolated between, you can observe -# the effective zoom factor between any two frames, based on the transition from frame's -# values to the next - -# -# Idea behind ZoomFactorKeyframe -# -# Attaches to the timeline based on width ranges. Still haven't solved several -# aspects of this half-baked idea yet. -# Main goal is to be able to set a speed factor for a range of frames. -# -# If a keyframe sets an axis -# range for a specific frame number, then the two surrounding frame-spans up to the nearest adjacent -# range keyframes need to be stretched or squished so that the product of all the zoom factors -# between keyframes achieves the ranges set in the adjacent range keyframes. Or more simply, -# if you add a range keyframe, then the surrounding zoom factors are adjusted so existing -# range keyframe targets aren't changed. -# The default ramp type for zoom factor interpolation is linear. The logic behind this is that -# if the factor is linearly monotonically increasing, then the feel is acceleration. So linear -# interpolation of zoom factors results in a ramp of speed between keyframes. I imagine that more -# explicit curve control for zoom factor interpolation will be desirable. -# -# [[Linear interpolation does seem to make it harder to set a constant speed though, doesn't it?]] -# [[Does it work to add a keyframe as "set a constant zoom factor between all these frames"?]] -# - -## -# A - attempt at constructing a 'move this image to this frame' kind of behavior -## -# Take the calculated range values at frame + 4, and shift them to the target frame. -# Other stuff has to follow along... and attempting to write this is showing me what's left dangling... -# -# Quick observation: -# We'll come at this edit by looking at an existing render, and wanting a modification. -# The existing render has specific base information at a frame we're observing. -# This construction should be resilient to upstream base information modifications -# (i.e. if something 'before' is changed, this edit should still work about the same way) - -# And this really has to be range-dependent, despite my trying to dodge that over and over and over. -# I keep trying to say "the base is rigidly calculated" -# And keep realizing that speed manipulations for a visible range MUST be dependent on some -# flexible attachment of the effect to a range of base values. - -# Maybe the deal is, that range manipulations are all calculated in advance, and in the specific order of -# their declaration? Like, there's no complicated layering of range manipulations, only -# a procedural description of all the range manipulations, which is realized into the baked-in -# base information for every frame, before axes are generated or meshes are manipulated. - -# Adding set values non-monotonically, will basically play segments in reverse, right? -# start = 1.0, end = .0001, 100 frames long -# +keyframe in place at frame 10 (so frames 1-10 are equivalent to the default base ranges) -# +keyframe at frame 15, back to 1.0 (so plays start->key->start->end) -# (also, frames 15-100 are equivalent to what used to happen in frames 1-100) -# -# So now, want to set a 'speed' for 15->20 -# Or better, want to set the reverse speed to faster... -# But I shouldn't be allowed to say "play a frame range faster", should I? -# Because it's dependent on the interpolated values that it has to hit. -# But I need to be able to say for this UNINTERRUPTED frame span (could verify on application?!), -# Set my zoom factors to BLAH. -# A speed manipulation is a localized effect, that will change the (?upstream and?) downstream interpolations. -# The probem is, we maybe need to figure out what those ranges are iteratively, before allowing -# it to 'lock' into place? - -#targetFrameNumber = 2 -#sampleFromFrameNumber = targetFrameNumber + 4 -# -# Looks like that when we're sampling the 'current' shape for a future frame number, -# we need to force that number into a keyframe, if it's part of an existing manipulation? -#if mainSpan.frameNumberHasParametricEffects(sampleFromFrameNumber): -#if mainSpan.frameNumberHasBaseModifiers(sampleFromFrameNumber): -# # Creates a 'current values' keyframe, unrolls parametric changes to get to concrete values -# mainSpan.addKeyframeInPlace(sampleFromFrameNumber) -# -#addBaseRangeKeyframeInPlace -# -#focusRangeWidth, focusRangeHeight = mainSpan.getRangeShapeForFrameNumber(sampleFromFrameNumber) - -# When we set a window keyframe, we've changed the 'effective zoom factor' of all upstream and downstream frames. -# The range of the 'effective speed change' is bounded by the next closest keyframes. - -## -# B - attempt at constructing a "speed up these 4 frames" kinda behavior -## -# -#targetFrameNumber = 3 -#targetFrameCount = 4 -#startingRangeWidth, startingRangeHeight = mainSpan.getRangeShapeForFrameNumber(targetFrameNumber) -#endingRangeWidth, endingRangeHeight = mainSpan.getRangeShapeForFrameNumber(targetFrameNumber + targetFrameCount - 1) - -# No speed assignments that span across existing base keyframes allowed. - -# Seems like this is getting close. -# The thing is, for a given range (whether forward or backward), I can't manipulate the speed in a way that -# breaks the base keyframe endpoints. -# -# Maybe that's it... -# No 'speed' assignments possible, only equivalent 'base keyframe' assignments. -# So I can't say 'play from this base range to that base range faster, -# -# Really want speed to ramp as a default, and make it so sudden jumps in speed -# are more difficult to achieve by default. - -# THAT'S RIGHT - I forgot, that speed modifiers don't hang on frame numbers, but they're applied -# on regions within the visible ranges... -# BUT THAT WON'T WORK as a timeline thing, because if we reverse, then we don't want the -# speed modifier to have an effect for all the matching ranges. - -# Maybe it's that at a given keyframe, BOTH range targets, and a zoom factor can be applied? - -## Maybe "speed change" is a function that does a bunch of math for you? -#mainSpan.addRangeSpeedKeyframe(startFrameNumber, leadingTransitionFrames=2, trailingTransitionFrames=2, startingZoomFactor * 2, -# -#mainSpan.addRangeSpeedModifier(startFrameNumber, leadingTransitionFrames=2, trailingTransitionFrames=2, startingZoomFactor * 2) -# -#internally, it says: -#firstFrameZoomFactor = mainSpan.getZoomFactorBetweenFrameNumbers(startFrameNumber, startFrameNumber + 1) -#?rangeKeyframeAxisGenerator()? -#...generate keyframes for target ranges, based on this... somehow... - - - -# -- -# Default settings for the dive. All of these can be overridden from the -# command line -# -- -def set_demo1_params(fractal_ctx, view_ctx): - print("+ Running in demo mode - loading default mandelbrot dive params") - #fractal_ctx.img_width = 1024 - #fractal_ctx.img_height = 768 - #fractal_ctx.img_width = 320 - #fractal_ctx.img_height = 240 - - fractal_ctx.img_width = 160 - fractal_ctx.img_height = 120 - - #fractal_ctx.img_width = 80 - #fractal_ctx.img_height = 60 - #fractal_ctx.img_width = 16 - #fractal_ctx.img_height = 12 - - #fractal_ctx.img_width = 4 - #fractal_ctx.img_height = 3 - - cmplx_width_str = '5.0' - cmplx_height_str = '3.5' - #cmplx_width_str = '.001' - #cmplx_height_str = '.00075' - #cmplx_width_str = '2.0' - #cmplx_height_str = str(1024*2/768) - - # For debugging, this window (and a couple zooms after) is where things - # tend to go wrong when precision is getting clipped off - #cmplx_width_str = '6.736977389253725e-08' - #cmplx_height_str = '4.7158841724776074e-08' - fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) - fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) - - - # This is close t Misiurewicz point M32,2 - # fractal_ctx.cmplx_center = fractal_ctx.ctxc(-.77568377, .13646737) - #center_real_str = '-1.769383179195515018213' - #center_imag_str = '0.00423684791873677221' - #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) - #center_str = '-1.769383179195515018213+0.00423684791873677221j' - - - center_real_str = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884' - center_imag_str = '0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690' - - -# Shorter, for debugging -# center_real_str = '-1.76938317919551501821384728608547378290574726365475143746552821652788819126475645883616344638952966730448582578182030315748749123842171940312824619511374752125508480620857874547728033032251679986623911241845427430171292144236397931692967543941816568313013426227935414237685724357839108499720568695273052075081914417347810617942906997531749111337143517341661174565202727561591789320429089324651026717908784146646282137559906504607383722834707778703064588828982026040017443489083888449628870745058537' -# center_imag_str = '0.004236847918736772214926507171367997076682670917403757279459435650112344000805545157302430995023636506313532683359652571823004948055387363061275248149392923559308928343920507967248879049219866660455766269469006661034940149047143237255869797899085206566832026580640241153003788267897863946416220353410551029004563057237186845272103773258463079175126287746720056933262328069538227967558325171888734791243614309894854955011240963294216828273306935321715053674555266373827069885834569156846732024622119' - - - #center_str = '-0.05+0.6805j' - #fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_str) - fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(center_real_str, center_imag_str) - - fractal_ctx.project_name = 'demo1' - fractal_ctx.scaling_factor = .8 - #fractal_ctx.scaling_factor = .5 - - fractal_ctx.write_video = False - - - # Looks like 23.976/8 fps (almost 3fps), duration=60 (which could be 180 frames) - # max-iter=4096 - # Bit depth was only ~400 - # Ended up reaching e-51 - # Most iteration answers were around 1200-1300? - - - # Trying to double that... Basically, can just double duration? - # And then, can start off at frame 175? - # - # Looks like 23.976/8 fps (almost 3fps), duration=120 (which could be 360 frames) - # Which is 45 frames per process, for 8 processes. - # max-iter=4096 - # Let's stack bit depth to accommodate max_iter? - # Lower bound I'm going with is: .4438 - # So bits = 1817? Let's make it a round 1800? - # Ended up reaching... - # Most iteration answers were around ? - - # Think I forgot to set bits deep enough? - # Blew out at e-57? - # Looks like iters were around 1500 - # Precision was (accidentally?) 400 - - # 550 * 3.32 = 1826 - # max-iter=4096 - # Looks like 23.976/8 fps (almost 3fps), - # duration=90 (which could be 270 frames) - # lost all detail at ~227? - # iters were hitting 4000 - # Looks like it hit e-68 - - # Ok. - # 8192 iter - # ~3621 bits => 1125 digits - # duration=180 (~540 frames defined, just giving myself runway) - # Gonna run frames 200+? - - # Frame 240 is hitting e-72 widths, 4400 iters - # Looks like window definition has ~953 digits of unused precision? - # Lol. - - # Hit ~e-91 at frame 303 - # iters sitting around 5300 - - - # Shaving off 400 digits of precision... (to 725 digits = 2407 bits) - # Also trying only 6 simultaneous, frames 300-350 - - # frame 350 hit ~e-107 - # Iters around 6300 - - # Shaving more precision off - 625 digits - - # Blew out around 385? - # Hitting 7900 iters - # Hitting e-114, so is using something like 300 positions total? - - # K, for frames 375+, gonna go with... - # doubling iters again, - # Adding another 100 positions? (went to 825 positions) - - # Finally timed that run - # 50 frames (375-425) - # 2048*8 max iter (== 16384 iters) - # 825 digits (2739 bits) - # real 61m38.026s - # user 514m30.844s - # sys 0m9.203s - # Which is ~74 seconds per frame. - # ~10,400 iters used - # ~e-129 widths - - # Moving endpoint to 320 seconds? (~957 frames?) - # Going up to 2048 * 10 (= 20,480 iters) - # Looks like the last increment used 15 more digits... - # So, Maybe add 100 positions? - # That'll be 925 digits (3071 bits) - # real 30m22.199s - # user 381m58.938s - # sys 0m12.344s - # hit e-135 at 450 - # using 11,000 iters - - - # Frames 450-475 (no big changes for this segment) - # real 38m16.362s - # user 445m59.469s - # sys 0m12.938s - # hit e-143 at 475 - # using ~14,800 itersA - # Looks like this gets to ~10:30 in the edge of infinity dive. - # So, out of 2:29:04, that's about 6% of the dive? - # - # Gonna raise to 41k iterations (2048 * 20) - # Gonna bump up to 1500 digits? (4830 bits?) - # Trying to take a bigger bite, 475-900? - - # Attempt at batching to 599 failed the first time, - # BUT resulted in: - # used 27k iter - # e-180 widths - # Looks like this was gonna work just fine... - - # 25 frames hit ~109 minutes (~4.5 minutes per frame) - # (1308 minutes user time) = 52 minutes per frame. - # Iters hit ~24k - - # real 142m25.250s - # user 1797m10.031s - # sys 0m11.922s - + return(f"DiveSpanKeyframe {self.lastObservedTime} {self.dataType} {self.value}, in={self.transitionIn}, out={self.transitionOut}") - #fractal_ctx.max_iter = 128 - #fractal_ctx.max_iter = 255 - #fractal_ctx.max_iter = 512 # covers ~e-34 or so - #fractal_ctx.max_iter = 1024 # covers ~e-48 or so - fractal_ctx.max_iter = 2048 - #fractal_ctx.max_iter = 2048 * 15 # ~30k, ok at e-180? - #fractal_ctx.max_iter = int(2048 * 15.5) # ~31k, ok at e-225 - #fractal_ctx.max_iter = int(2048 * 16) # ~31k, ok at e-225 - #fractal_ctx.max_iter = 2048 * 20 # ~41k, ok at e-391 - #fractal_ctx.max_iter = 2048 * 40 # ~82k, ok at e-497 - #fractal_ctx.max_iter = 2048 * 34 # ~70k, ok at e-504 - #fractal_ctx.max_iter = 2048 * 71 # ~145k, ok at e-752 - #fractal_ctx.max_iter = 2048 * 140 # ~286k iter, ok at e-1204 - #fractal_ctx.max_iter = 2048 * 300 # ~614k iter, ok at e-1505 - #fractal_ctx.max_iter = 2048 * 400 # ~819k iter, ok at e-1806 - #fractal_ctx.max_iter = 2048 * 1300 # ~2,662k iter, ok at e-2011 - - fractal_ctx.escape_rad = 2 - fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['escape_radius'] = fractal_ctx.escape_rad - fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name]['max_escape_iterations'] = fractal_ctx.max_iter - - # Basically make this command-line, not demo? - #fractal_ctx.clip_start_frame = 7 - #fractal_ctx.clip_frame_count = 3 - - fractal_ctx.verbose = 3 - fractal_ctx.build_cache=False - - # FPS still isn't set quite right, but we'll get it there eventually. - #view_ctx.fps = 23.976 / 4.0 - #view_ctx.fps = 23.976 / 2.0 - #view_ctx.fps = 23.976 - #view_ctx.fps = 29.97 / 2.0 - #view_ctx.fps = 23.976 / 8.0 - - #view_ctx.duration = math.ceil(6.0 / view_ctx.fps) - #view_ctx.duration = 4.0 - #view_ctx.duration = 540.0 - #view_ctx.duration = 1500.0 - #view_ctx.duration = 30.0 - #view_ctx.duration = 2.0 - #view_ctx.duration = 0.25 - #view_ctx.duration = 2300.0 # A bit more than EoI's dive (6682), this is ~6890 frames, if a .5 zoom factor at 23.976/8 fps. - - - # - view_ctx.fps = 23.976 / 2.0 - view_ctx.duration = 180.0 # Just enough to explore? - -def set_julia_walk_demo1_params(fractal_ctx, view_ctx): - print("+ Running in demo mode - loading default julia walk params") - fractal_ctx.img_width = 1024 - fractal_ctx.img_height = 768 - - cmplx_width_str = '3.2' - cmplx_height_str = '2.5' - fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(cmplx_width_str) - fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(cmplx_height_str) - - - #fractal_ctx.algorithm_extra_params['julia_center'] = fractal_ctx.math_support.createComplex(-.8,.145) - fractal_ctx.algorithm_extra_params['julia']['julia_center'] = fractal_ctx.math_support.createComplex(-.8,.145) - - #fractal_ctx.algorithm_extra_params['julia_center'] = fractal_ctx.math_support.createComplex(0,0) - fractal_ctx.julia_list = [fractal_ctx.math_support.createComplex(0.355,0.355), fractal_ctx.math_support.createComplex(0.0,0.8), fractal_ctx.math_support.createComplex(0.3355,0.355)] - - fractal_ctx.cmplx_center = fractal_ctx.math_support.createComplex(0,0) - - fractal_ctx.project_name = 'julia_demo1' - - fractal_ctx.scaling_factor = 1.0 - - fractal_ctx.max_iter = 255 - - fractal_ctx.escape_rad = 2. - #fractal_ctx.escape_rad = 32768. - - fractal_ctx.verbose = 3 - fractal_ctx.build_cache = True - - view_ctx.duration = 4.0 - #view_ctx.duration = 0.5 - - # FPS still isn't set quite right, but we'll get it there eventually. - view_ctx.fps = 23.976 / 8.0 - #view_ctx.fps = 23.976 / 2.0 - #view_ctx.fps = 29.97 / 2.0 - -def set_preview_mode(fractal_ctx, view_ctx): - print("+ Running in preview mode ") - - fractal_ctx.img_width = 300 - fractal_ctx.img_height = 200 - - fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(3.0) - fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(2.5) - - fractal_ctx.scaling_factor = .75 - - #fractal_ctx.escape_rad = 4. - fractal_ctx.escape_rad = 32768. - - view_ctx.duration = 4 - view_ctx.fps = 4 - - -def set_snapshot_mode(fractal_ctx, view_ctx, snapshot_filename='snapshot.gif'): - print("+ Running in snapshot mode ") - - fractal_ctx.snapshot = True - view_ctx.vfilename = snapshot_filename - - fractal_ctx.img_width = 3000 - fractal_ctx.img_height = 2000 - - fractal_ctx.max_iter = 2000 - - fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(3.0) - fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(2.5) - - fractal_ctx.scaling_factor = .99 # set so we can zoom in more accurately - - #fractal_ctx.escape_rad = 4. - fractal_ctx.escape_rad = 32768. - - view_ctx.duration = 0 - view_ctx.fps = 0 +class DiveSpanCustomKeyframe(DiveSpanKeyframe): + """ + TODO: Don't use this yet. + Function-based keyframes aren't yet handled by the persistence functions. + """ + def __init__(self, span, targetObject, targetFunction, transitionIn='linear', transitionOut='linear'): + super().__init__(span, 'no_type', None, transitionIn, transitionOut) + self.targetObject = targetObject + self.targetFunction = targetFunction + def calculateValueForTime(self, targetTime, targetPercentBetweenKeyframes, currentParameters): + print("CALCULATION CANCELLED - DEBUGGING DiveSpanCustomKeyframe") + # Not even tested yet... + #runnableFunction = getattr(self.targetObject, targetFunction) + #return runnableFunction(targetTime, targetPercentBetweenKeyframes, currentParameters) def make_project(params): """ @@ -1389,8 +570,8 @@ def make_project(params): "exploration_markers_path": "exploration/markers", "edit_markers_path": "edit/markers", "edit_timelines_path": "edit/timelines", - "render_mesh_width": "1024", - "render_mesh_height": "768", + "render_image_width": "1024", + "render_image_height": "768", "render_fps": "23.976", "render_output_path": "output", "render_exports_path": "exports", @@ -1419,55 +600,9 @@ def parse_options(): Handles parsing of command line parameters, including those specified in Algo implementations. - For all modes except "--make-project", also load the values + For all modes except "--make-project", also loads the values from params.json into params['project_params'] """ - -#def parse_options(fractal_ctx, view_ctx): - # argv = sys.argv[1:] - # opts, args = getopt.getopt(argv, "pd:m:s:f:w:h:c:a:", - # ["preview", - # "algo=", - # "flint", - # "flintcustom", - # "gmp", - # "demo", - # "demo-julia-walk", - # "duration=", - # "fps=", - # "clip-start-frame=", - # "clip-frame-count=", - # "project-name=", - # "image-frame-path=", - # "shared-cache-path=", - # "build-cache", - # "invalidate-cache", - # "banner", - # "max-iter=", - # "img-w=", - # "img-h=", - # "cmplx-w=", - # "cmplx-h=", - # "center=", - # "scaling-factor=", - # "snapshot=", - # "gif=", - # "mpeg=", - # "verbose=", - # "palette-test=", - # #"color=", - # "color", - # "julia-center=", # Julia - # "julia-list=", # Julia - # "burn", # Hopefully all algorithms? - # "escape_radius", # Mandelbrot, Julia - # "max_escape_iterations", # Mandelbrot, Julia - # "smooth", # Mandelbrot, Julia - # "make-project=", - ## "project=", - # "math-support=", - # ]) - params = {} # Most everything here fills this dictionary options_list = ["math-support=", @@ -1484,7 +619,7 @@ def parse_options(): "expl-center=", "expl-frame-number=", # Timeline-only params - "batch-frame-numbers=", + "batch-frame-file=", ] # Add *all* the command line options that Algos recognize, even though @@ -1497,7 +632,6 @@ def parse_options(): options_list.extend(algorithm_class.options_list()) # Make param list unique, just for readability options_list = list(set(options_list)) - opts, args = getopt.getopt(sys.argv[1:], "", options_list) # First-pass at params to set up MathSupport because lots of @@ -1519,7 +653,7 @@ def parse_options(): params['digits_precision'] = int(arg) # Important to also set expected precision before parsing param values support_precision = params.get('digits_precision', 16) # 16 == native - math_support.setPrecision(support_precision) + math_support.setPrecision(round(support_precision * 3.32)) # ~3.32 bits per position params['math_support'] = math_support # Second-pass at params, figures out which mode we're operating @@ -1600,162 +734,43 @@ def parse_options(): elif params['mode'] == 'timeline': for opt, arg in opts: - if opt in ['--batch-frame-numbers']: + if opt in ['--batch-frame-file']: # We're in batch mode, instead of entire-timeline mode, # so get more specific. params['mode'] = 'batch_timeline' - params['batch_frame_numbers'] = arg + params['batch_frame_file'] = arg return params -# # First-pass parameters handled so others can be responsive -# # - Math support, so instantiations are properly typed -# # - Algorithm name, so additional parameters can be read -# for opt, arg in opts: -# if opt in ['--gmp']: -# fractal_ctx.math_support = fm.DiveMathSupportGmp() -# elif opt in ['--flint']: -# fractal_ctx.math_support = fm.DiveMathSupportFlint() -# elif opt in ['--flintcustom']: -# fractal_ctx.math_support = fm.DiveMathSupportFlintCustom() -# elif opt in ['-a', '--algo']: -# if str(arg) in fractal_ctx.algorithm_map: -# fractal_ctx.algorithm_name = str(arg) -# -# if fractal_ctx.algorithm_name is None: -# fractal_ctx.algorithm_name = 'mandelbrot' -# -# # Kinda a crazy invocation. Loads algorithm-specific parameters into -# # a dictionary, based on that algorithm's static class parse function. -# fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] = fractal_ctx.algorithm_map[fractal_ctx.algorithm_name].parse_options(opts) -# # Theoretically possible we'll eventually want to run this for all -# # possible algorithm types, but for now, just loading for the -# # 'active' algorithm. -# -# for opt,arg in opts: -# if opt in ['-p', '--preview']: -# set_preview_mode(fractal_ctx, view_ctx) -# elif opt in ['-s', '--snapshot']: -# set_snapshot_mode(fractal_ctx, view_ctx, arg) -# elif opt in ['--demo']: -# set_demo1_params(fractal_ctx, view_ctx) -# elif opt in ['--demo-julia-walk']: -# set_julia_walk_demo1_params(fractal_ctx, view_ctx) -# -# palette = fp.FractalPalette() # Will get stashed for the algorithm to use -# -# for opt, arg in opts: -# if opt in ['-d', '--duration']: -# view_ctx.duration = float(arg) -# elif opt in ['-f', '--fps']: -# view_ctx.fps = float(arg) -# elif opt in ['--clip-start-frame']: -# # For construct_simple_timeline, use of --clip-start-frame -# # makes --clip-frame-count kinda required -# fractal_ctx.clip_start_frame = int(arg) -# elif opt in ['--clip-frame-count']: -# fractal_ctx.clip_frame_count = int(arg) -# elif opt in ['-m', '--max-iter']: -# fractal_ctx.max_iter = int(arg) -# elif opt in ['-w', '--img-w']: -# fractal_ctx.img_width = int(arg) -# elif opt in ['-h', '--img-h']: -# fractal_ctx.img_height = int(arg) -# elif opt in ['--cmplx-w']: -# fractal_ctx.cmplx_width = fractal_ctx.math_support.createFloat(arg) -# elif opt in ['--cmplx-h']: -# fractal_ctx.cmplx_height = fractal_ctx.math_support.createFloat(arg) -# elif opt in ['-c', '--center']: -# fractal_ctx.cmplx_center= fractal_ctx.math_support.createComplex(arg) -# elif opt in ['--scaling-factor']: -# fractal_ctx.scaling_factor = float(arg) -# elif opt in ['-z', '--zoom']: -# fractal_ctx.set_zoom_level = int(arg) -# -# # Worth noting, julia-list is used only for Timeline construction, -# # and isn't an intrisic thing to the Algo. -# elif opt in ['--julia-list']: -# raw_julia_list = eval(arg) # expects a list of complex numbers -# if len(raw_julia_list) <= 1: -# print("Error: List of complex numbers for Julia walk must be at least two points") -# sys.exit(0) -# julia_list = [] -# for currCenter in raw_julia_list: -# julia_list.append(fractal_ctx.math_support.create_complex(currCenter)) -# fractal_ctx.julia_list = julia_list -# elif opt in ['--palette-test']: -# if str(arg) == "gauss": -# palette.create_gauss_gradient((255,255,255),(0,0,0)) -# elif str(arg) == "exp": -# palette.create_exp_gradient((255,255,255),(0,0,0)) -# elif str(arg) == "exp2": -# palette.create_exp2_gradient((0,0,0),(128,128,128)) -# elif str(arg) == "list": -# palette.create_gradient_from_list() -# else: -# print("Error: --palette-test arg must be one of gauss|exp|list") -# sys.exit(0) -# palette.display() -# sys.exit(0) -## elif opt in ['--color']: -## if str(arg) == "gauss": -## palette.create_gauss_gradient((255,255,255),(0,0,0)) -## elif str(arg) == "exp": -## palette.create_exp_gradient((255,255,255),(0,0,0)) -## elif str(arg) == "exp2": -## palette.create_exp2_gradient((0,0,0),(128,128,128)) -## elif str(arg) == "list": -## palette.create_gradient_from_list() -## else: -## print("Error: --palette-test arg must be one of gauss|exp|list") -## sys.exit(0) -## fractal_ctx.palette = palette -# elif opt in ['--project-name']: -# fractal_ctx.project_name = arg -# elif opt in ['--shared-cache-path']: -# fractal_ctx.shared_cache_path = arg -# elif opt in ['--build-cache']: -# fractal_ctx.build_cache = True -# elif opt in ['--invalidate-cache']: -# fractal_ctx.invalidate_cache = True -# elif opt in ['--banner']: -# view_ctx.banner = True -# elif opt in ['--verbose']: -# verbosity = int(arg) -# if verbosity not in [0,1,2,3]: -# print("Invalid verbosity level (%d) use range 0-3"%(verbosity)) -# sys.exit(0) -# fractal_ctx.verbose = verbosity -# elif opt in ['--gif']: -# if view_ctx.vfilename != None: -# print("Error : Already specific media type %s"%(view_ctx.vfilename)) -# sys.exit(0) -# view_ctx.vfilename = arg -# elif opt in ['--mpeg']: -# if view_ctx.vfilename != None: -# print("Error : Already specific media type %s"%(view_ctx.vfilename)) -# sys.exit(0) -# view_ctx.vfilename = arg -# -# # Stash the palette as an extra parameter -# extra_params = fractal_ctx.algorithm_extra_params[fractal_ctx.algorithm_name] -# extra_params['palette'] = palette - def run_exploration(params): algorithm_name = params['expl_algo'] project_params = params['project_params'] project_folder_name = params['project_name'] - timeline = DiveTimeline(projectFolderName=project_folder_name, algorithm_name=algorithm_name, framerate=23.976, frameWidth=project_params['exploration_mesh_width'], frameHeight=project_params['exploration_mesh_height'], mathSupport=params['math_support']) - real_width = params['expl_real_width'] - imag_width = params['expl_imag_width'] - center = params['expl_center'] + timeline = DiveTimeline(projectFolderName=project_folder_name, algorithmName=algorithm_name, framerate=23.976, frameWidth=project_params['exploration_mesh_width'], frameHeight=project_params['exploration_mesh_height'], mathSupport=params['math_support']) + mesh_real_width = params['expl_real_width'] + mesh_imag_width = params['expl_imag_width'] + mesh_center = params['expl_center'] + + # Note: --max-escape-iterations is passed along via algorithm_extra_params + + span_duration = 40 + main_span = timeline.addNewSpan(0,span_duration) + + main_span.addNewParameterKeyframe(0, 'complex', 'meshCenter', mesh_center, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'complex', 'meshCenter', mesh_center, transitionIn='root-to', transitionOut='root-to') - span = timeline.addNewSpanAtEnd(1, center, real_width, imag_width, real_width, imag_width) + main_span.addNewParameterKeyframe(0, 'float', 'meshRealWidth', mesh_real_width, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'float', 'meshRealWidth', mesh_real_width, transitionIn='root-to', transitionOut='root-to') + + main_span.addNewParameterKeyframe(0, 'float', 'meshImagWidth', mesh_imag_width, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'float', 'meshImagWidth', mesh_imag_width, transitionIn='root-to', transitionOut='root-to') extra_params = params['algorithm_extra_params'] - dive_mesh=timeline.getMeshForFrame(0) - extra_params.update(dive_mesh.extraParams) # Allow per-frame info to overwrite algorithm info? + frame_time = timeline.getTimeForFrameNumber(0) + dive_mesh=timeline.getMeshForTime(frame_time) + # Allow per-frame info to overwrite algorithm info. + extra_params.update(dive_mesh.extraParams) output_folder_name = os.path.join(project_folder_name, project_params['exploration_output_path']) @@ -1765,42 +780,156 @@ def run_exploration(params): frame_algorithm.run() - -# if algorithm_name == 'julia': -# # Probably better to check for instance-of Julia Algo? -# span = DiveTimelineSpan(timeline, 1) -# span.addNewWindowKeyframe(0, real_width, imag_width) -# span.addNewWindowKeyframe(1, real_width, imag_width) -# span.addNewUniformKeyframe(0) -# span.addNewUniformKeyframe(1) -# -# span.addNewCenterKeyframe(0, center) -# span.addNewCenterKeyframe(1, center) -# -## Let's try to rely on extra params to pass in the julia_center? -## Should work, since I don't have to interpolate it anywhere? -## -## julia_center = algo_params['julia_center'] -## span.addNewComplexParameterKeyframe(0, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') -## span.addNewComplexParameterKeyframe(1, 'julia_center', currJuliaCenter, transitionIn='linear', transitionOut='linear') +def run_timeline(params): + """ + Parameters come from command-line script 'params' hash, as well + as from the 'project_params' sub-hash, which loads the params.json + """ + project_params = params['project_params'] + project_folder_name = params['project_name'] + timeline_name = params['timeline_name'] + + timeline_file_name = os.path.join(params['project_name'], params['project_params']['edit_timelines_path'], params['timeline_name']) + timeline = load_timeline_from_file(timeline_file_name, params) + + main_span = timeline.getMainSpan() + frame_count = timeline.getFramesInDuration(main_span.duration) + print(f"duration: {main_span.duration}") + print(f"frame count: {frame_count}") + + output_folder_name = os.path.join(project_folder_name, project_params['render_output_path'], timeline_name) + if not os.path.exists(output_folder_name): + os.makedirs(output_folder_name) + + frame_file_names = [] + for curr_frame_number in range(frame_count): + frame_time = timeline.getTimeForFrameNumber(curr_frame_number) + print(f"frame {curr_frame_number} at {frame_time}") + dive_mesh = timeline.getMeshForTime(frame_time) + print(f"{dive_mesh.realMeshGenerator.baseWidth} x {dive_mesh.imagMeshGenerator.baseWidth} ({dive_mesh.extraParams['max_escape_iterations']} iter)") + # Following is a class instantiation, of a string-specified Algo class + algorithm_map = DiveTimeline.algorithm_map() + frame_algorithm = algorithm_map[timeline.algorithmName](dive_mesh=dive_mesh, frame_number=curr_frame_number, output_folder_name=output_folder_name, extra_params=dive_mesh.extraParams) + frame_algorithm.run() + + frame_file_names.append(frame_algorithm.output_image_file_name) + + export_base_name = f"{timeline_name}.mp4" + export_file_name = os.path.join(project_folder_name, project_params['render_exports_path'], export_base_name) + clip = mpy.ImageSequenceClip(frame_file_names, fps=timeline.framerate) + clip.write_videofile(export_file_name, fps=timeline.framerate, audio=False, codec="mpeg4") + +def load_timeline_from_file(timeline_file_name, params): + print(f"loading timeline from:\n{timeline_file_name}") + with open(timeline_file_name, 'rt') as timeline_handle: + timelineJSON = json.load(timeline_handle) + timeline = DiveTimeline.build_from_json_and_params(timelineJSON, params) + return timeline + +#### +#### This block will be useful when timeline is a script loader, not just json +#### +# timeline_file_base = u"%s.py" % timeline_name +# timeline_file = os.path.join(project_folder_name, project_params['edit_timelines_path'], timeline_file_base) # -# timeline.timelineSpans.append(span) +# # Using SourceFileLoader to load a script file from a specified path. +# from importlib.machinery import SourceFileLoader +# timeline_module = SourceFileLoader('timeline_file', timeline_file).load_module() +# timeline = timeline_module.getTimeline(params) # -# else: -# span = timeline.addNewSpanAtEnd(1, center, real_width, imag_width, real_width, imag_width) +# timeline = debugTimelineMaker(params) +#### +def debugTimelineMaker(params): + project_folder_name = params['project_name'] + math_support = params['math_support'] -def run_timeline(params): - pass + timeline_name = params['timeline_name'] + + project_params = params['project_params'] + + framerate = float(project_params.get('render_fps', 23.976)) + frame_width = int(project_params.get('render_image_width', 160)) + frame_height = int(project_params.get('render_image_height', 120)) + + max_iterations = 255 + + timeline = DiveTimeline(project_folder_name, 'mandelbrot_smooth', framerate, frame_width, frame_height, math_support) + + span_duration = 2000 + main_span = timeline.addNewSpan(0,span_duration) + + mesh_center = math_support.createComplex('-1.76938+0.00423j') + mesh_real_width = math_support.createFloat('1.0') + mesh_imag_width = math_support.createFloat(frame_height / frame_width * mesh_real_width) + + end_mesh_real_width = mesh_real_width * .01 + end_mesh_imag_width = mesh_imag_width * .01 + + main_span.addNewParameterKeyframe(0, 'complex', 'meshCenter', mesh_center, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'complex', 'meshCenter', mesh_center, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(0, 'float', 'meshRealWidth', mesh_real_width, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'float', 'meshRealWidth', end_mesh_real_width, transitionIn='root-to', transitionOut='root-to') + + main_span.addNewParameterKeyframe(0, 'float', 'meshImagWidth', mesh_imag_width, transitionIn='root-to', transitionOut='root-to') + main_span.addNewParameterKeyframe(span_duration, 'float', 'meshImagWidth', end_mesh_imag_width, transitionIn='root-to', transitionOut='root-to') + + iterations_delta = 200 + main_span.addNewParameterKeyframe(0, 'int', 'max_escape_iterations', max_iterations, transitionIn='linear', transitionOut='linear') + main_span.addNewParameterKeyframe(span_duration * .25, 'int', 'max_escape_iterations', max_iterations-iterations_delta, transitionIn='linear', transitionOut='linear') + main_span.addNewParameterKeyframe(span_duration * .5, 'int', 'max_escape_iterations', max_iterations, transitionIn='linear', transitionOut='linear') + main_span.addNewParameterKeyframe(span_duration * .75, 'int', 'max_escape_iterations', max_iterations-iterations_delta, transitionIn='linear', transitionOut='linear') + main_span.addNewParameterKeyframe(span_duration, 'int', 'max_escape_iterations', max_iterations, transitionIn='linear', transitionOut='linear') + + return timeline + def run_batch_timeline(params): - pass + """ + Parameters come from command-line script 'params' hash, as well + as from the 'project_params' sub-hash, which loads the params.json + """ + project_params = params['project_params'] + project_folder_name = params['project_name'] + timeline_name = params['timeline_name'] + + timeline_file_name = os.path.join(params['project_name'], params['project_params']['edit_timelines_path'], params['timeline_name']) + timeline = load_timeline_from_file(timeline_file_name, params) + + main_span = timeline.getMainSpan() + frame_count = timeline.getFramesInDuration(main_span.duration) + print(f"duration: {main_span.duration}") + print(f"frame count: {frame_count}") + + output_folder_name = os.path.join(project_folder_name, project_params['render_output_path'], timeline_name) + if not os.path.exists(output_folder_name): + os.makedirs(output_folder_name) + + frame_numbers = [] + batch_frame_file + with open(batch_frame_file, 'rt') as batch_handle: + for currLine in batch_handle: + frameNumbers.append(int(currLine.strip())) + + #frame_file_names = [] + for curr_frame_number in frame_numbers: + frame_time = timeline.getTimeForFrameNumber(curr_frame_number) + print(f"batch frame {curr_frame_number} at {frame_time}") + dive_mesh = timeline.getMeshForTime(frame_time) + print(f"{dive_mesh.realMeshGenerator.baseWidth} x {dive_mesh.imagMeshGenerator.baseWidth} ({dive_mesh.extraParams['max_escape_iterations']} iter)") + # Following is a class instantiation, of a string-specified Algo class + algorithm_map = DiveTimeline.algorithm_map() + frame_algorithm = algorithm_map[timeline.algorithmName](dive_mesh=dive_mesh, frame_number=curr_frame_number, output_folder_name=output_folder_name, extra_params=dive_mesh.extraParams) + frame_algorithm.run() + + #frame_file_names.append(frame_algorithm.output_image_file_name) if __name__ == "__main__": print("++ fractal.py version %s" % (MANDL_VER)) params = parse_options() + #print(params) mode = params['mode'] if mode == 'exploration': run_exploration(params) @@ -1812,10 +941,3 @@ def run_batch_timeline(params): raise ValueError("Run mode is unrecognized - abandoning run") - #fractal_ctx = FractalContext() - #view_ctx = MediaView(16, 16, fractal_ctx) - #parse_options(fractal_ctx, view_ctx) - - #view_ctx.setup() - #view_ctx.run() - diff --git a/fractalmath.py b/fractalmath.py index 7c800d1..02e5a20 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -56,7 +56,14 @@ def setPrecision(self, newPrecision): #raise NotImplementedError("setPrecision is meaningless for base class, and must be implemented in DiveMathSupport sublcasses") def precision(self): - return 16 # Heh - pytnon's native float64, right? + return 53 # Heh - pytnon's native float64, right? + + def setDigitsPrecision(self, digits): + #print(f"Setting precision DIGITS: {digits}") + self.setPrecision(round(digits * 3.32)) + + def digitsPrecision(self): + return round(self.precision() / 3.32) def createComplex(self, *args): """ @@ -99,7 +106,11 @@ def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterati return startValue * (overallZoomFactor ** iterations) def createLinspace(self, paramFirst, paramLast, quantity): - """Attempt at a mostly type-agnostic linspace(), seems to work with flint types too""" + """ + Attempt at a mostly type-agnostic linspace(), seems to work with + flint types too. + """ + #print(f"createLinspace {paramFirst}, {paramLast}, {quantity}") dataRange = paramLast - paramFirst answers = np.zeros((quantity), dtype=object) @@ -116,6 +127,8 @@ def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): was an important for a Python/Decimal version to be able to have a type-specific implementation in what was a DiveMeshDecimal function """ + #print(f"createLinspaceAroundValuesCenter {valuesCenter}, {spreadWidth}, {quantity}") + return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) def interpolate(self, transitionType, startX, startY, endX, endY, targetX, extraParams={}): @@ -124,18 +137,34 @@ def interpolate(self, transitionType, startX, startY, endX, endY, targetX, extra were needed for each type, but we might just be able to pass in an extra param hash? """ + print(f"DiveMathSuppport interpolation type {transitionType}") + if transitionType == 'log-to': return self.interpolateLogTo(startX, startY, endX, endY, targetX) elif transitionType == 'root-to': return self.interpolateRootTo(startX, startY, endX, endY, targetX) + elif transitionType == 'root-from': + return self.interpolateRootFrom(startX, startY, endX, endY, targetX) + elif transitionType == 'root-to-ease-in': + return self.interpolateRootToEaseIn(startX, startY, endX, endY, targetX) + elif transitionType == 'root-to-ease-out': + return self.interpolateRootToEaseOut(startX, startY, endX, endY, targetX) + elif transitionType == 'root-from-ease-in': + return self.interpolateRootFromEaseIn(startX, startY, endX, endY, targetX) + elif transitionType == 'root-from-ease-out': + return self.interpolateRootFromEaseOut(startX, startY, endX, endY, targetX) elif transitionType == 'linear': return self.interpolateLinear(startX, startY, endX, endY, targetX) + elif transitionType == 'step': + return startY # Kinda a special not-actual-interpolation elif transitionType == 'quadratic-to': return self.interpolateQuadraticEaseOut(startX, startY, endX, endY, targetX) elif transitionType == 'quadratic-from': return self.interpolateQuadraticEaseIn(startX, startY, endX, endY, targetX) - else: # transitionType == 'quadratic-to-from' + elif transitionType == 'quadratic-to-from': return self.interpolateQuadraticEaseInOut(startX, startY, endX, endY, targetX) + else: # transitionType == 'quadratic-to-from' + raise ValueError(f"ERROR - Transition type \"{transitionType}\" isn't recognized!") def interpolateLogTo(self, startX, startY, endX, endY, targetX): """ @@ -181,14 +210,108 @@ def interpolateRootTo(self, startX, startY, endX, endY, targetX): we want to be able to interpolate between two points using the frame count as the root. """ + print(f"root-to attempt") if targetX == endX: + print(" end") return endY elif targetX == startX or startX == endX or startY == endY: + print(" start or identical") return startY else: - root = endX - startX + root = abs(endX - startX) scaleFactor = (endY / startY) ** (1 / root) - return startY * (scaleFactor ** targetX) + debugAnswer = startY * (scaleFactor ** abs(targetX - startX)) + print(f" root-to answer {debugAnswer}") + return startY * (scaleFactor ** abs(targetX - startX)) + #root = endX - startX + #scaleFactor = (endY / startY) ** (1 / root) + #debugAnswer = startY * (scaleFactor ** (targetX - startX)) + #print(f" root-to answer {debugAnswer}") + #return startY * (scaleFactor ** (targetX - startX)) + + def interpolateRootFrom(self, startX, startY, endX, endY, targetX): + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + print(f"root-froming into (({startX},{startY}),({endX},{endY}))") + #debugAnswer = self.interpolateRootTo(startX, endY, endX, startY, targetX) + #print(f"root-froming answer {debugAnswer}") + #return self.interpolateRootTo(startX, endY, endX, startY, targetX) + return self.interpolateRootTo(endX, endY, startX, startY, targetX) + + ######## + # For the combination interpolations... + # + # First, map the target position to a quadratic-adjusted + # position, then use that adjusted target position as the + # input to the 'primary' interpolation + ######## + + def interpolateRootToEaseIn(self, startX, startY, endX, endY, targetX): + """ + For example, zoom factors are root-to, so use this to settle + into the target zoom (rather than hitting it suddenly). + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + targetXRatio = self.interpolateQuadraticEaseIn(startX, 0.0, endX, 1.0, targetX) + adjustedTargetX = ((endX - startX) * targetXRatio) + startX + + print(f"root-to-ease-in adjusted target from {targetX} to {adjustedTargetX} mapping into (({startX},{startY}),({endX},{endY}))") + return self.interpolateRootTo(startX, startY, endX, endY, adjustedTargetX) + + def interpolateRootToEaseOut(self, startX, startY, endX, endY, targetX): + """ + For example, zoom factors are root-to, so use this to slowly + build up speed when zooming in, rather than suddenly starting. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + targetXRatio = self.interpolateQuadraticEaseOut(startX, 0.0, endX, 1.0, targetX) + adjustedTargetX = ((endX - startX) * targetXRatio) + startX + print(f"root-to-ease-out adjusted target from {targetX} at ratio {targetXRatio} to {adjustedTargetX} mapping into (({startX},{startY}),({endX},{endY}))") + + return self.interpolateRootTo(startX, startY, endX, endY, adjustedTargetX) + + def interpolateRootFromEaseIn(self, startX, startY, endX, endY, targetX): + """ + When zooming OUT at a constant speed, the behavior is root-from, + so this interpolation settles into the end point. + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + targetXRatio = self.interpolateQuadraticEaseIn(startX, 0.0, endX, 1.0, targetX) + adjustedTargetX = ((endX - startX) * targetXRatio) + startX + print(f"root-from-ease-in adjusted target from {targetX} to {adjustedTargetX} mapping into (({startX},{startY}),({endX},{endY}))") + + return self.interpolateRootFrom(startX, startY, endX, endY, adjustedTargetX) + def interpolateRootFromEaseOut(self, startX, startY, endX, endY, targetX): + """ + When zooming OUT at a constant speed, the behavior is root-from, + so this interpolation slowly builds up speed from rest (rather + than starting suddenly). + """ + if targetX == endX: + return endY + elif targetX == startX or startX == endX or startY == endY: + return startY + else: + targetXRatio = self.interpolateQuadraticEaseOut(startX, 0.0, endX, 1.0, targetX) + adjustedTargetX = ((endX - startX) * targetXRatio) + startX + print(f"root-from-ease-out adjusted target from {targetX} to {adjustedTargetX} mapping into (({startX},{startY}),({endX},{endY}))") + + return self.interpolateRootFrom(startX, startY, endX, endY, adjustedTargetX) def interpolateQuadraticEaseIn(self, startX, startY, endX, endY, targetX): """ @@ -288,6 +411,8 @@ def mandelbrot(self, c, escapeRadius, maxIter): z = z*z + c + #print("input: %s" % (str(c))) + #print("answer: %s, lastZ: %s" % (str(n), str(z))) return (n, z) def julia(self, c, z0, escapeRadius, maxIter): @@ -485,6 +610,7 @@ def __init__(self): self.precisionType = 'flint' def setPrecision(self, newPrecision): + #print(f"Setting precision: {newPrecision}") oldPrecision = self.flint.ctx.prec self.flint.ctx.prec = newPrecision return oldPrecision @@ -665,6 +791,32 @@ def shorterStringFromFloat(self, paramFloat, places): def floor(self, value): return self.flint.arb(value).floor() +# def createLinspace(self, paramFirst, paramLast, quantity): +# """ +# """ +# print(f"Flint createLinspace {paramFirst}, {paramLast}, {quantity}") +# dataRange = paramLast - paramFirst +# answers = np.zeros((quantity), dtype=object) +# +# for x in range(0, quantity): +# answers[x] = paramFirst + dataRange * (x / (quantity - 1)) +# answers[x] = self.flint.arb(answers[x].mid(), 0) +# return answers +# +# def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): +# """ +# """ +# firstValue = valuesCenter - spreadWidth * 0.5 +# firstValue = self.flint.arb(firstValue.mid(), 0) +# +# lastValue = valuesCenter + spreadWidth * 0.5 +# lastValue = self.flint.arb(lastValue.mid(), 0) +# +# print(f"createLinspaceAroundValuesCenter {valuesCenter}, {spreadWidth}, {quantity}") +# +# return self.createLinspace(firstValue, lastValue, quantity) +# #return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) + def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): """ Shortcut calculate the starting point for the last frame's properties, @@ -712,9 +864,12 @@ def interpolateRootTo(self, paramStartX, paramStartY, paramEndX, paramEndY, para elif targetX == startX or startX == endX or startY == endY: return startY else: - root = endX - startX + root = endX - startX scaleFactor = (endY / startY) ** (1 / root) - return startY * (scaleFactor ** targetX) + print(f"root: {root}\nscaleFactor: {scaleFactor}") + debugAnswer = startY * (scaleFactor ** (targetX - startX)) + print(f" flint root-to answer {debugAnswer}") + return startY * (scaleFactor ** (targetX - startX)) def interpolateQuadraticEaseOut(self, paramStartX, paramStartY, paramEndX, paramEndY, paramTargetX): """ @@ -775,6 +930,8 @@ def mandelbrot(self, c, escapeRadius, maxIter): zImagNoErr = self.flint.arb(z.imag.mid(), 0) z = self.flint.acb(zRealNoErr, zImagNoErr) + #print("input: %s" % (str(c))) + #print("answer: %s, lastZ: %s" % (str(n), str(z))) return (n, z) def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): @@ -881,6 +1038,10 @@ def clamp(self, num, min_value, max_value): return max(min(num, max_value), min_value) class DiveMathSupportFlintCustom(DiveMathSupportFlint): + def __init__(self): + super().__init__() + self.precisionType = 'flintcustom' + def mandelbrot(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) diff --git a/fractalpalette.py b/fractalpalette.py index df996be..19e9593 100644 --- a/fractalpalette.py +++ b/fractalpalette.py @@ -236,3 +236,54 @@ def display(self): clip = mpy.VideoClip(self.make_frame, duration=64) clip.preview(fps=1) #fps 1 is really all that works ## FractalPalette + +class FractalPaletteWithSchemes(FractalPalette): + def __init__(self, scheme_list): + super().__init__() + + self.scheme_index = 0 + self.scheme_list = scheme_list + + # These probably belong in more specific sublcasses, but + # no working example yet, so not sure. + #self.scheme_multipiers # will be a per-color multiplier for val + #self.scheme_offsets # will be a per-color constant for inside the val clculation + + def set_scheme_index(self, new_scheme_index): + if new_scheme_index < len(self.scheme_list): + self.scheme_index = new_scheme_index + + def map_value_to_color(self, val): + """ + Kinda cheating, just overriding instead of switching behaviors. + """ + if math.isnan(val): + return (0,0,0) + + currColorValues = self.scheme_list[self.scheme_index] + + # (yellow blue 0,.6,1.0) + c1 = 1 + math.cos(3.0 + val*0.15 + currColorValues[0]) + c2 = 1 + math.cos(3.0 + val*0.15 + currColorValues[1]) + c3 = 1 + math.cos(3.0 + val*0.15 + currColorValues[2]) + + + if c1 <= 0 or math.isnan(c1): + c1int = 0 + else: + c1int = int(255.*((c1/4.) * 3.) / 1.5) + if c2 <= 0 or math.isnan(c2): + c2int = 0 + else: + c2int = int(255.*((c2/4.) * 3.) / 1.5) + if c3 <= 0 or math.isnan(c3): + c3int = 0 + else: + c3int = int(255.*((c3/4.) * 3.) / 1.5) + + return (c1int,c2int,c3int) +# else: +# c1 = 1 + math.cos(3.0 + val*0.15) +# cint = int(255.*((c1/4.) * 3.) / 1.5) +# return (cint,cint,cint) +# diff --git a/julia_smooth.py b/julia_smooth.py new file mode 100644 index 0000000..ed6d90b --- /dev/null +++ b/julia_smooth.py @@ -0,0 +1,119 @@ +# -- +# File: julia_solo.py +# +# +# Interesting julia-center points: +# -.8+.145j +# +# -- + +import os +import pickle + +import math +import numpy as np + +from PIL import Image, ImageDraw + +from julia_solo import JuliaSolo +import fractalpalette as fp + +class JuliaSmooth(JuliaSolo): + + def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): + super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) + + self.algorithm_name = 'julia_smooth' + + # TODO: really should be handled by a palette + self.color = (.0,.6,1.0) # blue / yellow + + def process_counts(self): + smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) + self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + + def generate_image(self): + # Capturing the transpose of our array, because it looks like I mixed + # up rows and cols somewhere along the way. + if self.use_smoothing == True: + pixel_values_2d = self.cache_frame.frame_info.smooth_values.T + else: + pixel_values_2d = self.cache_frame.frame_info.raw_values.T + #print("shape of things to come: %s" % str(pixel_values_2d.shape)) + +# TODO: Really, width and height are all kinda incorrect here - +# gotta spend some TLC on the array shape and transpose. + (image_width, image_height) = pixel_values_2d.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + for x in range(0, image_width): + for y in range(0, image_height): + color = self.palette.map_value_to_color(pixel_values_2d[x,y]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_frame_metadata() + if meta: + burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) + self.burn_text_to_drawing(burn_in_text, draw) + + return im + + def generate_image(self): + (image_height, image_width) = self.processed_array.shape + im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) + draw = ImageDraw.Draw(im) + + # Note: Image's width,height is backwards from numpy's size (rows, cols) + for x in range(0, image_width): + for y in range(0, image_height): + color = self.map_value_to_color(self.processed_array[y,x]) + + # Plot the point + draw.point([x, y], color) + + if self.burn_in == True: + meta = self.get_metadata() + if meta: + burn_in_text = u"%d" % (meta['frame_number']) + self.burn_text_to_drawing(burn_in_text, draw) + + image_filename_base = u"%d.tiff" % self.frame_number + self.output_image_file_name = os.path.join(self.output_folder_name, image_filename_base) + im.save(self.output_image_file_name) + + def map_value_to_color(self, val): + # TODO: should make this a special rotating palette, + # or bound the values before doing a lookup to a palette. + if math.isnan(val): + return (0,0,0) + + if self.color: + # (yellow blue 0,.6,1.0) + c1 = 1 + math.cos(3.0 + val*0.15 + self.color[0]) + c2 = 1 + math.cos(3.0 + val*0.15 + self.color[1]) + c3 = 1 + math.cos(3.0 + val*0.15 + self.color[2]) + + if c1 <= 0 or math.isnan(c1): + c1int = 0 + else: + c1int = int(255.*((c1/4.) * 3.) / 1.5) + if c2 <= 0 or math.isnan(c2): + c2int = 0 + else: + c2int = int(255.*((c2/4.) * 3.) / 1.5) + if c3 <= 0 or math.isnan(c3): + c3int = 0 + else: + c3int = int(255.*((c3/4.) * 3.) / 1.5) + + return (c1int,c2int,c3int) + else: + c1 = 1 + math.cos(3.0 + val*0.15) + cint = int(255.*((c1/4.) * 3.) / 1.5) + return (cint,cint,cint) + + diff --git a/mandelbrot_smooth.py b/mandelbrot_smooth.py index 5f109ef..d3ce280 100644 --- a/mandelbrot_smooth.py +++ b/mandelbrot_smooth.py @@ -16,6 +16,8 @@ from mandelbrot_solo import MandelbrotSolo +from fractalpalette import FractalPaletteWithSchemes + class MandelbrotSmooth(MandelbrotSolo): # TODO: Yeah, really should be a palette @@ -42,64 +44,21 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) self.algorithm_name = 'mandelbrot_smooth' - self.color = (.0,.6,1.0) # blue / yellow - #self.color = (.1,.2,.3) # dark + # Hacky hard-code for now. + # TODO: need to persist palette definitions with the timeline json. + self.palette = FractalPaletteWithSchemes([(.0,.6,1.0), # blue-tan + (1.0,.4,.4), # teal-pink + (1.0,.2,.4), # green-pink + (.4,.2,.6), # green-purple + ]) + self.palette_index = extra_params.get('palette_scheme_index', 0) def process_counts(self): + print(f"process_counts says mathSupport is {self.dive_mesh.mathSupport.precisionType}") smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) def generate_image(self): - (image_height, image_width) = self.processed_array.shape - im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) - - # Note: Image's width,height is backwards from numpy's size (rows, cols) - for x in range(0, image_width): - for y in range(0, image_height): - color = self.map_value_to_color(self.processed_array[y,x]) - - # Plot the point - draw.point([x, y], color) - - if self.burn_in == True: - meta = self.get_metadata() - if meta: - burn_in_text = u"%d" % (meta['frame_number']) - self.burn_text_to_drawing(burn_in_text, draw) - - image_filename_base = u"%d.tiff" % self.frame_number - self.output_image_file_name = os.path.join(self.output_folder_name, image_filename_base) - im.save(self.output_image_file_name) - - def map_value_to_color(self, val): - # TODO: should make this a special rotating palette, - # or bound the values before doing a lookup to a palette. - if math.isnan(val): - return (0,0,0) - - if self.color: - # (yellow blue 0,.6,1.0) - c1 = 1 + math.cos(3.0 + val*0.15 + self.color[0]) - c2 = 1 + math.cos(3.0 + val*0.15 + self.color[1]) - c3 = 1 + math.cos(3.0 + val*0.15 + self.color[2]) - - if c1 <= 0 or math.isnan(c1): - c1int = 0 - else: - c1int = int(255.*((c1/4.) * 3.) / 1.5) - if c2 <= 0 or math.isnan(c2): - c2int = 0 - else: - c2int = int(255.*((c2/4.) * 3.) / 1.5) - if c3 <= 0 or math.isnan(c3): - c3int = 0 - else: - c3int = int(255.*((c3/4.) * 3.) / 1.5) - - return (c1int,c2int,c3int) - else: - c1 = 1 + math.cos(3.0 + val*0.15) - cint = int(255.*((c1/4.) * 3.) / 1.5) - return (cint,cint,cint) + self.palette.set_scheme_index(self.palette_index) + super().generate_image() diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py index b12b650..20837fe 100644 --- a/mandelbrot_solo.py +++ b/mandelbrot_solo.py @@ -40,6 +40,9 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) def generate_counts(self): math_support = self.dive_mesh.mathSupport + mathDigits = math_support.digitsPrecision() + print(f"Running with {self.max_escape_iterations} iter and {mathDigits} digits") + #mandelbrot_function = np.vectorize(math_support.mandelbrot_beginning) mandelbrot_function = np.vectorize(math_support.mandelbrot) (self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) @@ -54,6 +57,8 @@ def pre_image_hook(self): # numpyArray.shape returns (rows, columns) for y in range(0, self.mesh_array.shape[0]): + #print(f"first col {self.counts_array[y,0]}") + for x in range(0, self.mesh_array.shape[1]): # Not using mathSupport's floor() here, because it should just be a normal-scale float if self.counts_array[y,x] < self.max_escape_iterations: diff --git a/markers_to_timeline.py b/markers_to_timeline.py new file mode 100644 index 0000000..5b13147 --- /dev/null +++ b/markers_to_timeline.py @@ -0,0 +1,201 @@ +# -- +# File: markers_to_timeline.py +# +# For a specified project (path), and a set of marker IDs, construct a +# timeline that places the markers as keyframes at 'reasonable' places +# across the duration. +# +# All markers need to be stored in the project's exploration markers +# subdirectory, named just by their marker ID (integer) number. +# +# -- + +import getopt +import os, sys + +import pickle +import json + +from divemesh import * +from fractalmath import * +from fractal import * + +from algo import Algo # Abstract base class import, because we rely on it. +from mandelbrot_solo import MandelbrotSolo +from mandelbrot_smooth import MandelbrotSmooth +from mandeldistance import MandelDistance +from julia_solo import JuliaSolo +from julia_smooth import JuliaSmooth + +def parse_options(): + params = {} + opts, args = getopt.getopt(sys.argv[1:], "", + ["project=", + "timeline-name=", + "duration=", + "marker-list=", + ]) + + # First-pass at params pulls out the project file, because + # it probably gives us a default math support + for opt, arg in opts: + if opt in ['--project']: + params['project_name'] = arg + elif opt in ['--timeline-name']: + params['timeline_name'] = arg + elif opt in ['--marker-list']: + params['raw_marker_list'] = eval(arg) # expects a list of ints + elif opt in ['--duration']: + params['duration'] = float(arg) + + # Require that a project has been specified. + if 'project_name' not in params: + raise ValueError("Specifying --project= is required") + + # Load project parameters out of the params file + param_file_name = os.path.join(params['project_name'], 'params.json') + with open(param_file_name, 'rt') as param_handle: + params['project_params'] = json.load(param_handle) + + # Require at least 2 markers + if not 'raw_marker_list' in params or len(params['raw_marker_list']) <= 1: + raise ValueError("--marker-list=... List of marker IDs (ints) be at least two markers long") + + # Require a non-existing timeline name for writing + if not 'timeline_name' in params: + raise ValueError("Specifying --timeline-name= is required") + timeline_file_name = os.path.join(params['project_name'], params['project_params']['edit_timelines_path'], params['timeline_name']) + if os.path.exists(timeline_file_name): + raise ValueError("NOT CREATING timeline:\n File already exists where timeline would be created.") + + params['timeline_file_name'] = timeline_file_name + + # Require a duration (in seconds) + if 'duration' not in params: + raise ValueError("Specifying --duration= is required") + + # Calculate the needed duration for the parameters, which is + # basically rounding up the parameter duration to the next full frame's time. + framerate = float(params['project_params']['render_fps']) + coveringFrameCount = getFramesForSecondsAndFramerate(params['duration'], framerate) + params['millisecond_duration'] = getDurationForFramesAndFramerate(coveringFrameCount, framerate) + + return params + +def loadMarkers(params): + # Only one algo type allowed for the entire set of markers. + observedAlgorithm = None + + markerList = [] + for currMarkerID in params['raw_marker_list']: + markerTitle = "%d.marker.pik" % currMarkerID + markerFileName = os.path.join(params['project_name'], params['project_params']['exploration_markers_path'], markerTitle) + + with open(markerFileName, 'rb') as markerHandle: + currMarker = pickle.load(markerHandle) + + if observedAlgorithm == None: + observedAlgorithm = currMarker.algorithmName + if observedAlgorithm != currMarker.algorithmName: + raise ValueError(f"Can't ingest markers with mixed algorithm types \"{observedAlgorithm}\" and \"{currMarker.algorithmName}\"") + + markerList.append(currMarker) + + return markerList + +def getMathSupportFromMarkerList(markerList): + # Also for now, (because no converters written yet), force all + # markers to be one math support type, though precision may vary. + + observedPrecisionType = None + maxDigits = 1 + for currMarker in markerList: + if observedPrecisionType == None: + observedPrecisionType = currMarker.diveMesh.mathSupport.precisionType + if observedPrecisionType != currMarker.diveMesh.mathSupport.precisionType: + raise ValueError(f"Can't ingest markers with mixed MathSupport types \"{observedPrecisionType}\" and \"{currMarker.diveMesh.mathSupport.precisionType}\"") + + currDigits = currMarker.diveMesh.mathSupport.digitsPrecision() + + if currDigits > maxDigits: + maxDigits = currDigits + + # Going to steal the last mathSupport, and force it to the max + # observed precision + hijackedSupport = markerList[-1].diveMesh.mathSupport + hijackedSupport.setDigitsPrecision(maxDigits) + + return hijackedSupport + +def assignTimingsForMarkerList(markerList, params): + # Stupid for now, but just to get running, let's just divide the spans + # evenly across the duration + framerate = float(params['project_params']['render_fps']) + framesAcrossDuration = getFramesForSecondsAndFramerate(params['millisecond_duration'] / 1000, framerate) + # Assign across one fewer frame duration than this is, because then the + # endpoint will be ON the start of the last frame? + framesAcrossDuration -= 1 + timeForLastFrameStart = getDurationForFramesAndFramerate(framesAcrossDuration, framerate) + return params['math_support'].createLinspace(0, timeForLastFrameStart, len(markerList)).astype('int') + +def getFramesForSecondsAndFramerate(secondsDuration, framerate): + """ + Adapted from from fractal.py->DiveTimeline->getFramesInDuration + """ + # Use 6 decimal places for rounding down, but otherwise, round up. + # 1 second * 24 frames / second = 24 frames + # .041 seconds * 24 frames / second = .984 frames (should be 1) + # .042 seconds * 24 frames / second = 1.008 frames (should be 2) + # .041666 * 24 frames / second = .999984 frames (should be 1) + # .04166666 * 24 frames / second = .99999984 frames (should be 1) + # .04166667 * 24 frames / second = 1.00000008 frames (should be 1) + # .0416667 * 24 frames / second = 1.0000008 frames (should be 2) + rawCount = secondsDuration * framerate + framesCount = int(rawCount) + remainderCount = rawCount - framesCount + + if remainderCount != 0.0 and round(remainderCount,6) != 0.0: + framesCount += 1 + + return framesCount + +def getDurationForFramesAndFramerate(frames, framerate): + secondsDuration = frames / framerate + return int(1000 * secondsDuration) + +if __name__ == '__main__': + + params = parse_options() + + markerList = loadMarkers(params) + mathSupport = getMathSupportFromMarkerList(markerList) + + # Extra stashing of math support for easier use elsewhere + params['math_support'] = mathSupport + + markerTimings = assignTimingsForMarkerList(markerList, params) + + # Only one algo, so just grab the first + algorithmName = markerList[0].algorithmName + + project_params = params['project_params'] + + timeline = DiveTimeline(projectFolderName=params['project_name'], algorithmName=algorithmName, framerate=project_params['render_fps'], frameWidth=project_params['render_image_width'], frameHeight=project_params['render_image_height'], mathSupport=mathSupport) + + span = timeline.addNewSpan(0, params['millisecond_duration']) + for currIndex in range(len(markerList)): + currTime = markerTimings[currIndex] + currMarker = markerList[currIndex] + currMesh = currMarker.diveMesh + + span.addNewParameterKeyframe(currTime, 'complex', 'meshCenter', currMesh.getCenter(), 'linear', 'linear') + span.addNewParameterKeyframe(currTime, 'float', 'meshRealWidth', currMesh.realMeshGenerator.baseWidth, 'root-to', 'root-to') + span.addNewParameterKeyframe(currTime, 'float', 'meshImagWidth', currMesh.imagMeshGenerator.baseWidth, 'root-to', 'root-to') + + span.addNewParameterKeyframe(currTime, 'int', 'max_escape_iterations', currMarker.maxEscapeIterations, 'linear', 'linear') + + with open(params['timeline_file_name'], 'w') as outfile: + json.dump(timeline.__getstate__(), outfile, indent=4) + + + diff --git a/mesh_explore.py b/mesh_explore.py index b679307..f772c7f 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -2,19 +2,12 @@ # File: mesh_explore.py # # Viewer to explore results generated by fractal.py. -# Command-line (and --demo) parameters set how the image is generated. -# -# This means that when fractal.py runs out of frames, it might be -# able to render, that there will be an error in this exploration. -# If you run out of frames, then adjust the value in fractal.py for -# 'duration'. This might happen after adjusting 'fps' as well, except -# that changing 'fps' will ALSO change the frame number that a -# feature appears at. +# Command-line parameters set how the image is generated. # # In addition, to achieve higher depths, you need to adjust 2 values # as you explore: -# 1.) max_iter (e.g. line ~1550 in fractal.py sets for --demo) -# 2.) FLINT_HIGH_PRECISION_SIZE (e.g. line ~31 in fractalmath.py) +# 1.) --max-escape-iterations +# 2.) mathSupport.setPrecision(numberOfDigits) # # If the image goes all white, it's likely you need higher iteration count. # If the image goes splotchy, it's likely you need higher precision. @@ -37,74 +30,118 @@ def parse_options(): params = {} - - argv = sys.argv[1:] - opts, args = getopt.getopt(argv, "f:c:", - ["frame=", + opts, args = getopt.getopt(sys.argv[1:], "", + ["math-support=", + "digits-precision=", + "escape-iterations=", + "project=", "center=", + "next-frame-number=", + "real-width=", + "imag-width=", + "zoom-factor=", + "algo=", ]) + # First-pass at params pulls out the project file, because + # it probably gives us a default math support + for opt, arg in opts: + if opt in ['--project']: + params['project_name'] = arg + + # Require that a project has been specified. + if 'project_name' not in params: + raise ValueError("Specifying --project= is required") + + # Load project parameters out of the params file + paramFileName = os.path.join(params['project_name'], 'params.json') + with open(paramFileName, 'rt') as paramHandle: + params['project_params'] = json.load(paramHandle) + + # Second-pass at params to set up MathSupport because lots of + # things depend on it for names and types. + mathSupportClasses = {'native': fm.DiveMathSupport, + 'flint': fm.DiveMathSupportFlint, + 'flintcustom': fm.DiveMathSupportFlintCustom, + # maybe not complete?# 'gmp': fm.DiveMathSupportGmp, + # maybe not complete?# 'decimal': fm.DiveMathSupportDecimal, + # definitely not built yet.# 'libbf': fm.DiveMathSupportLibbf, + } + mathSupportName = params['project_params'].get('math_support', 'native') + + for opt, arg in opts: + if opt in ['--math-support']: + if arg in mathSupportClasses: + mathSupportName = arg + elif opt in ['--digits-precision']: + params['digits_precision'] = int(arg) + # Creates an instance + mathSupport = mathSupportClasses[mathSupportName]() + + # Important to also set expected precision before parsing param values + supportPrecision = params.get('digits_precision', 16) # 16 == native + mathSupport.setPrecision(round(supportPrecision * 3.32)) # ~3.32 bits per position + params['math_support'] = mathSupport + + # Defaults, overwritable by cmd line args + params['algo'] = 'mandelbrot_smooth' + params['next_frame_number'] = 1 + + # Third pass at params, now that math support is all set up for opt, arg in opts: - if opt in ['f', '--frame']: - params['frameNumber'] = int(arg) - elif opt in ['-c', '--center']: - params['centerString'] = arg - - # Eventually, we should just be able to point to a project file - # (as a pickle), and this ui script will read that, and not have - # to create all its own commands, paths, and parameters, though - # we should probably build our own 'exploration' cache separately. - # - #### Custom flint, just mandelbrot - params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --flintcustom --clip-frame-count=1' - params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/mandelbrot_solo" - # - #### Standard Flint, just mandelbrot - #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --flint --clip-frame-count=1' - #params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/mandelbrot_solo" - # - #### Custom Flint, smooth - #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=smooth --demo --burn --flintcustom --clip-frame-count=1' - #params['wholeCacheFolder'] = "demo1_cache/image_frames/flint/smooth" - # - #### Native calc, smooth - # NOTE: When using 'native', complex should be formatted with - # parens like "(0.1+0.04j)" - #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=smooth --demo --burn --clip-frame-count=1' - #params['wholeCacheFolder'] = "demo1_cache/image_frames/native/smooth" - # - #### Native calc, just mandelbrot - #params['fractalCommandBase'] = 'python3.9 ./fractal.py --algo=mandelbrot_solo --demo --burn --clip-frame-count=1' - #params['wholeCacheFolder'] = "demo1_cache/image_frames/native/mandelbrot_solo" - - params['frameIncrement'] = 1 + if opt in ['--center']: + params['center'] = mathSupport.createComplex(arg) + elif opt in ['--next-frame-number']: + params['next_frame_number'] = int(arg) + elif opt in ['--real-width']: + params['real_width'] = mathSupport.createFloat(arg) + elif opt in ['--imag-width']: + params['imag_width'] = mathSupport.createFloat(arg) + elif opt in ['--escape-iterations']: + params['escape_iterations'] = int(arg) + elif opt in ['--zoom-factor']: + params['zoom_factor'] = float(arg) # No extra precision needed + elif opt in ['--algo']: + params['algo'] = arg + + params['wholeCacheFolder'] = os.path.join(params['project_name'], params['project_params']['exploration_output_path']) + + projectDefaultZoom = float(params['project_params'].get('exploration_default_zoom_factor', 0.8)) + params['zoom_factor'] = params.get('zoom_factor', projectDefaultZoom) + + # Whether an incoming parameter or not, make some values available + # as parameters. + params['digits_precision'] = supportPrecision + + escapeIterations = params.get('escape_iterations', 255) + params['escape_iterations'] = escapeIterations return params def nextClicked(event): global params - origFrameNumber = params.get('frameNumber', 0) - frameNumber = origFrameNumber + params.get('frameIncrement', 1) - params['frameNumber'] = frameNumber - - updateForFrameNumber(frameNumber) + params['real_width'] = params['real_width'] * params['zoom_factor'] + params['imag_width'] = params['imag_width'] * params['zoom_factor'] + updateView() + def prevClicked(event): global params - origFrameNumber = params.get('frameNumber', 0) - frameNumber = origFrameNumber - params.get('frameIncrement', 1) - if frameNumber < 0: - frameNumber = 0 - params['frameNumber'] = frameNumber + params['real_width'] = params['real_width'] / params['zoom_factor'] + params['imag_width'] = params['imag_width'] / params['zoom_factor'] - updateForFrameNumber(frameNumber) + updateView() -def updateForFrameNumber(frameNumber): +def updateView(): + global params global uiElements + clickedLocus = uiElements.get('lastClickedLocus', None) if clickedLocus == None: return - runFractalCall(frameNumber, str(clickedLocus)) + runFractalCallForCenter(clickedLocus) + + frameNumber = params['next_frame_number'] imageFileTitle = "%d.tiff" % frameNumber imageFileName = os.path.join(params['wholeCacheFolder'], imageFileTitle) @@ -113,8 +150,7 @@ def updateForFrameNumber(frameNumber): clickableImage = uiElements['clickableImage'] clickableImage.set_data(imageData) - meshFileTitle = "%s.mesh.pik" % imageFileTitle - meshFileName = os.path.join(params['wholeCacheFolder'], meshFileTitle) + meshFileName = getMeshFileNameForFrameNumber(frameNumber) with open(meshFileName, 'rb') as meshHandle: diveMesh = pickle.load(meshHandle) @@ -123,57 +159,137 @@ def updateForFrameNumber(frameNumber): updateTitle() plt.draw() -def runFractalCall(frameNumber, centerString): +def runFractalCallForCenter(center): global params - fractalCallString = "%s --clip-start-frame=%d" % (params['fractalCommandBase'], frameNumber) - if centerString != None: - fractalCallString += " --center='%s'" % (centerString) + + # TODO: remove the existing cached outputs of this name, if present + + projectName = params['project_name'] + mathSupport = params['math_support'] + algoName = params['algo'] + realWidthString = str(params['real_width']) + imagWidthString = str(params['imag_width']) + nextFrameNumber = params['next_frame_number'] + + projectDefaultZoom = float(params['project_params'].get('exploration_default_zoom_factor', 0.8)) + zoomFactor = params.get('zoom_factor', projectDefaultZoom) + + # Shift to the new center - bit of string deviousness to prevent + # parser errors when no imaginary component exists (flint trims the j?!) + params['center'] = center + centerString = str(center) + if center.imag == 0: + trimmed = centerString + if centerString.endswith(')'): + trimmed = centerString[:-1] + if not trimmed.endswith('j'): + trimmed = trimmed + "+0j" + if centerString.endswith(')'): + centerString = trimmed + ")" + else: + centerString = trimmed + + fractalCallString = f"python3.9 ./fractal.py --project='{projectName}' --exploration --expl-algo={algoName} --burn --math-support={mathSupport.precisionType} --digits-precision={params['digits_precision']} --max-escape-iterations={params['escape_iterations']} --expl-frame-number={nextFrameNumber} --expl-real-width='{realWidthString}' --expl-imag-width='{imagWidthString}' --expl-center='{centerString}'" print("Calling: %s" % fractalCallString) subprocess.call([fractalCallString], shell=True) print("(Call finished.)") print("Exploration invocation for this point:") - print("python3.9 ./mesh_explore.py --frame=%d --center='%s'" % (frameNumber, centerString)) + explorationCallString = f"python3.9 ./mesh_explore.py --project='{projectName}' --algo={algoName} --math-support={mathSupport.precisionType} --digits-precision={params['digits_precision']} --escape-iterations={params['escape_iterations']} --next-frame-number={nextFrameNumber} --real-width='{realWidthString}' --imag-width='{imagWidthString}' --zoom-factor={zoomFactor} --center='{centerString}'" + print(explorationCallString) print("") -def plusClicked(event): +def lastMarkerClicked(event): + pass + + +def refreshClicked(event): + updateView() + +def saveMarkerClicked(event): + global uiElements + + diveMesh = uiElements['diveMesh'] + currFrameNumber = params['next_frame_number'] + + marker = MeshMarker(diveMesh, currFrameNumber, params['algo'], params['escape_iterations']) + + markerFileName = getMarkerFileNameForFrameNumber(currFrameNumber) + with open(markerFileName, 'wb') as markerHandle: + pickle.dump(marker, markerHandle) + + params['next_frame_number'] += 1 + + # Not really ideal to rebuild the image, but it *is* a good way + # to get the frame increment burned in, and to show that an + # increment/save happened. + updateView() + +def plusPrecisionClicked(event): + global params + params['digits_precision'] += 1 + updatePrecisionText() + plt.draw() + +def minusPrecisionClicked(event): + global params + params['digits_precision'] -= 1 + if params['digits_precision'] < 1: + params['digits_precision'] = 1 + updatePrecisionText() + plt.draw() + +def updatePrecisionText(): + global uiElements + global params + screenText = uiElements['precisionText'] + screenText.set_text(str(params['digits_precision'])) + +def plusIterationsClicked(event): + global params + params['escape_iterations'] += 16 + updateIterationsText() + plt.draw() + +def minusIterationsClicked(event): + global params + params['escape_iterations'] -= 16 + if params['escape_iterations'] < 16: + params['escape_iterations'] = 16 + updateIterationsText() + plt.draw() + +def updateIterationsText(): + global uiElements global params - origFrameIncrement = params.get('frameIncrement', 1) - frameIncrement = origFrameIncrement + 1 - params['frameIncrement'] = int(frameIncrement) + screenText = uiElements['iterationsText'] + screenText.set_text(str(params['escape_iterations'])) +def plusClicked(event): + global params + params['zoom_factor'] = round(params['zoom_factor'] + .01, 2) updateAdvanceText() plt.draw() def minusClicked(event): global params - origFrameIncrement = params.get('frameIncrement', 1) - frameIncrement = origFrameIncrement - if frameIncrement != 1: - frameIncrement -= 1 - params['frameIncrement'] = int(frameIncrement) - + params['zoom_factor'] = round(params['zoom_factor'] - .01, 2) + if params['zoom_factor'] < .1: + params['zoom_factor'] = .1 updateAdvanceText() plt.draw() def updateAdvanceText(): global uiElements - advanceText = uiElements['advanceText'] - global params - frameIncrement = params.get('frameIncrement', 1) - if frameIncrement == 1: - newText = "+ 1 frame" - else: - newText = "+ %d frames" % frameIncrement + screenText = uiElements['advanceText'] + screenText.set_text(str(params['zoom_factor'])) - advanceText.set_text(newText) - def updateTitle(): global uiElements diveMesh = uiElements['diveMesh'] - widthString = diveMesh.mathSupport.shorterStringFromFloat(diveMesh.imagMeshGenerator.baseWidth, 10) + widthString = diveMesh.mathSupport.shorterStringFromFloat(diveMesh.realMeshGenerator.baseWidth, 10) plt.suptitle("%s wide" % widthString) def onclick(event): @@ -203,37 +319,39 @@ def onclick(event): print(rebuiltClickedLocus) uiElements['lastClickedLocus'] = rebuiltClickedLocus -if __name__ == '__main__': +def getMeshFileNameForFrameNumber(frameNumber): + global params + + meshFileTitle = "%d.mesh.pik" % frameNumber + return os.path.join(params['wholeCacheFolder'], meshFileTitle) +def getMarkerFileNameForFrameNumber(frameNumber): global params - params = parse_options() - (params, remainingOptions) = parse_options() - print("First remaining: %s" % remainingOptions) - opts, secondRemaining = getopt.getopt(remainingOptions,["foo=", - ]) - print("Second remaining: %s" % secondRemaining) + meshFileTitle = "%d.marker.pik" % frameNumber + return os.path.join(params['project_name'], params['project_params']['exploration_markers_path'], meshFileTitle) - exit(0) +if __name__ == '__main__': - frameNumber = params.get('frameNumber', 0) + global params + params = parse_options() - centerString = params.get('centerString', None) + frameNumber = params.get('next_frame_number', 1) + center = params.get('center', params['math_support'].createComplex("(-1.0+0j)")) # Run the whole frame on first call, so we can read the # mesh dimensions from the result. Otherwise, we have to # guess at a lot of things like image size. - runFractalCall(frameNumber, centerString) + runFractalCallForCenter(center) global uiElements uiElements = {} # Start off with the parameterized locus as the center. - uiElements['lastClickedLocus'] = centerString + uiElements['lastClickedLocus'] = center imageFileTitle = "%d.tiff" % frameNumber - meshFileTitle = "%s.mesh.pik" % imageFileTitle - meshFileName = os.path.join(params['wholeCacheFolder'], meshFileTitle) + meshFileName = getMeshFileNameForFrameNumber(frameNumber) with open(meshFileName, 'rb') as meshHandle: diveMesh = pickle.load(meshHandle) @@ -249,46 +367,116 @@ def onclick(event): mainFigure.canvas.mpl_connect('button_press_event', onclick) - buttonWidth = 0.1 - buttonHeight = 0.06 + buttonHeight = 0.05 + largerButtonWidth = 0.08 + smallerButtonWidth = .04 + buttonWidth = smallerButtonWidth gutter = 0.01 positionX = gutter positionY = gutter + #### + # NOTE: + # When adding elements (Buttons OR Text) , you CAN'T reuse the + # capturing variable name, because the assignment apparently + # uses an internal reference. + #### + button1Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) - prevButton = mpl.widgets.Button(button1Axes, label="Prev") - prevButton.on_clicked(prevClicked) - uiElements['previousButton'] = prevButton + button = mpl.widgets.Button(button1Axes, label="|<") + button.on_clicked(lastMarkerClicked) + uiElements['lastMarkerButton'] = button + + buttonRedAxes = plt.axes([positionX,positionY+ buttonHeight + gutter,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(buttonRedAxes, label="re") + button.on_clicked(refreshClicked) + uiElements['refreshButton'] = button positionX += buttonWidth + gutter + buttonWidth = largerButtonWidth + button2Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) - nextButton = mpl.widgets.Button(button2Axes, label="Next") - nextButton.on_clicked(nextClicked) - uiElements['nextButton'] = nextButton + button = mpl.widgets.Button(button2Axes, label="+Prec") + button.on_clicked(plusPrecisionClicked) + uiElements['plusPrecisionButton'] = button + + positionX += buttonWidth + gutter + button3Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button3Axes, label="-Prec") + button.on_clicked(minusPrecisionClicked) + uiElements['minusPrecisionButton'] = button + positionX += buttonWidth + gutter - smallerButtonWidth = .04 + screenText1 = plt.text(positionX + .8, positionY, "(00)", horizontalalignment='left') + uiElements['precisionText'] = screenText1 - button3Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) - plusButton = mpl.widgets.Button(button3Axes, label="+") - plusButton.on_clicked(plusClicked) - uiElements['plusButton'] = plusButton + positionX += 0.05 + + button4Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button4Axes, label="+Iter") + button.on_clicked(plusIterationsClicked) + uiElements['plusIterationsButton'] = button + + positionX += buttonWidth + gutter + + button5Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button5Axes, label="-Iter") + button.on_clicked(minusIterationsClicked) + uiElements['minusIterationsButton'] = button + + positionX += buttonWidth + gutter + + screenText2 = plt.text(positionX + .6, positionY, "(00)", horizontalalignment='left') + uiElements['iterationsText'] = screenText2 + + positionX += 0.06 + + button6Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button6Axes, label="Out") + button.on_clicked(prevClicked) + uiElements['previousButton'] = button + + positionX += buttonWidth + gutter + + button7Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button7Axes, label="In") + button.on_clicked(nextClicked) + uiElements['nextButton'] = button + + positionX += buttonWidth + gutter + + button8Axes = plt.axes([positionX,positionY,buttonWidth,buttonHeight]) + button = mpl.widgets.Button(button8Axes, label="Save") + button.on_clicked(saveMarkerClicked) + uiElements['saveMarkerButton'] = button + + positionX += buttonWidth + gutter + + buttonWidth = smallerButtonWidth + + button9Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) + button = mpl.widgets.Button(button9Axes, label="+") + button.on_clicked(plusClicked) + uiElements['plusButton'] = button positionX += smallerButtonWidth + gutter - button4Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) - minusButton = mpl.widgets.Button(button4Axes, label="-") - minusButton.on_clicked(minusClicked) - uiElements['minusButton'] = minusButton + button10Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) + button = mpl.widgets.Button(button10Axes, label="-") + button.on_clicked(minusClicked) + uiElements['minusButton'] = button + + positionX += smallerButtonWidth + gutter - positionX += 0.8 # BUG: Not sure why adding another button width is wrong here - #positionX += smallerButtonWidth + gutter + screenText3 = plt.text(positionX + .1, positionY, "(scale)", horizontalalignment='left') + uiElements['advanceText'] = screenText3 - advanceText = plt.text(positionX, positionY, "+ frame", horizontalalignment='left') - uiElements['advanceText'] = advanceText + updatePrecisionText() + updateIterationsText() updateAdvanceText() updateTitle() diff --git a/strip_comments.py b/strip_comments.py new file mode 100644 index 0000000..5bee469 --- /dev/null +++ b/strip_comments.py @@ -0,0 +1,12 @@ + +import sys + +if __name__ == "__main__": + + fileName = sys.argv[1] + + with open(fileName, 'rt') as inHandle: + for currLine in inHandle: + if not currLine.startswith('#'): + sys.stdout.write(currLine) + diff --git a/test_fractalmath.py b/test_fractalmath.py index 62ad7de..60a8119 100644 --- a/test_fractalmath.py +++ b/test_fractalmath.py @@ -65,6 +65,56 @@ def test_interpolateLinear(self): # Not endpoint check self.assertAlmostEqual(2.5, self.mathSupport.interpolate('linear', startX, startY, endX, endY, 1.25)) + def test_interpolateRootTo(self): + startX = 1.0 + startY = 1.0 + multiplier = .92 + stepsToEnd = 4 + endX = 2.0 + endY = startY * (multiplier ** stepsToEnd) + + # Endpoints check + self.assertAlmostEqual(startY, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, startX))) + self.assertAlmostEqual(endY, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, endX))) + + # Non-endpoints check + targetX = .75 + stepsToTestPoint = 3 + midRangeValue = startY * (multiplier ** stepsToTestPoint) + self.assertAlmostEqual(midRangeValue, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, targetX))) + + targetX = .5 + stepsToTestPoint = 2 + midRangeValue = startY * (multiplier ** stepsToTestPoint) + self.assertAlmostEqual(midRangeValue, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, targetX))) + + def test_interpolateRootFrom(self): + multiplier = .92 + stepsToEnd = 4 + endX = 2.0 + endY = 1.0 + + startX = 1.0 + startY = endY * (multiplier ** stepsToEnd) + + # Endpoints check + self.assertAlmostEqual(startY, float(self.mathSupport.interpolate('root-from', startX, startY, endX, endY, startX))) + self.assertAlmostEqual(endY, float(self.mathSupport.interpolate('root-from', startX, startY, endX, endY, endX))) + + # Non-endpoints check + + # A little logically jumbled, but setting up forward-multiplied + # positions, based on the endY starting value. + targetX = .75 + stepsToTestPoint = 1 + midRangeValue = endY * (multiplier ** stepsToTestPoint) + self.assertAlmostEqual(midRangeValue, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, targetX))) + + targetX = .5 + stepsToTestPoint = 2 + midRangeValue = endY * (multiplier ** stepsToTestPoint) + self.assertAlmostEqual(midRangeValue, float(self.mathSupport.interpolate('root-to', startX, startY, endX, endY, targetX))) + def test_mandelbrot(self): # Native python isn't accurate past ~120 iterations. # So, we either reduce iterations, and require fewer decimal places to match. From f64cb1a0d2a848076eb7fb588c9e208ad3ba354c Mon Sep 17 00:00:00 2001 From: Clint Torres Date: Thu, 9 Sep 2021 16:49:27 -0700 Subject: [PATCH 37/44] Added files for the demonstration time_tinker project. Hopefully. As well as a description of the processe used for getting to an output animation. --- fractal.py | 4 +- time_tinker/edit/timelines/._time_tinker_demo | Bin 0 -> 227 bytes time_tinker/edit/timelines/time_tinker_demo | 983 ++++++++++++++++++ .../edit/timelines/time_tinker_demo_clean | 923 ++++++++++++++++ .../batch_0.txt | 180 ++++ .../batch_1.txt | 180 ++++ .../batch_2.txt | 180 ++++ .../batch_3.txt | 180 ++++ .../batch_4.txt | 180 ++++ .../batch_5.txt | 180 ++++ .../batch_6.txt | 180 ++++ .../batch_7.txt | 180 ++++ .../batch_8.txt | 180 ++++ .../batch_9.txt | 179 ++++ .../edit/timelines/time_tinker_sequence_01 | 303 ++++++ time_tinker/exploration/markers/1.marker.pik | Bin 0 -> 682 bytes time_tinker/exploration/markers/2.marker.pik | Bin 0 -> 650 bytes time_tinker/exploration/markers/3.marker.pik | Bin 0 -> 650 bytes time_tinker/exploration/markers/4.marker.pik | Bin 0 -> 650 bytes time_tinker/exploration/markers/5.marker.pik | Bin 0 -> 650 bytes time_tinker/exploration/markers/6.marker.pik | Bin 0 -> 591 bytes time_tinker/exploration/markers/7.marker.pik | Bin 0 -> 591 bytes time_tinker/exploration/markers/8.marker.pik | Bin 0 -> 591 bytes time_tinker/exploration/markers/9.marker.pik | Bin 0 -> 559 bytes time_tinker/output/tmp.txt | 0 time_tinker/params.json | 16 + time_tinker_explanation.txt | 96 ++ 27 files changed, 4122 insertions(+), 2 deletions(-) create mode 100644 time_tinker/edit/timelines/._time_tinker_demo create mode 100644 time_tinker/edit/timelines/time_tinker_demo create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_0.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_1.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_2.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_3.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_4.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_5.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_6.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_7.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_8.txt create mode 100644 time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_9.txt create mode 100644 time_tinker/edit/timelines/time_tinker_sequence_01 create mode 100644 time_tinker/exploration/markers/1.marker.pik create mode 100644 time_tinker/exploration/markers/2.marker.pik create mode 100644 time_tinker/exploration/markers/3.marker.pik create mode 100644 time_tinker/exploration/markers/4.marker.pik create mode 100644 time_tinker/exploration/markers/5.marker.pik create mode 100644 time_tinker/exploration/markers/6.marker.pik create mode 100644 time_tinker/exploration/markers/7.marker.pik create mode 100644 time_tinker/exploration/markers/8.marker.pik create mode 100644 time_tinker/exploration/markers/9.marker.pik create mode 100644 time_tinker/output/tmp.txt create mode 100644 time_tinker/params.json create mode 100644 time_tinker_explanation.txt diff --git a/fractal.py b/fractal.py index dc303fa..be4a09d 100644 --- a/fractal.py +++ b/fractal.py @@ -906,10 +906,10 @@ def run_batch_timeline(params): os.makedirs(output_folder_name) frame_numbers = [] - batch_frame_file + batch_frame_file = params['batch_frame_file'] with open(batch_frame_file, 'rt') as batch_handle: for currLine in batch_handle: - frameNumbers.append(int(currLine.strip())) + frame_numbers.append(int(currLine.strip())) #frame_file_names = [] for curr_frame_number in frame_numbers: diff --git a/time_tinker/edit/timelines/._time_tinker_demo b/time_tinker/edit/timelines/._time_tinker_demo new file mode 100644 index 0000000000000000000000000000000000000000..3b0db622cbcadbcffa6c1df76a97e327d37de077 GIT binary patch literal 227 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDI}@kSs9@gD;*5x_AdBnYYuq~iz>%cE%n znFbQ)XJ8Od&d=3LEGWoH)eA|jC~?h8&QHnAONVH<45S1Y7^DztauSP6N{drdQW8s2 kl>>r7%1TSpbSJNdz1UVn0p(?Z^F*TdTEM<^>O?p3~Qs8oA?)|G`03sy?N#KCC|IE z_*~WQqT;U8-o9;DtE%=f-0ezUc%Cnd(jT>ruiRs*L=3xYU4JXe#naQGiQ)R!u9dL2 z*2~s3N2c_NCXA;ek}1l#q&WkrKyn30kf|J%q6Bh4MiB{7966&PlO{1wl0tF#@X_cH zGm)DSmmgx-p6SN)%;DY?GRSj*C?!==$P|NJg3R({h?LOI!mx09afv(HTm6Xb@Sl)& zKishWRP8}{JtZ0Am{TBv0!kDN8Ihm_fZ|LbVZda9Bn$-=g7cgTiNHBgJqu(AJ&V1Q z2w27e7=REI6`|(@A`|K_!GCm_F!x-yJ}0f4rLm8%gqwP~sOrL7ccPt%Vd!)@H_KUF i`HzjOsP@#=HP!`xcW8O#c~R-wop!Bj%wtSR@cV0Fzvz1T^isFMU;jLy_=B~mI@zAe z+C??~WS*duW5^1e11UHuGEOL>z;ntuMkFT`Fr^6bjAn5B@KJJ{rM!>{B^SZ(mZFuT zNPllmQbcGh0+um^WI~D|pQgZMlHrTcm42d)SZ?SH>gG67HTkC~b>wfTaf<4;e=Q7s#*oib;_$lxbN)C*aawsG%U(=>4{{E-ot&)W}TC11zS5KcFB L8)oN+U77v{w>sL5 literal 0 HcmV?d00001 diff --git a/time_tinker/exploration/markers/3.marker.pik b/time_tinker/exploration/markers/3.marker.pik new file mode 100644 index 0000000000000000000000000000000000000000..975fadbd62e0a429e1606767e1bb754300a6634f GIT binary patch literal 650 zcma))&ubJh6vvBJ=}%#);;q!1B0J{SWD?tp)SoO{3L+ksA{jT`1SgXvlUdt?sQ28A z|F4-Du3CS#PCYC?6*$VdSK!y8Jh?mQHxxS6c|O$o(mHd~ zan}g1-nYx8bv}l}zU7VQb){>6+BV*r=aAqSc2~k(>w59xN;fgw`P!p|y_Kk1*__JS zCpF<@HU=4oC}*%hm~qT3P`etv6CHm5$bv}msL(6N=t6Deyv}?UJPYaA<*k23xRX5A$SGtYi=9d8_9IQp%$@Ww> zKB);O^9w>J6{w&r>zjcim5JJWPch5I61{2d?vir?eW5bpv} zZG^3tDY-ZP#<#oL4|?uZoScW9&Uz#DLisp34kIT;)#bmIwRFN;*S&IY&GWw2pRF@D z9e0iJ>Rr2BTIXYU*tfj#ysC8VPuj*?^9&Lk!|qDBZ@ON*xYSJyn_qjBu(uLbE1MHp z`=llu&&D9*5akRO2s4gZ&M^T5@q%y;0WL5>kPrZPP6bBZy~5hVaęM^F05WJEoeRq3q%*cfZmT7&~5E>D|JVkzII&k$bQ Mx{1^C{kF`m0mEwAr~m)} literal 0 HcmV?d00001 diff --git a/time_tinker/exploration/markers/6.marker.pik b/time_tinker/exploration/markers/6.marker.pik new file mode 100644 index 0000000000000000000000000000000000000000..48137028065213b08bdfe9fab6189c8e40cf68f4 GIT binary patch literal 591 zcmah`O-sZu6cj%ebys0g@g%G_MYgt0+ik^*xPGy$2qGRtGU8e^Q~B-O`dN1lJnoVw{m`5NKTmmg{*z$3bq03Wi^6?o2S1>v87-Rr6IL=!jnd7Otq+yorMOjEzB8^$ zYWPJxnJ8^6?6j2cmE%H`=Af>OlHX%&VRcINgDA&8??q){{%@-U)~2+mS#`ilT5;vh`Jj3g*S%p+|(c0-sK zqAOeH&=Zo5KdfT;M&}yueWsgl!UFGn;@gfd@WVJt5e^cBBN8QH%NP3?ck=j`Rum>_ z8bm41lE?v_sKiI)zo^fNo$xi;-uSGRxie?526-oOuHr@NC#52Yc#tAl8=K%}Q?l8r2SM+1GyX4s zlG$B9Pt9S%%)I2idGj*;elS%j4pawhOgKrYhWVbkdRX2Qso(VGtA5rSWj}9dT7l|; z0T;XP6%#~+h~NUVg+e_8%lxlGudko=di#<8Lo z*=QuB(y&%g{#K4+mZ(maD#3SsS3_-#^lqRcR`e%(UW zj_vrN9XQB#!*&=nx3*hN-&9G81^srKL}F#qc4^@x8!yk#87{z_8%+EEJUT=&AwvCJk-m`X;)6 zM0x02)MR2Ke1R7`KFfGy%;{EP5ywLznWFp*bE2WnaWWurB!#+3xe#Wz(BSy-Fulbi Sa%wEaiisrEy^rZEvOWQ2A#uN#1(IB(UA7h?_aS}Zg3T4H3`ueq zPbOSQ1-qH#JaQS(Q1;?Ta&}@{3aV2i-f8Hc&1j@x^(`ZT+7tybj(Rwh$uO`tY8rai zY}u~qv<%a7JFe3>I&L-`-86C2>^v$cdnm&BAW(1X#)7HX1C27|FQ6;3k#C7;%Q(p< ztdV>#WE&bILvPt_$FvOFaZ(^R!)fbXJI5hAP`Ca=EEh^ZYb?9rFlJsde7_9qDDb(U tlCU9SSb@e+IL3h|xO|Ej=gIA0lcCu~^o%C>CRrJhl!uWT6pFE@{{oFg#o+(| literal 0 HcmV?d00001 diff --git a/time_tinker/output/tmp.txt b/time_tinker/output/tmp.txt new file mode 100644 index 0000000..e69de29 diff --git a/time_tinker/params.json b/time_tinker/params.json new file mode 100644 index 0000000..cf4fc28 --- /dev/null +++ b/time_tinker/params.json @@ -0,0 +1,16 @@ +{ + "math_support": "flintcustom", + "exploration_mesh_width": "160", + "exploration_mesh_height": "120", + "exploration_default_algo_name": "mandelbrot_smooth", + "exploration_default_zoom_factor": "0.1", + "exploration_output_path": "exploration/output", + "exploration_markers_path": "exploration/markers", + "edit_markers_path": "edit/markers", + "edit_timelines_path": "edit/timelines", + "render_image_width": "80", + "render_image_height": "60", + "render_fps": "2.997", + "render_output_path": "output", + "render_exports_path": "exports" +} diff --git a/time_tinker_explanation.txt b/time_tinker_explanation.txt new file mode 100644 index 0000000..2616b07 --- /dev/null +++ b/time_tinker_explanation.txt @@ -0,0 +1,96 @@ +# Time Tinker Demo + +Everything except the intro title card and fade-ins was generated by the parameter keyframes in a timeline, at a native framerate of 23.976, ending at a window width around e-54. Since I don't have the intro title stuff working yet on the branch, I assembled this in Premiere (slapped the audio track and title card onto it, after planning for everything to start a few seconds later). + +The demo isn't very high resolution, but that's just to save render time. It also isn't the most interesting point, but that's just a result of not having explored a whole lot before committing to a plan. +(Note, the window widths all should be modified if the aspect ratio changes from 4:3). + +When constructing an animation, it really helps to keep the frameWidth and frameHeight (the json file's values should be what controls actual output) as small as is tolerable, so you can quickly see if you're on track with what you're trying to make happen. Same with FPS, where I usually work at 1/4 or 1/8 of full speed. + + +# How This Was Made + +## Project Creation + +A project is just a subfolder, which comes with an initial set of files and folders for organization. + +Created a 'project', which is a set of subdirs and a params file. + +python3.9 ./fractal.py --make-project="time_tinker" --math-support="flintcustom" + +Edited the time_tinker/params.json file to lower the exploration resoultion. + + +## Exploration + +Since this is a kind of work-out demo, I planned out a series of zooms, pans, and modifications to try to show off various mechanisms. + +Listening and measuring out the music track gave me specific (millisecond) time targets to try to hit with the parameter keyframes. Then, points of interest were saved as mesh markers, according the drawn up plan. + +Mesh markers are saved by clicking the 'save' button while exploring. + +For this demo, I started with a set of about 10 markers. To get more simply connected zoom phases, I actually started at the furthest dive point (the point with the smallest window width), and generally proceeded backwards through the exploration. + +python3.9 ./mesh_explore.py --project='time_tinker' --real-width=2.0 --imag-width=1.5 + +NOTE: As exploring, the current window leaves behind a command invocation that can start the exploration script at the same place again. This is helpful when working in phases, and also when trying to tinker with specific parameters. + + +## Timeline Creation + +Next, the markers were used to auto-generate a rough timeline as a basis to build something more complex from. All the times except for the endpoints will be moved around, but assigning them like this is a reasonable start. + +python3.9 ./markers_to_timeline.py --project="time_tinker" --marker-list=[9,8,7,6,5,4,3,2,1] --timeline-name='time_tinker_sequence_01' --duration=75.0 + +Then, I just went to town modifying and keyframes in the json by hand. I figured out a whole list of time offsets that I wanted to use, and made the keyframes from the zoom plan from above hit at those time offsets. + +I'd prefer have a 'load from script function' mechanism be the default for loading timelines (and already figured out how that will work), but this was the fastest way I could get to a demo. + +cp time_tinker/edit/timelines/time_tinker_sequence_01 time_tinker/edit/timelines/time_tinker_demo + +To keep sane, I kept illegal comment lines in the json file as I worked, then ran the whole thing through a comment-removal script at the end. The comments are still in 'time_tinker_demo'. + +python3.9 ./strip_comments.py time_tinker/edit/timelines/time_tinker_demo > time_tinker/edit/timelines/time_tinker_demo_clean + +Note(s): Interpolations are very sensitive, and easy to get wrong enough that your planned animation will suffer. + +One of the most important tricks seems to be to take the most-zoomed-in center definitions, and just copy/paste those centers on top of earlier marker center points, if the dive stays centered on that point. This makes it so there's no accidental drifting around of the focus point as the dive progresses. + + +## Rendering Setup + +The most direct way to create rendered frames is to run a timeline all at once, but there's some other improvements below if this isn't quite enough. + +When rendering for output, don't forget to up the frameWidth, frameHeight, and fps in the timeline file. + +python3.9 ./fractal.py --project='time_tinker' --timeline-name='time_tinker_demo_clean' + +This command does generate an output video file, but the individual frame .tiff files are still probably the most useful thing for using in a video project or setup. + + +### Batching + +Helper batching script can split up all the frame numbers in a timeline into files. Default is to round-robin frame numbers, so the overall animation can be viewed from start towards finish as it renders. + +python3.9 ./batch_files_for_timeline.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-count=10 + +Then, each of the batch scripts needs ot be run through fractal.py to generate .tiff frames. + +(These were thrown in a one-off shell script, but we'll have something more friendly soon.) + +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_0.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_1.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_2.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_3.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_4.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_5.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_6.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_7.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_8.txt' & +python3.9 ./fractal.py --project='time_tinker' --timeline-name="time_tinker_demo_clean" --batch-frame-file='time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_9.txt' & + +Finally, all the tiff files *can be* thrown into a single animation file, for easier viewing. Most likely though, using the output folder as .tiff sequence is the best way to use or import the individual frames. + +python3.9 compile_video.py --dir='time_tinker/output/time_tinker_demo_clean/' --out='time_tinker.mp4' --frame-count=1798 + + From 1ed7ecc892bbe75937be0f4aaf97b0d972c72fc0 Mon Sep 17 00:00:00 2001 From: clint Date: Tue, 14 Sep 2021 19:21:23 -0700 Subject: [PATCH 38/44] in-place library build of a native arb and native mpfr mandelbrot function. Lots of stuff in shambles and unorganized still, but it seems to be working --- Makefile | 54 +++++++++++++ arb_fractal_lib.c | 185 +++++++++++++++++++++++++++++++++++++++++++ arb_fractal_lib.h | 10 +++ arb_fractalmath.pyx | 54 +++++++++++++ fractalmath.py | 12 +++ mpfr_fractal_lib.c | 170 +++++++++++++++++++++++++++++++++++++++ mpfr_fractal_lib.h | 9 +++ mpfr_fractalmath.pyx | 55 +++++++++++++ setup_arb.py | 49 ++++++++++++ setup_mpfr.py | 46 +++++++++++ timing_comparison.py | 48 +++++++---- 11 files changed, 677 insertions(+), 15 deletions(-) create mode 100644 Makefile create mode 100644 arb_fractal_lib.c create mode 100644 arb_fractal_lib.h create mode 100644 arb_fractalmath.pyx create mode 100644 mpfr_fractal_lib.c create mode 100644 mpfr_fractal_lib.h create mode 100644 mpfr_fractalmath.pyx create mode 100644 setup_arb.py create mode 100644 setup_mpfr.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c400cb6 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ + +CFLAGS=-Wall +#INC=-I/usr/local/include/flint +INC= +#LIBS=-lm -lflint + +all: arb_pythonlib mpfr_pythonlib + +# In python, "import arb_fractalmath" +# arb_fractal_lib.c +# arb_fractal_lib.h +# (compiles into) +# libarbfractalmath.a +# +# arb_fractalmath.pyx +# (makes) +# arb_fractalmath.c +# arb_fractalmath.so + +# In python, "import mpfr_fractalmath" +# mpfr_fractal_lib.c +# mpfr_fractal_lib.h +# (compiles into) +# libmpfrfractalmath.a +# +# mpfr_fractalmath.pyx +# (makes) +# mpfr_fractalmath.c +# mpfr_fractalmath.so + +arb_pythonlib: libarbfractalmath.a arb_fractalmath.pyx + python3.9 setup_arb.py build_ext --inplace + +libarbfractalmath.a: arb_fractal_lib.o + ar -ru $@ $^ $(LIBS) + ranlib $@ +# $(CC) -shared $(LDFLAGS) -o $@ $^ $(LIBS) + +mpfr_pythonlib: libmpfrfractalmath.a mpfr_fractalmath.pyx + python3.9 setup_mpfr.py build_ext --inplace + +libmpfrfractalmath.a: mpfr_fractal_lib.o + ar -ru $@ $^ $(LIBS) + ranlib $@ + +%.o: %.c + $(CC) $(INC) $(CFLAGS) -c -o $@ $< + +clean: + python3.9 setup_arb.py clean --all + python3.9 setup_mpfr.py clean --all + rm -f *.o *.d *~ *.so *.a + rm -rf build + diff --git a/arb_fractal_lib.c b/arb_fractal_lib.c new file mode 100644 index 0000000..d57925c --- /dev/null +++ b/arb_fractal_lib.c @@ -0,0 +1,185 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "arb.h" +#include "acb.h" + +#define max(a,b) \ +({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; \ +}) + +#define min(a,b) \ +({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; \ +}) + +void arb_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, long *remaining_precision, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) +{ + // Step-wise algorithm modified from: + // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ + // Calculates (zreal + zimag)^2 to get + // zr^2 + 2*zr*zi + zi^2 + // Which is helpful, because for the iteration, zr^2 and zi^2 + // were already calculated. + // So subtract them, and the remaining term is just 2*zr*zi. + + // Big blocks of declarations, hang-overs from cython + // old-style C declaration rules. + arb_t temp; + arb_t zr_squared; + arb_t zi_squared; + arb_t temp_magnitude; + arb_t temp_sum_a; + arb_t temp_sub_a; + arb_t rad_squared; // Can't mix types, so need an arb for radius + + arb_t start_real; + arb_t start_imag; + arb_t last_z_real; + arb_t last_z_imag; + + bool precisionExceeded; + long bitsValue; + + arb_init(temp); + arb_init(zr_squared); + arb_init(zi_squared); + arb_init(temp_magnitude); + arb_init(temp_sum_a); + arb_init(temp_sub_a); + arb_init(rad_squared); + + arb_init(start_real); + arb_init(start_imag); + arb_init(last_z_real); + arb_init(last_z_imag); + + arb_set_str(start_real, start_real_str, prec); + arb_set_str(start_imag, start_imag_str, prec); + //printf("start_real \"%s\"\n", start_real_str); + //printf("start_imag \"%s\"\n", start_imag_str); + + // Magic conversion for converting prec to dps... roughly. + // (needed for back-to-string conversions) + long digits_precision = max(1, round(prec/3.3219280948873626)-1); + + // r^2 + arb_set_d(rad_squared, (double)(radius * radius)); + +// #print("starting stepping c accuracy:") +// #bitsValue = acb_rel_accuracy_bits(c) +// #print("bits") +// #print(bitsValue) +// +// # Apperently important not to use an expression as parameter +// # in place of prec, but to compute the value here. +// #prec = precin + 10 + + /* initialize Z as zero */ + arb_zero(last_z_real); + arb_zero(last_z_imag); + arb_zero(zr_squared); + arb_zero(zi_squared); + + precisionExceeded = false; + + // Since the zero iteration is just setting the starting values, it's not + // counted in the iterations count (so loop until *equal to* max_iter). + for(long currIter = 0; currIter <= max_iter; currIter++) + { + //char * curr_real = arb_get_str(acb_realref(last_z), digits_precision, 0); + //char * curr_imag = arb_get_str(acb_imagref(last_z), digits_precision, 0); + //printf("curr real \"%s\"\n", curr_real); + //printf("curr imag \"%s\"\n\n", curr_imag); + + *result = currIter; + arb_add(temp_magnitude, zr_squared, zi_squared, prec); + //#print(arb_get_str(temp_magnitude, 0, 0)); + + if(arb_gt(temp_magnitude, rad_squared)) + { + break; + } + + + arb_add(temp_sum_a, last_z_real, last_z_imag, prec); + arb_mul(temp, temp_sum_a, temp_sum_a, prec); + arb_sub(temp_sub_a, temp, zr_squared, prec); + arb_sub(temp, temp_sub_a, zi_squared, prec); + + arb_add(last_z_imag, temp, start_imag, prec); + + arb_sub(temp_sub_a, zr_squared, zi_squared, prec); + arb_add(last_z_real, temp_sub_a, start_real, prec); + + arb_mul(zr_squared, last_z_real, last_z_real, prec); + arb_mul(zi_squared, last_z_imag, last_z_imag, prec); + +// # This might be it! +// # Forcibly reset error to zero, when we've exceeded the +// # available precision +// # +// # I'd rather not zero this out, but I guess we might +// # as well, if it keeps stability of answers longer? + bitsValue = min(arb_rel_accuracy_bits(last_z_real), arb_rel_accuracy_bits(last_z_imag)); + //bitsValue = acb_rel_accuracy_bits(last_z); +// #if warningPrinted == False and bitsValue < 1: +// # print("Exceeded precision at iteration %d (%d)." % (currIter, bitsValue)) +// # warningPrinted = True +// # TODO: should be a 'min_precision' param, not a magic number, right? + if(bitsValue < 4) + { + precisionExceeded = true; + + mag_zero(arb_radref(last_z_real)); + mag_zero(arb_radref(last_z_imag)); + mag_zero(arb_radref(zr_squared)); + mag_zero(arb_radref(zi_squared)); + } + } + + if(precisionExceeded == true) + { + *remaining_precision = 0; + + mag_zero(arb_radref(last_z_real)); + mag_zero(arb_radref(last_z_imag)); + mag_zero(arb_radref(zr_squared)); + mag_zero(arb_radref(zi_squared)); + } + else + { + *remaining_precision = min(arb_rel_accuracy_bits(last_z_real), arb_rel_accuracy_bits(last_z_imag)); + } + + // Ask for 'more', knowing the trailing digits may be imprecise? + *last_z_real_str = arb_get_str(last_z_real, digits_precision, ARB_STR_MORE); + *last_z_imag_str = arb_get_str(last_z_imag, digits_precision, ARB_STR_MORE); + + arb_clear(temp); + arb_clear(zr_squared); + arb_clear(zi_squared); + arb_clear(temp_magnitude); + arb_clear(temp_sum_a); + arb_clear(temp_sub_a); + arb_clear(rad_squared); + + arb_clear(start_real); + arb_clear(start_imag); + arb_clear(last_z_real); + arb_clear(last_z_imag); +} + + diff --git a/arb_fractal_lib.h b/arb_fractal_lib.h new file mode 100644 index 0000000..33bf9cc --- /dev/null +++ b/arb_fractal_lib.h @@ -0,0 +1,10 @@ + +#include +#include +#include + +#include "arb.h" +#include "acb.h" + +void arb_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, long *remaining_precision, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec); + diff --git a/arb_fractalmath.pyx b/arb_fractalmath.pyx new file mode 100644 index 0000000..bfbde1b --- /dev/null +++ b/arb_fractalmath.pyx @@ -0,0 +1,54 @@ +# Lazily using flint's arb-from-string function here, though flint +# isn't really required... Should fix that... +# ACTUALLY, maybe flint is required, as an input parameter to start value? +import flint + +# Python usage: +# import flint +# import fractalmath_arb +# flintComplexValue = flint.arb(-1.76938, .00423) +# (answer, last_z, remaining_precision) = fractalmath_arb.mandelbrot_steps(flintComplexValue, 2.0, 255); + +# Organization Discussion +# +# arbfractalmath.h and arbfractalmath.h +# - compile into arbfractalmath.a +# +# arb_fractalmath.pyx +# - compiles into fractalmath_arb.so module via setup.py, invoked from makefile +# arb_fractalmath.pyx (defines python interface for arbfractalmath.a) +# - needs to accept python-flint arguments +# - needs to invoke arb_fractalmath's arb_mandelbrot_steps() with appropriate params + +cdef extern from "arb_fractal_lib.h": + void arb_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, long *remaining_precision, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) + +def mandelbrot_steps(s, radius, max_iter): + cdef long answer = 0 + cdef long remaining_precision = 0 + + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + start_real_py_str = s.real.str(more=True).encode('ascii') + start_imag_py_str = s.imag.str(more=True).encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + arb_mandelbrot_steps(&answer, &last_z_real_str, &last_z_imag_str, &remaining_precision, start_real_str, start_imag_str, float(radius), max_iter, flint.ctx.prec) + + # Extra step for string conversion back from bytes to python string + answer_real_py_str = last_z_real_str.decode('ascii') + answer_imag_py_str = last_z_imag_str.decode('ascii') + last_z = flint.acb(answer_real_py_str, answer_imag_py_str) + + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + return (answer, last_z, remaining_precision) + diff --git a/fractalmath.py b/fractalmath.py index 02e5a20..33fc228 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -9,6 +9,9 @@ import numpy as np +import arb_fractalmath +import mpfr_fractalmath + #FLINT_HIGH_PRECISION_SIZE = 53 # 53 is how many bits are in float64 #3.32 bits per digit, on average #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling @@ -1042,6 +1045,7 @@ def __init__(self): super().__init__() self.precisionType = 'flintcustom' + def mandelbrot(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) @@ -1051,6 +1055,14 @@ def mandelbrot(self, c, escapeRadius, maxIter): #print("answer: %s, remainingPrecision: %s" % (str(answer), str(remainingPrecision))) return(answer, lastZ) + def mandelbrot_arb(self, c, escapeRadius, maxIter): + (answer, lastZ, remainingPrecision) = arb_fractalmath.mandelbrot_steps(c, escapeRadius, maxIter) + return(answer, lastZ) + + def mandelbrot_mpfr(self, c, escapeRadius, maxIter): + (answer, lastZ) = mpfr_fractalmath.mandelbrot_steps(c, escapeRadius, maxIter) + return(answer, lastZ) + def mandelbrot_check_precision(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ #print("mandelbrot center: %s radius: %s maxIter: %s" % (str(c), str(escapeRadius), str(maxIter))) diff --git a/mpfr_fractal_lib.c b/mpfr_fractal_lib.c new file mode 100644 index 0000000..b6a4dfe --- /dev/null +++ b/mpfr_fractal_lib.c @@ -0,0 +1,170 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mpfr.h" + +#define max(a,b) \ +({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; \ +}) + +#define min(a,b) \ +({ \ + __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a < _b ? _a : _b; \ +}) + + +void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) +{ + // Step-wise algorithm modified from: + // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ + // Calculates (zreal + zimag)^2 to get + // zr^2 + 2*zr*zi + zi^2 + // Which is helpful, because for the iteration, zr^2 and zi^2 + // were already calculated. + // So subtract them, and the remaining term is just 2*zr*zi. + + // Big blocks of declarations, hang-overs from cython + // old-style C declaration rules. + mpfr_t temp; + mpfr_t zr_squared; + mpfr_t zi_squared; + mpfr_t temp_magnitude; + mpfr_t temp_sum_a; + mpfr_t temp_sub_a; + mpfr_t rad_squared; // Can't mix types, so need an arb for radius + + mpfr_t start_real; + mpfr_t start_imag; + mpfr_t last_z_real; + mpfr_t last_z_imag; + + //bool precisionExceeded; + //long bitsValue; + + //mpfr_exp_t print_exp; + long print_buffer_size = 65536; + char print_buffer[print_buffer_size]; + //char last_z_real_buffer[print_buffer_size]; + //char last_z_imag_buffer[print_buffer_size]; + + mpfr_set_default_prec(prec); + + mpfr_init(temp); + mpfr_init(zr_squared); + mpfr_init(zi_squared); + mpfr_init(temp_magnitude); + mpfr_init(temp_sum_a); + mpfr_init(temp_sub_a); + mpfr_init(rad_squared); + + mpfr_init2(start_real, prec); + mpfr_init(start_imag); + mpfr_init(last_z_real); + mpfr_init(last_z_imag); + + mpfr_set_str(start_real, start_real_str, 10, MPFR_RNDN); // 10 = base, not precision + mpfr_set_str(start_imag, start_imag_str, 10, MPFR_RNDN); + + //printf("start_real_str \"%s\"\n", start_real_str); + //printf("start_imag_str \"%s\"\n", start_imag_str); + + //printf("start_real: "); + //mpfr_out_str(stdout, 10,100,start_real, MPFR_RNDN); + //printf("\nstart_imag: "); + //mpfr_out_str(stdout, 10,100,start_imag, MPFR_RNDN); + //printf("\n"); + //fflush(stdout); + + + // Magic conversion for converting prec to dps... roughly. + // (needed for back-to-string conversions) + //long digits_precision = max(1, round(prec/3.3219280948873626)-1); + //int max_exponent_length = 9; + + // r^2 + mpfr_set_d(rad_squared, (double)(radius * radius), MPFR_RNDN); + +// #print("starting stepping c accuracy:") +// #bitsValue = acb_rel_accuracy_bits(c) +// #print("bits") +// #print(bitsValue) +// +// # Apperently important not to use an expression as parameter +// # in place of prec, but to compute the value here. +// #prec = precin + 10 + + /* initialize Z as zero */ + + mpfr_set_zero(last_z_real,1); + mpfr_set_zero(last_z_imag,1); + mpfr_set_zero(zr_squared,1); + mpfr_set_zero(zi_squared,1); + + //precisionExceeded = false; + + // Since the zero iteration is just setting the starting values, it's not + // counted in the iterations count (so loop until *equal to* max_iter). + for(long currIter = 0; currIter <= max_iter; currIter++) + { + *result = currIter; + mpfr_add(temp_magnitude, zr_squared, zi_squared, MPFR_RNDN); + + //mpfr_sprintf(print_buffer, "%Re", temp_magnitude); + //printf("%s\n", print_buffer); + //fflush(stdout); + + if(mpfr_cmp(temp_magnitude, rad_squared) > 0) + { + break; + } + + mpfr_add(temp_sum_a, last_z_real, last_z_imag, MPFR_RNDN); + mpfr_mul(temp, temp_sum_a, temp_sum_a, MPFR_RNDN); + mpfr_sub(temp_sub_a, temp, zr_squared, MPFR_RNDN); + mpfr_sub(temp, temp_sub_a, zi_squared, MPFR_RNDN); + + mpfr_add(last_z_imag, temp, start_imag, MPFR_RNDN); + + mpfr_sub(temp_sub_a, zr_squared, zi_squared, MPFR_RNDN); + mpfr_add(last_z_real, temp_sub_a, start_real, MPFR_RNDN); + + mpfr_mul(zr_squared, last_z_real, last_z_real, MPFR_RNDN); + mpfr_mul(zi_squared, last_z_imag, last_z_imag, MPFR_RNDN); + } + + // Print the value to the print buffer, then create a new string + // of appropriate length from there? + //mpfr_snprintf(print_buffer, digits_precision + max_exponent_length, "%Re", last_z_real); + mpfr_sprintf(print_buffer, "%Re", last_z_real); + *last_z_real_str = strdup(print_buffer); + //mpfr_snprintf(print_buffer, digits_precision + max_exponent_length, "%Re", last_z_imag); + mpfr_sprintf(print_buffer, "%Re", last_z_imag); + *last_z_imag_str = strdup(print_buffer); + + mpfr_clear(temp); + mpfr_clear(zr_squared); + mpfr_clear(zi_squared); + mpfr_clear(temp_magnitude); + mpfr_clear(temp_sum_a); + mpfr_clear(temp_sub_a); + mpfr_clear(rad_squared); + + mpfr_clear(start_real); + mpfr_clear(start_imag); + mpfr_clear(last_z_real); + mpfr_clear(last_z_imag); +} + + diff --git a/mpfr_fractal_lib.h b/mpfr_fractal_lib.h new file mode 100644 index 0000000..8eb154a --- /dev/null +++ b/mpfr_fractal_lib.h @@ -0,0 +1,9 @@ + +#include +#include +#include + +#include "mpfr.h" + +void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec); + diff --git a/mpfr_fractalmath.pyx b/mpfr_fractalmath.pyx new file mode 100644 index 0000000..03d8dd5 --- /dev/null +++ b/mpfr_fractalmath.pyx @@ -0,0 +1,55 @@ +# Lazily using flint's arb-from-string function here, though flint +# isn't really required... Should fix that... +# ACTUALLY, maybe flint is required, as an input parameter to start value? +import flint + +# Python usage: +# import flint +# import fractalmath_arb +# flintComplexValue = flint.arb(-1.76938, .00423) +# (answer, last_z, remaining_precision) = fractalmath_arb.mandelbrot_steps(flintComplexValue, 2.0, 255); + +# Organization Discussion +# +# arbfractalmath.h and arbfractalmath.h +# - compile into arbfractalmath.a +# +# arb_fractalmath.pyx +# - compiles into fractalmath_arb.so module via setup.py, invoked from makefile +# arb_fractalmath.pyx (defines python interface for arbfractalmath.a) +# - needs to accept python-flint arguments +# - needs to invoke arb_fractalmath's arb_mandelbrot_steps() with appropriate params + +cdef extern from "mpfr_fractal_lib.h": + void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) + +def mandelbrot_steps(s, radius, max_iter): + cdef long answer = 0 + + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + start_real_py_str = s.real.str(radius=False, more=True).encode('ascii') + start_imag_py_str = s.imag.str(radius=False, more=True).encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + #print(f"param's start_real_str \"{start_real_str}\"") + #print(f"param's start_imag_str \"{start_imag_str}\"") + mpfr_mandelbrot_steps(&answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, float(radius), max_iter, flint.ctx.prec) + + # Extra step for string conversion back from bytes to python string + answer_real_py_str = last_z_real_str.decode('ascii') + answer_imag_py_str = last_z_imag_str.decode('ascii') + last_z = flint.acb(answer_real_py_str, answer_imag_py_str) + + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + return (answer, last_z) + diff --git a/setup_arb.py b/setup_arb.py new file mode 100644 index 0000000..94d0934 --- /dev/null +++ b/setup_arb.py @@ -0,0 +1,49 @@ + +""" +Wrapping of custom arb-native impelementation of math functions for fractals. +""" + +import sys,os + +from setuptools import Extension, setup +from Cython.Build import cythonize + +import numpy +from numpy.distutils.system_info import default_include_dirs, default_lib_dirs + +#flint_include_dirs = [ +#print(f"numpy.get_include(): \"{numpy.get_include()}\"") +#print(f"defaults: \"{default_include_dirs}\"") + + +#default_include_dirs = default_include_dirs + [numpy.get_include()] + +arb_include_dirs = default_include_dirs.copy() +#arb_include_dirs.append(".") + +#for curr_dir in default_include_dirs: +# arb_include_dirs.append(curr_dir) +# flint_include_dirs.append(os.path.join(curr_dir, "arb")) + +#print(f"arb_include_dirs: \"{arb_include_dirs}\"") + +arb_library_dirs = default_lib_dirs.copy() +arb_library_dirs.append(".") # Trick to make an in-place library be includable + +#print(f"arb_library_dirs: \"{arb_library_dirs}\"") + +extensions = [ +# Extension('flint_fractal', ['flint_fractal.pyx'], libraries=["flintfractalmath"], library_dirs=['.'], include_dirs=['.']) + #Extension('flint_fractal', ['flint_fractal.pyx'], libraries=["arb", "flintfractalmath"], library_dirs=flint_library_dirs, include_dirs=flint_include_dirs) + + # The custom fractal library file name is "libarbfractalmath.a", + # so it's referred to as "arbfractalmath" here. + # The combination of the custom fractal library, and it's cython + # wrapper, is caled "fractalmath_arb", and can be imported by that name + # in a python script. + Extension('arb_fractalmath', ['arb_fractalmath.pyx'], libraries=["arb", "arbfractalmath"], library_dirs=arb_library_dirs, include_dirs=arb_include_dirs) + ] + +setup( + ext_modules = cythonize(extensions, language_level="3") + ) diff --git a/setup_mpfr.py b/setup_mpfr.py new file mode 100644 index 0000000..4854e91 --- /dev/null +++ b/setup_mpfr.py @@ -0,0 +1,46 @@ + +""" +Wrapping of custom arb-native impelementation of math functions for fractals. +""" + +import sys,os + +from setuptools import Extension, setup +from Cython.Build import cythonize + +import numpy +from numpy.distutils.system_info import default_include_dirs, default_lib_dirs + +#flint_include_dirs = [ +#print(f"numpy.get_include(): \"{numpy.get_include()}\"") +#print(f"defaults: \"{default_include_dirs}\"") + + +#default_include_dirs = default_include_dirs + [numpy.get_include()] + +mpfr_include_dirs = default_include_dirs.copy() +#arb_include_dirs.append(".") + +#for curr_dir in default_include_dirs: +# arb_include_dirs.append(curr_dir) +# flint_include_dirs.append(os.path.join(curr_dir, "arb")) + +#print(f"arb_include_dirs: \"{arb_include_dirs}\"") + +mpfr_library_dirs = default_lib_dirs.copy() +mpfr_library_dirs.append(".") # Trick to make an in-place library be includable + +#print(f"arb_library_dirs: \"{arb_library_dirs}\"") + +extensions = [ + # The custom fractal library file name is "libmpfrfractalmath.a", + # so it's referred to as "mpfrfractalmath" here. + # The combination of the custom fractal library, and it's cython + # wrapper, is caled "fractalmath_mpfr", and can be imported by that name + # in a python script. + Extension('mpfr_fractalmath', ['mpfr_fractalmath.pyx'], libraries=["mpfr", "mpfrfractalmath"], library_dirs=mpfr_library_dirs, include_dirs=mpfr_include_dirs) + ] + +setup( + ext_modules = cythonize(extensions, language_level="3") + ) diff --git a/timing_comparison.py b/timing_comparison.py index 4ad0300..903382a 100644 --- a/timing_comparison.py +++ b/timing_comparison.py @@ -8,30 +8,34 @@ # of the Edge of Infinity point ############################### -decimalPlaces = 16 -mandelbrotMaxIter = 255 -timerRepeatCount = 10000 +#decimalPlaces = 16 +#mandelbrotMaxIter = 255 +#timerRepeatCount = 1000 #decimalPlaces = 100 #mandelbrotMaxIter = 7000 -#timerRepeatCount = 1000 - -#decimalPlaces = 500 -#mandelbrotMaxIter = 35000 #timerRepeatCount = 100 +decimalPlaces = 500 +mandelbrotMaxIter = 35000 +timerRepeatCount = 10 + +#decimalPlaces = 1000 +#mandelbrotMaxIter = 150000 +#timerRepeatCount = 10 + #decimalPlaces = 1500 #mandelbrotMaxIter = 290000 -#timerRepeatCount = 1 +#timerRepeatCount = 10 #decimalPlaces = 2200 #mandelbrotMaxIter = 2700000 -#timerRepeatCount = 1 +#timerRepeatCount = 3 -centerString = '-1.7693831791+0.0042368479j' +#centerString = '-1.7693831791+0.0042368479j' -#centerString = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884+0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690j' +centerString = '-1.7693831791955150182138472860854737829057472636547514374655282165278881912647564588361634463895296673044858257818203031574874912384217194031282461951137475212550848062085787454772803303225167998662391124184542743017129214423639793169296754394181656831301342622793541423768572435783910849972056869527305207508191441734781061794290699753174911133714351734166117456520272756159178932042908932465102671790878414664628213755990650460738372283470777870306458882898202604001744348908388844962887074505853707095832039410323454920540534378406083202543002080240776000604510883136400112955848408048692373051275999457470473671317598770623174665886582323619043055508383245744667325990917947929662025877792679499645660786033978548337553694613673529685268652251959453874971983533677423356377699336623705491817104771909424891461757868378026419765129606526769522898056684520572284028039883286225342392455089357242793475261134567912757009627599451744942893765395578578179137375672787942139328379364197492987307203001409779081030965660422490200242892023288520510396495370720268688377880981691988243756770625044756604957687314689241825216171368155083773536285069411856763404065046728379696513318216144607821920824027797857625921782413101273331959639628043420017995090636222818019324038366814798438238540927811909247543259203596399903790614916969910733455656494065224399357601105072841234072044886928478250600986666987837467585182504661923879353345164721140166670708133939341595205900643816399988710049682525423837465035288755437535332464750001934325685009025423642056347757530380946799290663403877442547063918905505118152350633031870270153292586262005851702999524577716844595335385805548908126325397736860678083754587744508953038826602270140731059161305854135393230132058326419325267890909463907657787245924319849651660028931472549400310808097453589135197164989941931054546261747594558823583006437970585216728326439804654662779987947232731036794099604937358361568561860539962449610052967074013449293876425609214167615079422980743121960127425155223407999875999884+0.00423684791873677221492650717136799707668267091740375727945943565011234400080554515730243099502363650631353268335965257182300494805538736306127524814939292355930892834392050796724887904921986666045576626946900666103494014904714323725586979789908520656683202658064024115300378826789786394641622035341055102900456305723718684527210377325846307917512628774672005693326232806953822796755832517188873479124361430989485495501124096329421682827330693532171505367455526637382706988583456915684673202462211937384523487065290004627037270912806345336469007546411109669407622004367957958476890043040953462048335322273359167297049252960438077167010004209439515213189081508634843224000870136889065895088138204552309352430462782158649681507477960551795646930149740918234645225076516652086716320503880420325704104486903747569874284714830068830518642293591138468762031036739665945023607640585036218668993884533558262144356760232561099772965480869237201581493393664645179292489229735815054564819560512372223360478737722905493126886183195223860999679112529868068569066269441982065315045621648665342365985555395338571505660132833205426100878993922388367450899066133115360740011553934369094891871075717765803345451791394082587084902236263067329239601457074910340800624575627557843183429032397590197231701822237810014080715216554518295907984283453243435079846068568753674073705720148851912173075170531293303461334037951893251390031841730968751744420455098473808572196768667200405919237414872570568499964117282073597147065847005207507464373602310697663458722994227826891841411512573589860255142210602837087031792012000966856067648730369466249241454455795058209627003734747970517231654418272974375968391462696901395430614200747446035851467531667672250261488790789606038203516466311672579186528473826173569678887596534006782882871835938615860588356076208162301201143845805878804278970005959539875585918686455482194364808816650829446335905975254727342258614604501418057192598810476108766922935775177687770187001388743012888530139038318783958771247007926690j' @@ -66,22 +70,36 @@ def flintBeginningMandelbrotter(): radius = 2.0 return flintCustomMathSupport.mandelbrot_beginning(theCenter, radius, mandelbrotMaxIter) +def arbMandelbrotter(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.mandelbrot_arb(theCenter, radius, mandelbrotMaxIter) + +def mpfrMandelbrotter(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.mandelbrot_mpfr(theCenter, radius, mandelbrotMaxIter) + def decMandelbrotter(): theCenter = decMathSupport.createComplex(centerString) radius = 2.0 return decMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) def main(): - print("pure python (incorrect when iter > 100)") - print(timeit.Timer(pyMandelbrotter).timeit(number= timerRepeatCount)) +# print("pure python (incorrect when iter > 100)") +# print(timeit.Timer(pyMandelbrotter).timeit(number= timerRepeatCount)) print("flint") print(timeit.Timer(flintMandelbrotter).timeit(number= timerRepeatCount)) print("flint custom") print(timeit.Timer(flintCustomMandelbrotter).timeit(number= timerRepeatCount)) print("flint low magnification") print(timeit.Timer(flintBeginningMandelbrotter).timeit(number= timerRepeatCount)) - print("decimal") - print(timeit.Timer(decMandelbrotter).timeit(number= timerRepeatCount)) + print("arb steps") + print(timeit.Timer(arbMandelbrotter).timeit(number= timerRepeatCount)) + print("mpfr steps") + print(timeit.Timer(mpfrMandelbrotter).timeit(number= timerRepeatCount)) +# print("decimal") +# print(timeit.Timer(decMandelbrotter).timeit(number= timerRepeatCount)) if __name__ == '__main__': main() From e7db6e4514045e8a05305d1bab8c294520957cb7 Mon Sep 17 00:00:00 2001 From: clint Date: Thu, 16 Sep 2021 13:50:26 -0700 Subject: [PATCH 39/44] directory existence for exploration. Square function in mpfr. --- algo.py | 7 +++++++ mpfr_fractal_lib.c | 14 +++++++++++--- timing_comparison.py | 28 ++++++++++++++++++---------- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/algo.py b/algo.py index 9993685..dd60fc3 100644 --- a/algo.py +++ b/algo.py @@ -98,6 +98,13 @@ def mesh_setup(self): self.mesh_array = self.dive_mesh.generateMesh() mesh_base_name = u"%d.mesh.pik" % self.frame_number + + # Originally relied on the directory structure to exist, but after + # clearing intermediates a few times, it got annoying, so let's just + # cross our fingers that this all ends up where we hoped it would. + if not os.path.exists(self.output_folder_name): + os.makedirs(output_folder_name) + mesh_file_name = os.path.join(self.output_folder_name, mesh_base_name) with open(mesh_file_name, 'wb') as mesh_handle: pickle.dump(self.dive_mesh, mesh_handle) diff --git a/mpfr_fractal_lib.c b/mpfr_fractal_lib.c index b6a4dfe..ef73875 100644 --- a/mpfr_fractal_lib.c +++ b/mpfr_fractal_lib.c @@ -130,8 +130,16 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i break; } + // Looks like ~4 percent increase in speed by using mpfr_sqr where + // possible, instead of mpfr_mul (which is interesting, because + // arb_sqr just redirects to mul). + // + // The mpfr docs claim in-place operations work too (where I'm pretty + // sure I had bugs trying to use arb_add(a,a,b) functions), and + // should be preferred over temporary values... but I haven't run + // any comparisons on that, so am not really worried about it? mpfr_add(temp_sum_a, last_z_real, last_z_imag, MPFR_RNDN); - mpfr_mul(temp, temp_sum_a, temp_sum_a, MPFR_RNDN); + mpfr_sqr(temp, temp_sum_a, MPFR_RNDN); mpfr_sub(temp_sub_a, temp, zr_squared, MPFR_RNDN); mpfr_sub(temp, temp_sub_a, zi_squared, MPFR_RNDN); @@ -140,8 +148,8 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i mpfr_sub(temp_sub_a, zr_squared, zi_squared, MPFR_RNDN); mpfr_add(last_z_real, temp_sub_a, start_real, MPFR_RNDN); - mpfr_mul(zr_squared, last_z_real, last_z_real, MPFR_RNDN); - mpfr_mul(zi_squared, last_z_imag, last_z_imag, MPFR_RNDN); + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); } // Print the value to the print buffer, then create a new string diff --git a/timing_comparison.py b/timing_comparison.py index 903382a..ae2d8d1 100644 --- a/timing_comparison.py +++ b/timing_comparison.py @@ -12,13 +12,13 @@ #mandelbrotMaxIter = 255 #timerRepeatCount = 1000 -#decimalPlaces = 100 -#mandelbrotMaxIter = 7000 -#timerRepeatCount = 100 +decimalPlaces = 100 +mandelbrotMaxIter = 7000 +timerRepeatCount = 1000 -decimalPlaces = 500 -mandelbrotMaxIter = 35000 -timerRepeatCount = 10 +#decimalPlaces = 500 +#mandelbrotMaxIter = 35000 +#timerRepeatCount = 10 #decimalPlaces = 1000 #mandelbrotMaxIter = 150000 @@ -85,13 +85,21 @@ def decMandelbrotter(): radius = 2.0 return decMathSupport.mandelbrot(theCenter, radius, mandelbrotMaxIter) +def linspacer(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.createLinspaceAroundValuesCenter(theCenter, .0005, 2048) + def main(): +# print("linspace") +# print(timeit.Timer(linspacer).timeit(number= timerRepeatCount*5)) + # print("pure python (incorrect when iter > 100)") # print(timeit.Timer(pyMandelbrotter).timeit(number= timerRepeatCount)) - print("flint") - print(timeit.Timer(flintMandelbrotter).timeit(number= timerRepeatCount)) - print("flint custom") - print(timeit.Timer(flintCustomMandelbrotter).timeit(number= timerRepeatCount)) +# print("flint") +# print(timeit.Timer(flintMandelbrotter).timeit(number= timerRepeatCount)) +# print("flint custom") +# print(timeit.Timer(flintCustomMandelbrotter).timeit(number= timerRepeatCount)) print("flint low magnification") print(timeit.Timer(flintBeginningMandelbrotter).timeit(number= timerRepeatCount)) print("arb steps") From fbf85be1868f19d8d64b11cfd5fb163e0bde0385 Mon Sep 17 00:00:00 2001 From: clint Date: Thu, 23 Sep 2021 11:48:17 -0700 Subject: [PATCH 40/44] seems like new architecture for 3 native algos is starting to work enough to save progress --- algo.py | 14 +- divemesh.py | 11 + fractal.py | 5 +- fractalmath.py | 984 +++++++++++++++++++++++------------------ mandelbrot_smooth.py | 5 +- mandelbrot_solo.py | 19 +- mandeldistance.py | 26 +- markers_to_timeline.py | 5 - mesh_explore.py | 19 +- mpfr_fractal_lib.c | 182 ++++++-- mpfr_fractal_lib.h | 2 + mpfr_fractal_lib.o | Bin 0 -> 2984 bytes mpfr_fractalmath.pyx | 240 ++++++++-- setup_mpfr.py | 3 +- timing_comparison.py | 25 +- 15 files changed, 971 insertions(+), 569 deletions(-) create mode 100644 mpfr_fractal_lib.o diff --git a/algo.py b/algo.py index dd60fc3..bf18e0a 100644 --- a/algo.py +++ b/algo.py @@ -57,8 +57,12 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) # Algo should fill in these values self.mesh_array = None + self.mesh_real_array = None + self.mesh_imag_array = None self.counts_array = None self.last_values_array = None + self.last_values_real_array = None + self.last_values_imag_array = None self.processed_array = None self.output_image_file_name = None @@ -95,20 +99,20 @@ def mesh_setup(self): to handle this differently, or maybe to not save results out to file. """ - self.mesh_array = self.dive_mesh.generateMesh() - - mesh_base_name = u"%d.mesh.pik" % self.frame_number - # Originally relied on the directory structure to exist, but after # clearing intermediates a few times, it got annoying, so let's just # cross our fingers that this all ends up where we hoped it would. if not os.path.exists(self.output_folder_name): os.makedirs(output_folder_name) + mesh_base_name = u"%d.mesh.pik" % self.frame_number mesh_file_name = os.path.join(self.output_folder_name, mesh_base_name) with open(mesh_file_name, 'wb') as mesh_handle: pickle.dump(self.dive_mesh, mesh_handle) + self.mesh_real_array = self.dive_mesh.generateRealMesh() + self.mesh_imag_array = self.dive_mesh.generateImagMesh() + def generate_counts(self): """ Business end of getting results for a mesh """ raise NotImplementedError('Subclass must implement generate_counts()') @@ -174,7 +178,7 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) # Load, with optional default values - self.escape_radius = extra_params.get('escape_radius', 2.0) + self.escape_radius = extra_params.get('escape_radius', 10.0) self.max_escape_iterations = int(extra_params.get('max_escape_iterations', 255)) self.burn_in = extra_params.get('burn_in', False) self.palette = extra_params.get('palette', fp.FractalPalette()) diff --git a/divemesh.py b/divemesh.py index 7e8c75c..553f0a7 100644 --- a/divemesh.py +++ b/divemesh.py @@ -86,6 +86,7 @@ def __setstate__(self, state): """ mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, "DiveMathSupport":fm.DiveMathSupport} (mathSupportClassName, precisionString) = state['mathSupport'].split(':') @@ -101,6 +102,15 @@ def getCenter(self): """ Pretty common to want the mesh center, so assemble it from the generators. """ return self.mathSupport.createComplex(self.realMeshGenerator.valuesCenter, self.imagMeshGenerator.valuesCenter) + def generateRealMesh(self): + #print(f"Mesh math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") + return self.realMeshGenerator.generateForDiveMesh(self) + + imagMesh = self.imagMeshGenerator.generateForDiveMesh(self) + + def generateImagMesh(self): + return self.imagMeshGenerator.generateForDiveMesh(self) + def generateMesh(self): #print(f"Mesh math support is \"{self.mathSupport.precisionType}\" at {self.mathSupport.digitsPrecision()} digits and {self.mathSupport.precision()} bits") realMesh = self.realMeshGenerator.generateForDiveMesh(self) @@ -163,6 +173,7 @@ def __setstate__(self, state): """ mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, "DiveMathSupport":fm.DiveMathSupport} (mathSupportClassName, precisionString) = state['mathSupport'].split(':') diff --git a/fractal.py b/fractal.py index be4a09d..fdbc9b1 100644 --- a/fractal.py +++ b/fractal.py @@ -288,6 +288,7 @@ def __setstate__(self, state): """ mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, "DiveMathSupport":fm.DiveMathSupport} (mathSupportClassName, precisionString) = state['mathSupport'].split(':') @@ -637,11 +638,9 @@ def parse_options(): # First-pass at params to set up MathSupport because lots of # things depend on it for names and types. math_support_classes = {'native': fm.DiveMathSupport, + 'mpfr': fm.DiveMathSupportMPFR, 'flint': fm.DiveMathSupportFlint, 'flintcustom': fm.DiveMathSupportFlintCustom, - # maybe not complete?# 'gmp': fm.DiveMathSupportGmp, - # maybe not complete?# 'decimal': fm.DiveMathSupportDecimal, - # definitely not built yet.# 'libbf': fm.DiveMathSupportLibbf, } math_support = math_support_classes['native']() # Creates an instance for opt, arg in opts: diff --git a/fractalmath.py b/fractalmath.py index 33fc228..a952584 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -16,9 +16,9 @@ #3.32 bits per digit, on average #2200 was therefore, ~662 digits, got 54 frames down at .5 scaling -#FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(2800 * 3.32) #FLINT_HIGH_PRECISION_SIZE = int(2200 * 3.32) # 2200*3.32 = 7304, lol -#FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) # 2200*3.32 = 7304, lol +#FLINT_HIGH_PRECISION_SIZE = int(1800 * 3.32) #FLINT_HIGH_PRECISION_SIZE = int(1125 * 3.32) #FLINT_HIGH_PRECISION_SIZE = int(500 * 3.32) #FLINT_HIGH_PRECISION_SIZE = int(400 * 3.32) @@ -34,6 +34,26 @@ #FLINT_HIGH_PRECISION_SIZE = int(30 * 3.32) FLINT_HIGH_PRECISION_SIZE = int(16 * 3.32) +class DecimalComplex: + def __init__(self, realValue, imagValue): + super().__init__() + self.real = realValue + self.imag = imagValue + + def __repr__(self): + if self.real == None: + realString = "0" + else: # self.real != None: + realString = str(self.real) + if self.imag == None: + imagString = "+0" + else: # self.imag != None: + if self.imag >= 0.0: + imagString = f"+{str(self.imag)}" + else: + imagString = str(self.imag) + return f"({realString}{imagString}j)" + class DiveMathSupport: """ Toolbox for math functions that need to be type-aware, so we @@ -47,55 +67,160 @@ class DiveMathSupport: the Decimal library, and had to keep separate real and imaginary components of a complex number ourselves. - Trying to preserve types where possible, without forcing casting, because sometimes - all the math operations will already work for custom numeric types. + Trying to preserve types where possible, without forcing casting, + because sometimes all the math operations will already work for + custom numeric types. """ def __init__(self): self.precisionType = 'native' - def setPrecision(self, newPrecision): - """ Seems meaningless for pure python impelmentation, but seems like calling this shouldn't force a crash either? """ - pass; - #raise NotImplementedError("setPrecision is meaningless for base class, and must be implemented in DiveMathSupport sublcasses") + self.decimal = __import__('decimal') # Only imports if you instantiate this DiveMathSupport subclass. + self.defaultPrecisionSize = 53 + self.decimal.getcontext().prec = self.bitsToDigits(self.defaultPrecisionSize) + def digitsToBits(self, digits): + return round(digits * 3.3219) + + def bitsToDigits(self, bits): + return round(bits / 3.3219) + + # Decimal precision is in digits natively, so we can handle + # the in and out conversions to make bits work the right way too. + def setPrecision(self, newPrecision): + newDigits = self.bitsToDigits(newPrecision) + oldPrecision = self.decimal.getcontext().prec + self.decimal.getcontext().prec = newDigits + return oldPrecision + def precision(self): - return 53 # Heh - pytnon's native float64, right? + return self.digitsToBits(self.decimal.getcontext().prec) - def setDigitsPrecision(self, digits): - #print(f"Setting precision DIGITS: {digits}") - self.setPrecision(round(digits * 3.32)) + def setDigitsPrecision(self, newDigits): + oldPrecision = self.decimal.getcontext().prec + self.decimal.getcontext().prec = newDigits + return oldPrecision def digitsPrecision(self): - return round(self.precision() / 3.32) + return self.decimal.getcontext().prec def createComplex(self, *args): """ + Most of the time, keeping separate real and imaginary numbers is + going to be more efficient than creating complex representations. + Compatible complex types will return values for .real() and .imag() - - Native complex type just passes on all params to complex() """ - # Can't make a native complex from 2 strings though, so when we - # detect that, jam them into floats - if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str): - return complex(float(args[0]), float(args[1])) - #elif len(args) == 1 and isinstance(args[0], str): - # # Strip parens out of the definition string? + if len(args) == 2: + return DecimalComplex(self.decimal.Decimal(args[0]), self.decimal.Decimal(args[1])) + elif len(args) == 1: + if isinstance(args[0], str): + partsString = args[0] + + # Trim off surrounding parens, if present + if partsString.startswith('('): + partsString = partsString[1:] + if partsString.endswith(')'): + partsString = partsString[:-1] + + # Trim off the leading sign + firstIsPositive = True + if partsString.startswith('+'): + partsString = partsString[1:] + elif partsString.startswith('-'): + firstIsPositive = False + partsString = partsString[1:] + + # Trim off the trailing letter + lastIsImag = False + if partsString.endswith('j'): + lastIsImag = True + partsString = partsString[:-1] + + # Remaining string might have an internal sign. + # If there's no internal sign, then the whole remaining + # string is either the real or the complex + positiveParts = partsString.split('+') + negativeParts = partsString.split('-') + + realIsPositive = True + imagIsPositive = True + realPart = "" + imagPart = "" + if len(positiveParts) == 2: + realIsPositive = firstIsPositive + realPart = positiveParts[0] + imagIsPositive = True + imagPart = positiveParts[1] + elif len(negativeParts) == 2: + realIsPositive = firstIsPositive + realPart = negativeParts[0] + imagIsPositive = False + imagPart = negativeParts[1] + elif len(positiveParts) == 1 and len(negativeParts) == 1: + # No internal + or -, so it should just be a number + if lastIsImag == True: + imagPart = partsString + imagIsPositive = firstIsPositive + else: + realPart = partsString + realIsPositive = firstIsPositive + else: + raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()" % args[0]) + + preparedReal = '0.0' + preparedImag = '0.0' + if realPart != "": + if realIsPositive == True: + preparedReal = realPart + else: + preparedReal = "-%s" % realPart + + if imagPart != "": + if imagIsPositive == True: + preparedImag = imagPart + else: + preparedImag = "-%s" % imagPart + + print(f"preparedReal \"{preparedReal}\"") + print(f"preparedImag \"{preparedImag}\"") + return DecimalComplex(self.decimal.Decimal(preparedReal), self.decimal.Decimal(preparedImag)) + else: + if isinstance(args[0], complex): + return DecimalComplex(self.decimal.Decimal(args[0].real), self.decimal.Decimal(args[0].imag)) + else: + return DecimalComplex(self.decimal.Decimal(args[0]),self.decimal.Decimal(0))# Just a constant, so make a 0-imaginary value + elif len(args) == 0: + return DecimalComplex(self.decimal.Decimal(0),self.decimal.Decimal(0)) else: - return complex(*args) + raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") + + # Maybe don't need real native 'complex' anywhere now? + ## Can't make a native complex from 2 strings though, so when we + ## detect that, jam them into floats + #if len(args) == 2 and isinstance(args[0], str) and isinstance(args[1], str): + # return complex(float(args[0]), float(args[1])) + ##elif len(args) == 1 and isinstance(args[0], str): + ## # Strip parens out of the definition string? + #else: + # return complex(*args) def createFloat(self, floatValue): - return float(floatValue) + return self.decimal.Decimal(floatValue) def stringFromFloat(self, paramFloat): return str(paramFloat) def shorterStringFromFloat(self, paramFloat, places): - # I guess for native, just ignore the truncation request? - return self.stringFromFloat(paramFloat) + return round(paramFloat, places) def floor(self, value): return math.floor(value) + def arrayToStringArray(self, paramArray): + #stringifier = np.vectorize(str) + stringifier = np.vectorize(self.stringFromFloat) # So sublasses work better? + return stringifier(paramArray) + def scaleValueByFactorForIterations(self, startValue, overallZoomFactor, iterations): """ Shortcut calculate the starting point for the last frame's properties, @@ -117,8 +242,12 @@ def createLinspace(self, paramFirst, paramLast, quantity): dataRange = paramLast - paramFirst answers = np.zeros((quantity), dtype=object) + # Subtly, also likely forcing quantity to Decimal + oneLessQuantity = self.createFloat(quantity - 1) + + #print(f"first {paramFirst.__class__}, last {paramLast.__class__}, quantity {quantity.__class__}") for x in range(0, quantity): - answers[x] = paramFirst + dataRange * (x / (quantity - 1)) + answers[x] = paramFirst + dataRange * (x / oneLessQuantity) return answers @@ -132,7 +261,7 @@ def createLinspaceAroundValuesCenter(self, valuesCenter, spreadWidth, quantity): """ #print(f"createLinspaceAroundValuesCenter {valuesCenter}, {spreadWidth}, {quantity}") - return self.createLinspace(valuesCenter - spreadWidth * 0.5, valuesCenter + spreadWidth * 0.5, quantity) + return self.createLinspace(valuesCenter - spreadWidth * self.createFloat(0.5), valuesCenter + spreadWidth * self.createFloat(0.5), quantity) def interpolate(self, transitionType, startX, startY, endX, endY, targetX, extraParams={}): """ @@ -392,210 +521,413 @@ def interpolateLinear(self, startX, startY, endX, endY, targetX): else: return (((targetX - startX)/ (endX - startX)) * (endY - startY)) + startY - def mandelbrot(self, c, escapeRadius, maxIter): - """ - Now that smoothing is handled separately, the native python implementation - COULD work for flint as well, except it uses 'complex(0,0)' instead - of self.createComplex(0,0). - The reason for this is just as below - to reduce one extra function call in - the core calculation. - - Could just call julia with a zero start here, but seems wiser to - not have one extra function call in the core of the calculation? - """ - z = complex(0,0) - n = 0 - - for currIter in range(maxIter + 1): - n = currIter - - if abs(z) > escapeRadius: - break - - z = z*z + c + def julia(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): + currShape = realValues.shape + results = np.array(currShape, dtype=object) + lastRealValues = np.array(currShape, dtype=object) + lastImagValues = np.array(currShape, dtype=object) + + # numpyArray.shape returns (rows, columns) + for y in range(currShape[0]): + for x in range(currShape[1]): + (results[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.juliaSingle(realValues[y,x], imagValues[y,x], realJuliaValue, imagJuliaValue, escapeRadius, maxIter) - #print("input: %s" % (str(c))) - #print("answer: %s, lastZ: %s" % (str(n), str(z))) - return (n, z) + return (results, lastRealValues, lastImagValues) - def julia(self, c, z0, escapeRadius, maxIter): + def juliaSingle(self, realValue, imagValue, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): """ + Default behavior is 2D decimal to 2D decimal. + Some interesting c values c = complex(-0.8, 0.156) c = complex(-0.4, 0.6) c = complex(-0.7269, 0.1889) + """ + radSquared = escapeRadius * escapeRadius + result = 0 - Looks like this implementation is able to handle flint types, - now that smoothing is handled separately. + # Perhaps no longer relevant, but observations about python math: + # fabs(z) returns the modulus of a complex number, which is the + # distance to 0 (on a 2x2 cartesian plane), and NORMALLY, we'd + # check zRealSquared + zImagSquared against the escape-value-squared, + # but in python it looks like it takes almost 1/2 the time to do + # abs(Z) (when Z is a complex number), so checking against the + # un-squared radius is even faster somehow. - fabs(z) returns the modulus of a complex number, which is the - distance to 0 (on a 2x2 cartesian plane) - However, instead we just square both sides of the inequality to - avoid the sqrt - e.g.: while (float((z.real**2)+(z.imag**2))) <= escapeSquared and n < maxIter: + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) - However, for python, it looks like it takes almost 1/2 of the time to do abs(Z) - """ - z = z0 - n = 0 + zReal = self.decimal.Decimal(realJuliaValue) + zImag = self.decimal.Decimal(imagJuliaValue) + zrSquared = zReal * zReal + ziSquared = zImag * zImag for currIter in range(maxIter + 1): - n = currIter + result = currIter - if abs(z) > escapeRadius: + if (zrSquared + ziSquared) > radSquared: break - z = z*z + c + # Below is 2(z.real*z.imag) + c.imag, but without an extra multiply + partSum = zReal * zImag + zImag = partSum + partSum + imagDecimal - return (n, z) + zReal = zrSquared - ziSquared + realDecimal - def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): - # TODO: seems like the order of params to smoothAfterCalculation are - # all needlessly scrambled up? + zrSquared = zReal * zReal + ziSquared = zImag * zImag - if endingIter == maxIter: - return float(maxIter) - else: - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - # Note: Results in a float. We think. - #if endingIter != 1: - # print("iter was %d" % endingIter) - #print("z: \"%s\" max_iter: %d iter: %d" % (endingZ, maxIter, endingIter)) - #return endingIter + 1 - math.log(math.log2(abs(endingZ))) - #return endingIter + 1 - math.log(math.log2(abs(endingZ)) / math.log(2.0)) - return endingIter + 1 - self.twoLogsHelper(endingZ, escapeRadius) / math.log(2.0) + return (result, zReal, zImag) - def justTwoLogs(self, value): - return math.log(math.log(value)) + def mandelbrot(self, realValues, imagValues, escapeRadius, maxIter): + currShape = realValues.shape - def twoLogsHelper(self, value, radius): + results = np.empty(currShape, dtype=object) + lastRealValues = np.empty(currShape, dtype=object) + lastImagValues = np.empty(currShape, dtype=object) + + # numpyArray.shape returns (rows, columns) + for y in range(currShape[0]): + for x in range(currShape[1]): + (results[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.mandelbrotSingle(realValues[y,x], imagValues[y,x], escapeRadius, maxIter) + + return (results, lastRealValues, lastImagValues) + + def mandelbrotSingle(self, realValue, imagValue, escapeRadius, maxIter): + """ + Normally a sub-library would set up appropriate mandelbrot and + julia calls, but since native implementation is right here, we + have a special case where we know which implementation to use, + and can call it directly """ - The UNoptimized smoothing calculation. + return self.juliaSingle(realValue, imagValue, 0, 0, escapeRadius, maxIter) - sn = n - ( ln( ln(abs(value))/ln(radius) ) / ln(2.0)) + def smoothAfterCalculation(self, lastRealValues, lastImagValues, endingIters, maxIter, escapeRadius): + """ + Not at all ideal to iterate over the numpy array, but I can't see a + trivial way to iterate all the arrays in parallel at the moment. + Probably a 'pass-the-index' technique of some kind. + """ + currShape = lastRealValues.shape - Of which, we're just running the upper part here, because the ln(2.0) - and subtraction don't need extra precision support. - - So, this calculates: - ln(ln(abs(value))/ln(radius)) + results = np.empty(currShape, dtype=object) + + # numpyArray.shape returns (rows, columns) + for y in range(currShape[0]): + for x in range(currShape[1]): + results[y,x] = self.smoothAfterCalculationSingle(lastRealValues[y,x], lastImagValues[y,x], endingIters[y,x], maxIter, escapeRadius) + + return results + + def smoothAfterCalculationSingle(self, lastReal, lastImag, endingIter, maxIter, escapeRadius): + # Below from: + # https://iquilezles.org/www/articles/mset_smooth/mset_smooth.htm + # + # // smooth iteration count + # //float sn = n - log(log(length(z))/log(B))/log(2.0); + # + # // equivalent optimized smooth iteration count + # float sn = n - log2(log2(dot(z,z))) + 4.0; + # + # But, that seems to be for B = 2.0 + # I'm trying to use escape radius of 10.0 + # So, log10(10.0) = 1.0, which kills the denominator inside the logs. + # Which brings the expression to + # log(log(length(z))) / log(2.0) + if endingIter == maxIter: + return self.createFloat(maxIter) + else: + sqMagnitude = self.decimal.Decimal(lastReal) ** 2 + self.decimal.Decimal(lastImag) ** 2 + return endingIter - ((sqMagnitude.sqrt().ln().ln()) / (self.decimal.Decimal('2').ln())) + + def mandelbrotDistanceEstimate(self, realValues, imagValues, escapeRadius, maxIter): + currShape = realValues.shape + + results = np.empty(currShape, dtype=object) + lastRealValues = np.empty(currShape, dtype=object) + lastImagValues = np.empty(currShape, dtype=object) + + # numpyArray.shape returns (rows, columns) + for y in range(currShape[0]): + for x in range(currShape[1]): + (results[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.mandelbrotDistanceEstimateSingle(realValues[y,x], imagValues[y,x], escapeRadius, maxIter) + + return (results, lastRealValues, lastImagValues) + + def mandelbrotDistanceEstimateSingle(self, realValue, imagValue, escapeRadius, maxIter): + """ + Wonderfully slow implementation of distance estimate, but we need + a fallback/baseline, so here we go. """ - return math.log(math.log(abs(value))/math.log(radius)) + radSquared = escapeRadius * escapeRadius + result = 0 - def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): -# TODO: profile to make sure the exclusion is worth the extra multiplications -# c2 = c.real*c.real + c.imag*c.imag -# # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm -# if 256.0*c2*c2 - 96.0*c2 + 32.0*c.real - 3.0 < 0.0: -# return 0.0 -# # skip computation inside M2 - http://iquilezles.org/www/articles/mset_2bulb/mset2bulb.htm -# if 16.0*(c2+2.0*c.real+1.0) - 1.0 < 0.0: -# return 0.0 + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) - didEscape = False - z = complex(0,0) - dz = complex(0,0) # (0+0j) start for mandelbrot, (1+0j) for julia - absOfZ = 0.0 # Will re-use the final result for smoothing - n = 0 + zReal = self.decimal.Decimal(0) + zImag = self.decimal.Decimal(0) + zrSquared = zReal * zReal + ziSquared = zImag * zImag + # Mandelbrot derivative initialize to (0+0j), but julia is (1+0j), FYI + dzReal = self.decimal.Decimal(0) + dzImag = self.decimal.Decimal(0) + for currIter in range(maxIter + 1): - n = currIter - - if absOfZ > escapeRadius: + result = currIter + + if (zrSquared + ziSquared) > radSquared: break - # Z' -> 2·Z·Z' + 1 - dz = 2.0 * (z*dz) + 1 + # Z' -> 2·Z·Z' + 1 + # FYI: No "+1" for julia, dz is just 2·Z·Z' + # (a + bi) * (c + di) + # = ac + adi + bci - bd = ac - bd + (ad+bc)i + partSum = zReal * dzReal - zImag * dzImag + newDzReal = partSum + partSum + 1 # Don't stomp dz yet + dzImag = zReal * dzImag + zImag * dzReal + dzReal = newDzReal # Now safe to stomp + # Z -> Z² + c - z = z*z + c + # Below is 2(z.real*z.imag) + c.imag, but without an extra multiply + partSum = zReal * zImag + zImag = partSum + partSum + imagDecimal + zReal = zrSquared - ziSquared + realDecimal + + zrSquared = zReal * zReal + ziSquared = zImag * zImag + + # Tried to extract the distance smoothing from this a few times, + # but it seems a lot messier to pass around the (needed) derivative + # as well, so we just go ahead and return an extra term here, knowing + # this theoretically could be better handled in the 'normal' two steps. + if result == maxIter: + return (result, zReal, zImag) + else: + realPart = zReal * zReal + imagPart = zImag * zImag + zMagnitude = (realPart + imagPart).sqrt() + + realPart = dzReal * dzReal + imagPart = dzImag * dzImag + dzMagnitude = (realPart + imagPart).sqrt() + + ## Can't take logs of zMagnitude when <= 0, + ## and can't divide by zero-valued dzMagnitude. + ## (though these shouldn't ever be, because of sqrt?) + #if zMagnitude <= 0 or dzMagnitude <= 0: + # return (result, zReal, zImag) + # + # Current most likely: + #return ((zMagnitude * (zMagnitude.ln()) / dzMagnitude), zReal, zImag) + # But need to include the actual result, right? + #return (result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude), zReal, zImag) + tmpAnswer = result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude) + if result > 0: + return (tmpAnswer, zReal, zImag) + else: + return (self.decimal.Decimal(0), zReal, zImag) + + # More math, slightly less blown out? + #return ((zMagnitude * zMagnitude).ln() * zMagnitude / dzMagnitude, zReal, zImag) + # People seem to like this for 2D: + #return (result - self.decimal.Decimal('0.5') * zMagnitude.ln() * zMagnitude / dzMagnitude, zReal, zImag) + + # somewhere on: https://en.wikibooks.org/wiki/Fractals/Iterations_in_the_complex_plane/demm + #result:=2*log2(sqrt(xy2))*sqrt(xy2)/sqrt(sqr(eDx)+sqr(eDy)); + #return ((2 * zMagnitude.ln() * zMagnitude / dzMagnitude), zReal, zImag) + + # Perhaps identical to current most likely, but looks very close anyway + # From https://en.wikibooks.org/wiki/Fractals/Iterations_in_the_complex_plane/demm + #R = -K*Math.log(Math.log(R2+I2)*Math.sqrt((R2+I2)/(Dr*Dr+Di*Di))); // compute distance + #return (result - (2 * (zReal * zReal + zImag * zImag).ln() * zMagnitude / dzMagnitude), zReal, zImag) + + def juliaDistanceEstimate(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): + currShape = realValues.shape + + results = np.empty(currShape, dtype=object) + lastRealValues = np.empty(currShape, dtype=object) + lastImagValues = np.empty(currShape, dtype=object) + + # numpyArray.shape returns (rows, columns) + for y in range(currShape[0]): + for x in range(currShape[1]): + (results[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.juliaDistanceEstimateSingle(realValues[y,x], imagValues[y,x], realJuliaValue, imagJuliaValue, escapeRadius, maxIter) - absOfZ = abs(z) + return (results, lastRealValues, lastImagValues) + + def juliaDistanceEstimateSingle(self, realValue, imagValue, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): + """ + Wonderfully slow implementation of distance estimate, but we need + a fallback/baseline, so here we go. + """ + radSquared = escapeRadius * escapeRadius + result = 0 - if n == maxIter: - return (n, 0.0) - else: - return (n, absOfZ * math.log(absOfZ) / abs(dz)) + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) -# lastIter = 0 -# for i in range(0, maxIter): -# if abs(z) > escapeRadius: -# didEscape = True -# break + zReal = self.decimal.Decimal(realJuliaValue) + zImag = self.decimal.Decimal(imagJuliaValue) + zrSquared = zReal * zReal + ziSquared = zImag * zImag + + # Julia derivative inittialize to (1+0j), but mandelbrot is (0+0j), FYI + dzReal = self.decimal.Decimal(1) + dzImag = self.decimal.Decimal(0) + + for currIter in range(maxIter + 1): + result = currIter + + if (zrSquared + ziSquared) > radSquared: + break + + # Z' -> 2·Z·Z' + 1 + # FYI: There's an extra "+1" for mandelbrot dzReal, but + # julia dz is just 2·Z·Z' + # (a + bi) * (c + di) + # = ac + adi + bci - bd = ac - bd + (ad+bc)i + partSum = zReal * dzReal - zImag * dzImag + newDzReal = partSum + partSum # Don't stomp dz yet + dzImag = zReal * dzImag + zImag * dzReal + dzReal = newDzReal # Now safe to stomp + + # Z -> Z² + c + # Below is 2(z.real*z.imag) + c.imag, but without an extra multiply + partSum = zReal * zImag + zImag = partSum + partSum + imagDecimal + zReal = zrSquared - ziSquared + realDecimal + + zrSquared = zReal * zReal + ziSquared = zImag * zImag + + # Tried to extract the distance smoothing from this a few times, + # but it seems a lot messier to pass around the (needed) derivative + # as well, so we just go ahead and return an extra term here, knowing + # this theoretically could be better handled in the 'normal' two steps. + if result == maxIter: + return (result, zReal, zImag) + else: + realPart = zReal * zReal + imagPart = zImag * zImag + zMagnitude = (realPart + imagPart).sqrt() + + realPart = dzReal * dzReal + imagPart = dzImag * dzImag + dzMagnitude = (realPart + imagPart).sqrt() + + ## Can't take logs of zMagnitude when <= 0, + ## and can't divide by zero-valued dzMagnitude. + ## (though these shouldn't ever be, because of sqrt?) + #if zMagnitude <= 0 or dzMagnitude <= 0: + # return (result, zReal, zImag) + #return ((zMagnitude * (zMagnitude.ln()) / dzMagnitude), zReal, zImag) + + # Lots of commentary/options above in mandelbrot distance estimate, + # this is just pasted in to keep in line with whatever was chosen + # up there. + tmpAnswer = result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude) + if result > 0: + return (tmpAnswer, zReal, zImag) + else: + return (self.decimal.Decimal(0), zReal, zImag) + +# def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): +# ##print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) +# #if endingIter == maxIter or rawValue <= 0.0: +# # return 0.0 +# #else: +# # zoomValue = float(math.pow((1/scaleRange) * rawValue / 0.1, 0.1)) +# # return self.clamp(zoomValue, 0.0, 1.0) # -# # Z' -> 2·Z·Z' + 1 -# dz = 2.0 * (z*dz) + 1 -# # Z -> Z² + c -# z = z*z + c +# val = float(rawValue) +# if val < 0.0: +# val = 0.0 +# zoo = .1 +# zoom_level = 1. / scaleRange +# d = self.clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); +# return float(d) # -# lastIter += 1 +# def clamp(self, num, min_value, max_value): +# return max(min(num, max_value), min_value) # -# if didEscape == False: -# return (lastIter, 0.0) -# else: -# absZ = abs(z) # Save an extra multiply -# return (lastIter, absZ * math.log(absZ) / abs(dz)) - +# def squared_modulus(self, z): +# return ((z.real*z.real)+(z.imag*z.imag)) + +class DiveMathSupportMPFR(DiveMathSupport): + def __init__(self): + super().__init__() - def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): -# #print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) -# if endingIter == maxIter or rawValue <= 0.0: -# return 0.0 -# else: -# zoomValue = float(math.pow((1/scaleRange) * rawValue / 0.1, 0.1)) -# return self.clamp(zoomValue, 0.0, 1.0) + # Only imports if you instantiate this DiveMathSupport subclass. + self.mpfrlib = __import__('mpfr_fractalmath') - val = float(rawValue) - if val < 0.0: - val = 0.0 - zoo = .1 - zoom_level = 1. / scaleRange - d = self.clamp( pow(zoom_level * val/zoo,0.1), 0.0, 1.0 ); - return float(d) + # Unlike super()'s Decimal which keeps digits of precision, + # mpfr natively keeps bits, so we'll keep bits too, since + # this number is used a lot + self.defaultPrecisionSize = 53 + self.currPrecision = self.defaultPrecisionSize + self.precisionType = 'mpfr' - def clamp(self, num, min_value, max_value): - return max(min(num, max_value), min_value) + # Now that Decimal is native DiveMathSupport, there are actually + # 2 libraries now to keep at the same precision, so also send + # precision changes to super() - def orig_mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): - """ This seems to be old and a bit buggy. At the least it's off by one - for iters, because it doesn't update n and range quite the same? """ - c2 = c.real*c.real + c.imag*c.imag - # skip computation inside M1 - http://iquilezles.org/www/articles/mset_1bulb/mset1bulb.htm - if 256.0*c2*c2 - 96.0*c2 + 32.0*c.real - 3.0 < 0.0: - return 0.0 - # skip computation inside M2 - http://iquilezles.org/www/articles/mset_2bulb/mset2bulb.htm - if 16.0*(c2+2.0*c.real+1.0) - 1.0 < 0.0: - return 0.0 + def setPrecision(self, newPrecision): + super().setPrecision(newPrecision) # Also set Decimal's precision + oldPrecision = self.currPrecision + self.currPrecision = newPrecision + return oldPrecision - # iterate - di = 1.0; - z = complex(0.0); - m2 = 0.0; - dz = complex(0.0); - for i in range(0,maxIter): - if m2>escapeRadius : - di=0.0 - break + def precision(self): + return self.currPrecision - # Z' -> 2·Z·Z' + 1 - dz = 2.0*complex(z.real*dz.real-z.imag*dz.imag, z.real*dz.imag + z.imag*dz.real) + complex(1.0,0.0); - - # Z -> Z² + c - z = complex( z.real*z.real - z.imag*z.imag, 2.0*z.real*z.imag ) + c; - - m2 = self.squared_modulus(z) - - # distance - # d(c) = |Z|·log|Z|/|Z'| - d = 0.5*math.sqrt(self.squared_modulus(z)/self.squared_modulus(dz))*math.log(self.squared_modulus(z)); - if di>0.5: - d=0.0 + def setDigitsPrecision(self, newDigits): + super().setDigitsPrecision(newDigits) + newPrecision = self.digitsToBits(newDigits) + oldPrecision = self.currPrecision + self.currPrecision = newPrecision + return oldPrecision - return d + def digitsPrecision(self): + return self.bitsToDigits(self.currPrecision) + + def julia(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations): + """ + Default behavior is 2D decimal to 2D decimal. + """ + return self.mpfrlib.julia_2d_pydecimal_to_decimal(realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations, self.currPrecision) + + def juliaSingle(self, realValue, imagValue, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations): + realValues = np.array((realValue), dtype=object) + imagValues = np.array((imagValue), dtype=object) + return self.mpfrlib.julia_2d_pydecimal_to_decimal(realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations, self.currPrecision) + + def juliaToString(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations): + """ + Returns string types to dodge an extra string-to-decimal conversion. + Useful when you know further processing (e.g. smoothing) will happen. + """ + return self.mpfrlib.julia_2d_pydecimal_to_string(realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations, self.currPrecision) + + def mandelbrot(self, realValues, imagValues, escapeRadius, maxIterations): + """ + Default behavior is 2D decimal to 2D decimal. + """ + return self.mpfrlib.mandelbrot_2d_pydecimal_to_decimal(realValues, imagValues, escapeRadius, maxIterations, self.currPrecision) + + def mandelbrotSingle(self, realValue, imagValue, escapeRadius, maxIterations): + realValues = np.array((realValue), dtype=object) + imagValues = np.array((imagValue), dtype=object) + return self.mpfrlib.mandelbrot_2d_pydecimal_to_decimal(realValues, imagValues, escapeRadius, maxIterations, self.currPrecision) + + def mandelbrotToString(self, realValues, imagValues, escapeRadius, maxIterations): + """ + Returns string types to dodge an extra string-to-decimal conversion. + Useful when you know further processing (e.g. smoothing) will happen. + """ + return self.mpfrlib.mandelbrot_2d_pydecimal_to_string(realValues, imagValues, escapeRadius, maxIterations, self.currPrecision) - def squared_modulus(self, z): - return ((z.real*z.real)+(z.imag*z.imag)) - class DiveMathSupportFlint(DiveMathSupport): """ @@ -794,6 +1126,13 @@ def shorterStringFromFloat(self, paramFloat, places): def floor(self, value): return self.flint.arb(value).floor() + def stringFromARB(self, paramARB): + return paramARB.str(radius=False, more=True) + + def arrayToStringArray(self, paramArray): + stringifier = np.vectorize(self.stringFromARB) + return stringifier(paramArray) + # def createLinspace(self, paramFirst, paramLast, quantity): # """ # """ @@ -1012,7 +1351,7 @@ def mandelbrotDistanceEstimate(self, c, escapeRadius, maxIter): # return float(absZ * absZ.const_log2() / dz.abs_lower()) def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): -# #print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) + #print("rescale %s for range %s" % (str(rawValue), str(scaleRange))) val = float(rawValue) if val < 0.0: val = 0.0 @@ -1037,9 +1376,6 @@ def rescaleForRange(self, rawValue, endingIter, maxIter, scaleRange): d = self.clamp(pow_result, 0.0, 1.0 ); return float(d) - def clamp(self, num, min_value, max_value): - return max(min(num, max_value), min_value) - class DiveMathSupportFlintCustom(DiveMathSupportFlint): def __init__(self): super().__init__() @@ -1060,8 +1396,25 @@ def mandelbrot_arb(self, c, escapeRadius, maxIter): return(answer, lastZ) def mandelbrot_mpfr(self, c, escapeRadius, maxIter): - (answer, lastZ) = mpfr_fractalmath.mandelbrot_steps(c, escapeRadius, maxIter) - return(answer, lastZ) + realString = c.real.str(radius=False, more=True) + imagString = c.imag.str(radius=False, more=True) + + (answer, lastZReal, lastZImag) = mpfr_fractalmath.mandelbrot_steps(realString, imagString, escapeRadius, maxIter, self.precision()) + + #print(f"precision: {self.precision()}") + + #return(answer, self.flint.acb(lastZReal, lastZImag)) + return(answer, lastZReal) # TODO: Pretty useless, but just testing + #return(answer, lastZReals, lastZImags) + + def mandelbrot_mpfr_2d(self, numpyReals, numpyImags, escapeRadius, maxIter): + + #print(f"precision: {self.precision()}") + + (answer, lastZReals, lastZImags) = mpfr_fractalmath.mandelbrot_2d_string_to_string(numpyReals, numpyImags, escapeRadius, maxIter, self.precision()) + + #(answer, lastZ) = mpfr_fractalmath.mandelbrot_steps(c, escapeRadius, maxIter) + return(answer, lastZReals, lastZImags) def mandelbrot_check_precision(self, c, escapeRadius, maxIter): """ Slightly more efficient for HIGH maxIter values """ @@ -1086,250 +1439,3 @@ def mandelbrot_beginning_check_precision(self, c, escapeRadius, maxIter): #print("beginning answer: %s, lastZ: %s remainingPrecision: %s" % (str(answer), str(lastZ), str(remainingPrecision))) return(answer, lastZ, remainingPrecision) - -class DiveMathSupportGmp(DiveMathSupport): - """ - Overrides to instantiate gmpy2-specific complex types - - Note from the GMP docs - contexts and context managers are not thread-safe! - Modifying the context in one thread will impact all other threads. - """ - def __init__(self): - super().__init__() - - self.gmp = __import__('gmpy2') # Only imports if you instantiate this DiveMathSupport subclass. - self.defaultPrecisionSize = 53 - self.gmp.get_context().precision = self.defaultPrecisionSize - self.precisionType = 'gmp' - - def setPrecision(self, newPrecision): - oldPrecision = self.gmp.get_context().precision - self.gmp.get_context().precision = newPrecision - return oldPrecision - - def createComplex(self, *args): - """ - gmpy2.mpc() accepts one of: - 1 string (a native python complex string, with real and/or imag components) - 1 complex object (python native) - 1 float (no imaginary component) - 2 floats - 2 mpfr (gmp's float type) - """ - if len(args) == 2: - realPart = self.gmp.mpfr(args[0]) - imagPart = self.gmp.mpfr(args[1]) - return self.gmp.mpc(realPart, imagPart) - elif len(args) == 1: - return self.gmp.mpc(args[0]) - elif len(args) == 0: - return self.gmp.mpc(0.0) - else: - raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") - - def createFloat(self, floatValue): - return self.gmp.mpfr(floatValue) - - def floor(self, value): - return self.gmp.floor(value) - - def interpolateLogTo(self, startX, startY, endX, endY, targetX): - """ - Probably want additional log-defining params, but for now, let's just bake in one equation - """ - if targetX == endX: - return endY - elif targetX == startX or startX == endX or startY == endY: - return startY - else: - aVal = (endY - startY) / (self.gmp.log(endX - startX + 1)) - return aVal * (self.gmp.log(targetX - startX + 1)) + startY - - def mandelbrot(self, c, escapeRadius, maxIter): - """ - Now that smoothing is handled separately, the native python implementation - COULD work for flint as well, except it uses 'complex(0,0)' instead - of self.createComplex(0,0). - The reason for this is just as below - to reduce one extra function call in - the core calculation. - - Could just call julia with a zero start here, but seems wiser to - not have one extra function call in the core of the calculation? - - Really super didn't help to try locally caching function names - and using paren syntax - it doubled the runtime. Trying that was - probably just old advice for pre-python3? - """ - z = self.gmp.mpc(0.0) - n = 0 - - # Because norm() doesn't work for python3-gmp2 v 2.1.0a4 - #while float(self.gmp.sqrt(self.gmp.square(z.real) + self.gmp.square(z.imag))) <= escapeRadius and n < maxIter: - # looks like abs() gets to the right place, even though there's no explicit abs_lower() in libgmp? - for currIter in range(maxIter + 1): - n = currIter - - if abs(z) > escapeRadius: - break - - z = z*z + c - - return (n, z) - - def smoothAfterCalculation(self, endingZ, endingIter, maxIter, escapeRadius): - """ - This flint-specific implementation only really does flint-y logs in the smoothing. - The Decimal-specific implementation needed some extra steps, but I've ditched that one. - - NOTE: this wasn't updated with the new smoothing calcs like - the 'native' and 'flint' implementations were - """ - if endingIter == maxIter: - return float(maxIter) - else: - # The following code smooths out the colors so there aren't bands - # Algorithm taken from http://linas.org/art-gallery/escape/escape.html - # Note: Results in a float. We think. - return float(endingIter + 1 - self.gmp.log10(self.gmp.log2(self.gmp.norm(endingZ))) / math.log(2.0)) - #return float(endingIter + 1 - self.gmp.log10(self.gmp.log10(self.gmp.sqrt(self.gmp.square(endingZ.real) + self.gmp.square(endingZ.imag))))) - -class DecimalComplex: - def __init__(self, realValue, imagValue): - super().__init__() - self.real = realValue - self.imag = imagValue - - def __repr__(self): - return u"DecimalComplex(%s, %s)" % (str(self.real), str(self.imag)) - -class DiveMathSupportDecimal(DiveMathSupport): - def __init__(self): - super().__init__() - - self.decimal = __import__('decimal') # Only imports if you instantiate this DiveMathSupport subclass. - self.defaultPrecisionSize = 16 - self.decimal.getcontext().prec = self.defaultPrecisionSize - self.precisionType = 'decimal' - - def setPrecision(self, newPrecision): - oldPrecision = self.decimal.getcontext().prec - self.decimal.getcontext().prec = newPrecision - return oldPrecision - - def precision(self): - return self.decimal.getcontext().prec - - def createComplex(self, *args): - if len(args) == 2: - return DecimalComplex(self.decimal.Decimal(args[0]), self.decimal.Decimal(args[1])) - elif len(args) == 1: - if isinstance(args[0], str): - partsString = args[0] - - # Trim off surrounding parens, if present - if partsString.startswith('('): - partsString = partsString[1:] - if partsString.endswith(')'): - partsString = partsString[:-1] - - # Trim off the leading sign - firstIsPositive = True - if partsString.startswith('+'): - partsString = partsString[1:] - elif partsString.startswith('-'): - firstIsPositive = False - partsString = partsString[1:] - - # Trim off the trailing letter - lastIsImag = False - if partsString.endswith('j'): - lastIsImag = True - partsString = partsString[:-1] - - # Remaining string might have an internal sign. - # If there's no internal sign, then the whole remaining - # string is either the real or the complex - positiveParts = partsString.split('+') - negativeParts = partsString.split('-') - - realIsPositive = True - imagIsPositive = True - realPart = "" - imagPart = "" - if len(positiveParts) == 2: - realIsPositive = firstIsPositive - realPart = positiveParts[0] - imagIsPositive = True - imagPart = positiveParts[1] - elif len(negativeParts) == 2: - realIsPositive = firstIsPositive - realPart = negativeParts[0] - imagIsPositive = False - imagPart = negativeParts[1] - elif len(positiveParts) == 1 and len(negativeParts) == 1: - # No internal + or -, so it should just be a number - if lastIsImag == True: - imagPart = partsString - imagIsPositive = firstIsPositive - else: - realPart = partsString - realIsPositive = firstIsPositive - else: - raise ValueError("String parameter \"%s\" not identifiably a complex number, in createComplex()" % args[0]) - - preparedReal = '0.0' - preparedImag = '0.0' - if realPart != "": - if realIsPositive == True: - preparedReal = realPart - else: - preparedReal = "-%s" % realPart - - if imagPart != "": - if imagIsPositive == True: - preparedImag = imagPart - else: - preparedImag = "-%s" % imagPart - - return DecimalComplex(self.decimal.Decimal(preparedReal), self.decimal.Decimal(preparedImag)) - else: - if isinstance(args[0], complex): - return DecimalComplex(self.decimal.Decimal(args[0].real), self.decimal.Decimal(args[0].imag)) - else: - return DecimalComplex(self.decimal.Decimal(args[0]),self.decimal.Decimal(0))# Just a constant, so make a 0-imaginary value - elif len(args) == 0: - return DecimalComplex(self.decimal.Decimal(0),self.decimal.Decimal(0)) - else: - raise ValueError("Max 2 parameters are valid for createComplex(), but it's best to use one string") - - def mandelbrot(self, c, escapeRadius, maxIter): - """ - Step-wise impementation, sacrificing some theoretical precision for fewer - multiplications - """ - radSquared = escapeRadius * escapeRadius - - z = DecimalComplex(self.decimal.Decimal(0), self.decimal.Decimal(0)) - n = 0 - - zrSquared = self.decimal.Decimal(0) - ziSquared = self.decimal.Decimal(0) - - for currIter in range(maxIter + 1): - n = currIter - - currMagnitude = zrSquared + ziSquared - if currMagnitude > radSquared: - break - - partSum = z.real + z.imag - z.imag = partSum * partSum - zrSquared - ziSquared + c.imag - z.real = zrSquared - ziSquared + c.real - - zrSquared = z.real * z.real - ziSquared = z.imag * z.imag - - return (n, z) - - - diff --git a/mandelbrot_smooth.py b/mandelbrot_smooth.py index d3ce280..3f55b89 100644 --- a/mandelbrot_smooth.py +++ b/mandelbrot_smooth.py @@ -55,8 +55,9 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) def process_counts(self): print(f"process_counts says mathSupport is {self.dive_mesh.mathSupport.precisionType}") - smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) - self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + self.processed_array = self.dive_mesh.mathSupport.smoothAfterCalculation(self.last_values_real_array, self.last_values_imag_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + #smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) + #self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) def generate_image(self): self.palette.set_scheme_index(self.palette_index) diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py index 20837fe..06125c6 100644 --- a/mandelbrot_solo.py +++ b/mandelbrot_solo.py @@ -31,6 +31,9 @@ from algo import EscapeAlgo import fractalpalette as fp +# TODO: Temp - debugging +import timeit + class MandelbrotSolo(EscapeAlgo): def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}): super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) @@ -40,13 +43,13 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) def generate_counts(self): math_support = self.dive_mesh.mathSupport + # TODO: Disable this couple lines of debug info... mathDigits = math_support.digitsPrecision() print(f"Running with {self.max_escape_iterations} iter and {mathDigits} digits") - #mandelbrot_function = np.vectorize(math_support.mandelbrot_beginning) - mandelbrot_function = np.vectorize(math_support.mandelbrot) - (self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) + (self.counts_array, self.last_values_real_array, self.last_values_imag_array) = math_support.mandelbrot(self.mesh_real_array, self.mesh_imag_array, self.escape_radius, self.max_escape_iterations) + #print(self.counts_array) counts_name_base = u"%d.counts.pik" % self.frame_number counts_file_name = os.path.join(self.output_folder_name, counts_name_base) with open(counts_file_name, 'wb') as counts_handle: @@ -56,11 +59,11 @@ def pre_image_hook(self): hist = defaultdict(int) # numpyArray.shape returns (rows, columns) - for y in range(0, self.mesh_array.shape[0]): + for y in range(0, self.counts_array.shape[0]): #print(f"first col {self.counts_array[y,0]}") - - for x in range(0, self.mesh_array.shape[1]): - # Not using mathSupport's floor() here, because it should just be a normal-scale float + for x in range(0, self.counts_array.shape[1]): + # Not using mathSupport's floor() here, because it should + # just be a normal-scale float if self.counts_array[y,x] < self.max_escape_iterations: #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(counts_array[y,x]), str(math.floor(counts_array[y,x])))) hist[math.floor(self.counts_array[y,x])] += 1 @@ -76,7 +79,7 @@ def generate_image(self): # Note: Image's width,height is backwards from numpy's size (rows, cols) for x in range(0, image_width): for y in range(0, image_height): - color = self.palette.map_value_to_color(self.processed_array[y,x]) + color = self.palette.map_value_to_color(float(self.processed_array[y,x])) # Plot the point draw.point([x, y], color) diff --git a/mandeldistance.py b/mandeldistance.py index c07b2a5..1159e0e 100644 --- a/mandeldistance.py +++ b/mandeldistance.py @@ -25,22 +25,24 @@ def generate_counts(self): track of the derivative is important for the distance estimate, so we use the math_support's distance esitmate function instead. """ - mandelbrot_function = np.vectorize(self.dive_mesh.mathSupport.mandelbrotDistanceEstimate) - (self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) + #mandelbrot_function = np.vectorize(self.dive_mesh.mathSupport.mandelbrotDistanceEstimate) + #(self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) + (self.counts_array, self.last_values_real_array, self.last_values_imag_array) = self.dive_mesh.mathSupport.mandelbrotDistanceEstimate(self.mesh_real_array, self.mesh_imag_array, self.escape_radius, self.max_escape_iterations) counts_name_base = u"%d.counts.pik" % self.frame_number counts_file_name = os.path.join(self.output_folder_name, counts_name_base) with open(counts_file_name, 'wb') as counts_handle: pickle.dump(self.counts_array, counts_handle) - def process_counts(self): - smoothing_function = np.vectorize(self.dive_mesh.mathSupport.rescaleForRange) - self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) - - def pre_image_hook(self): - """ - Don't actually want to update the palette right now for - calculating the distance estimate color. - """ - pass +# def process_counts(self): +# smoothing_function = np.vectorize(self.dive_mesh.mathSupport.rescaleForRange) +# self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) + +# I guess with the new distance calc, we DO need to rescale the color range. +# def pre_image_hook(self): +# """ +# Don't actually want to update the palette right now for +# calculating the distance estimate color. +# """ +# pass diff --git a/markers_to_timeline.py b/markers_to_timeline.py index 5b13147..79f5d88 100644 --- a/markers_to_timeline.py +++ b/markers_to_timeline.py @@ -21,11 +21,6 @@ from fractal import * from algo import Algo # Abstract base class import, because we rely on it. -from mandelbrot_solo import MandelbrotSolo -from mandelbrot_smooth import MandelbrotSmooth -from mandeldistance import MandelDistance -from julia_solo import JuliaSolo -from julia_smooth import JuliaSmooth def parse_options(): params = {} diff --git a/mesh_explore.py b/mesh_explore.py index f772c7f..318678c 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -61,6 +61,7 @@ def parse_options(): # Second-pass at params to set up MathSupport because lots of # things depend on it for names and types. mathSupportClasses = {'native': fm.DiveMathSupport, + 'mpfr': fm.DiveMathSupportMPFR, 'flint': fm.DiveMathSupportFlint, 'flintcustom': fm.DiveMathSupportFlintCustom, # maybe not complete?# 'gmp': fm.DiveMathSupportGmp, @@ -100,7 +101,9 @@ def parse_options(): elif opt in ['--escape-iterations']: params['escape_iterations'] = int(arg) elif opt in ['--zoom-factor']: - params['zoom_factor'] = float(arg) # No extra precision needed + # No extra precision needed, but multiplied vs Decimal, so + # using native float, but have to convert when used. + params['zoom_factor'] = float(arg) elif opt in ['--algo']: params['algo'] = arg @@ -119,15 +122,17 @@ def parse_options(): def nextClicked(event): global params - params['real_width'] = params['real_width'] * params['zoom_factor'] - params['imag_width'] = params['imag_width'] * params['zoom_factor'] + sameTypeZoomFactor = params['math_support'].createFloat(params['zoom_factor']) + params['real_width'] = params['real_width'] * sameTypeZoomFactor + params['imag_width'] = params['imag_width'] * sameTypeZoomFactor updateView() def prevClicked(event): global params - params['real_width'] = params['real_width'] / params['zoom_factor'] - params['imag_width'] = params['imag_width'] / params['zoom_factor'] + sameTypeZoomFactor = params['math_support'].createFloat(params['zoom_factor']) + params['real_width'] = params['real_width'] / sameTypeZoomFactor + params['imag_width'] = params['imag_width'] / sameTypeZoomFactor updateView() @@ -274,8 +279,8 @@ def plusClicked(event): def minusClicked(event): global params params['zoom_factor'] = round(params['zoom_factor'] - .01, 2) - if params['zoom_factor'] < .1: - params['zoom_factor'] = .1 + if params['zoom_factor'] < .01: + params['zoom_factor'] = .01 updateAdvanceText() plt.draw() diff --git a/mpfr_fractal_lib.c b/mpfr_fractal_lib.c index ef73875..2dd375e 100644 --- a/mpfr_fractal_lib.c +++ b/mpfr_fractal_lib.c @@ -6,27 +6,18 @@ #include #include #include -#include -#include "mpfr.h" - -#define max(a,b) \ -({ \ - __typeof__ (a) _a = (a); \ - __typeof__ (b) _b = (b); \ - _a > _b ? _a : _b; \ -}) - -#define min(a,b) \ -({ \ - __typeof__ (a) _a = (a); \ - __typeof__ (b) _b = (b); \ - _a < _b ? _a : _b; \ -}) +#include "mpfr_fractal_lib.h" +#include "mpfr.h" void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) { + // Mandelbrot is just a special zero-initial case of julia, so we can share + // a single implementation. + const char *zero_string = "0.0"; + mpfr_julia_steps(result, last_z_real_str, last_z_imag_str, start_real_str, start_imag_str, zero_string, zero_string, radius, max_iter, prec); +/* // Step-wise algorithm modified from: // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ // Calculates (zreal + zimag)^2 to get @@ -35,29 +26,21 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i // were already calculated. // So subtract them, and the remaining term is just 2*zr*zi. - // Big blocks of declarations, hang-overs from cython - // old-style C declaration rules. mpfr_t temp; mpfr_t zr_squared; mpfr_t zi_squared; mpfr_t temp_magnitude; mpfr_t temp_sum_a; mpfr_t temp_sub_a; - mpfr_t rad_squared; // Can't mix types, so need an arb for radius + mpfr_t rad_squared; mpfr_t start_real; mpfr_t start_imag; mpfr_t last_z_real; mpfr_t last_z_imag; - //bool precisionExceeded; - //long bitsValue; - - //mpfr_exp_t print_exp; long print_buffer_size = 65536; char print_buffer[print_buffer_size]; - //char last_z_real_buffer[print_buffer_size]; - //char last_z_imag_buffer[print_buffer_size]; mpfr_set_default_prec(prec); @@ -87,33 +70,15 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i //printf("\n"); //fflush(stdout); - - // Magic conversion for converting prec to dps... roughly. - // (needed for back-to-string conversions) - //long digits_precision = max(1, round(prec/3.3219280948873626)-1); - //int max_exponent_length = 9; - // r^2 mpfr_set_d(rad_squared, (double)(radius * radius), MPFR_RNDN); -// #print("starting stepping c accuracy:") -// #bitsValue = acb_rel_accuracy_bits(c) -// #print("bits") -// #print(bitsValue) -// -// # Apperently important not to use an expression as parameter -// # in place of prec, but to compute the value here. -// #prec = precin + 10 - - /* initialize Z as zero */ - + // initialize Z as zero mpfr_set_zero(last_z_real,1); mpfr_set_zero(last_z_imag,1); mpfr_set_zero(zr_squared,1); mpfr_set_zero(zi_squared,1); - //precisionExceeded = false; - // Since the zero iteration is just setting the starting values, it's not // counted in the iterations count (so loop until *equal to* max_iter). for(long currIter = 0; currIter <= max_iter; currIter++) @@ -137,7 +102,7 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i // The mpfr docs claim in-place operations work too (where I'm pretty // sure I had bugs trying to use arb_add(a,a,b) functions), and // should be preferred over temporary values... but I haven't run - // any comparisons on that, so am not really worried about it? + // any comparisons on that yet, so don't know? mpfr_add(temp_sum_a, last_z_real, last_z_imag, MPFR_RNDN); mpfr_sqr(temp, temp_sum_a, MPFR_RNDN); mpfr_sub(temp_sub_a, temp, zr_squared, MPFR_RNDN); @@ -154,10 +119,8 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i // Print the value to the print buffer, then create a new string // of appropriate length from there? - //mpfr_snprintf(print_buffer, digits_precision + max_exponent_length, "%Re", last_z_real); mpfr_sprintf(print_buffer, "%Re", last_z_real); *last_z_real_str = strdup(print_buffer); - //mpfr_snprintf(print_buffer, digits_precision + max_exponent_length, "%Re", last_z_imag); mpfr_sprintf(print_buffer, "%Re", last_z_imag); *last_z_imag_str = strdup(print_buffer); @@ -173,6 +136,131 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i mpfr_clear(start_imag); mpfr_clear(last_z_real); mpfr_clear(last_z_imag); +*/ } +void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) +{ + // Step-wise algorithm modified from: + // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ + // Calculates (zreal + zimag)^2 to get + // zr^2 + 2*zr*zi + zi^2 + // Which is helpful, because for the iteration, zr^2 and zi^2 + // were already calculated. + // So subtract them, and the remaining term is just 2*zr*zi. + + mpfr_t temp; + mpfr_t zr_squared; + mpfr_t zi_squared; + mpfr_t temp_magnitude; + mpfr_t temp_sum_a; + mpfr_t temp_sub_a; + mpfr_t rad_squared; + + mpfr_t start_real; + mpfr_t start_imag; + mpfr_t last_z_real; + mpfr_t last_z_imag; + + long print_buffer_size = 65536; + char print_buffer[print_buffer_size]; + + mpfr_set_default_prec(prec); + + mpfr_init(temp); + mpfr_init(zr_squared); + mpfr_init(zi_squared); + mpfr_init(temp_magnitude); + mpfr_init(temp_sum_a); + mpfr_init(temp_sub_a); + mpfr_init(rad_squared); + + mpfr_init(start_real); + mpfr_init(start_imag); + mpfr_init(last_z_real); + mpfr_init(last_z_imag); + + mpfr_set_str(start_real, start_real_str, 10, MPFR_RNDN); // 10 = base, not precision + mpfr_set_str(start_imag, start_imag_str, 10, MPFR_RNDN); + + //printf("start_real_str \"%s\"\n", start_real_str); + //printf("start_imag_str \"%s\"\n", start_imag_str); + + //printf("start_real: "); + //mpfr_out_str(stdout, 10,100,start_real, MPFR_RNDN); + //printf("\nstart_imag: "); + //mpfr_out_str(stdout, 10,100,start_imag, MPFR_RNDN); + //printf("\n"); + //fflush(stdout); + + // r^2 + mpfr_set_d(rad_squared, (double)(radius * radius), MPFR_RNDN); + + // Julia init is to the julia value, which means squares should be initialized too. + mpfr_set_str(last_z_real, julia_real_str, 10, MPFR_RNDN); // 10 = base, not precision + mpfr_set_str(last_z_imag, julia_imag_str, 10, MPFR_RNDN); + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); + + // Since the zero iteration is just setting the starting values, it's not + // counted in the iterations count (so loop until *equal to* max_iter). + // This *seems* to still be reasonable for Julia iteration, even though the + // bit about 'just setting the starting values' doesn't seem as correct. + for(long currIter = 0; currIter <= max_iter; currIter++) + { + *result = currIter; + + mpfr_add(temp_magnitude, zr_squared, zi_squared, MPFR_RNDN); + + //mpfr_sprintf(print_buffer, "%Re", temp_magnitude); + //printf("%s\n", print_buffer); + //fflush(stdout); + + if(mpfr_cmp(temp_magnitude, rad_squared) > 0) + { + break; + } + + // Looks like ~4 percent increase in speed by using mpfr_sqr where + // possible, instead of mpfr_mul (which is interesting, because + // arb_sqr just redirects to mul). + // + // The mpfr docs claim in-place operations work too (where I'm pretty + // sure I had bugs trying to use arb_add(a,a,b) functions), and + // should be preferred over temporary values... but I haven't run + // any comparisons on that yet, so don't know? + mpfr_add(temp_sum_a, last_z_real, last_z_imag, MPFR_RNDN); + mpfr_sqr(temp, temp_sum_a, MPFR_RNDN); + mpfr_sub(temp_sub_a, temp, zr_squared, MPFR_RNDN); + mpfr_sub(temp, temp_sub_a, zi_squared, MPFR_RNDN); + + mpfr_add(last_z_imag, temp, start_imag, MPFR_RNDN); + + mpfr_sub(temp_sub_a, zr_squared, zi_squared, MPFR_RNDN); + mpfr_add(last_z_real, temp_sub_a, start_real, MPFR_RNDN); + + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); + } + + // Print the value to the print buffer, then create a new string + // of appropriate length from there? + mpfr_sprintf(print_buffer, "%Re", last_z_real); + *last_z_real_str = strdup(print_buffer); + mpfr_sprintf(print_buffer, "%Re", last_z_imag); + *last_z_imag_str = strdup(print_buffer); + + mpfr_clear(temp); + mpfr_clear(zr_squared); + mpfr_clear(zi_squared); + mpfr_clear(temp_magnitude); + mpfr_clear(temp_sum_a); + mpfr_clear(temp_sub_a); + mpfr_clear(rad_squared); + + mpfr_clear(start_real); + mpfr_clear(start_imag); + mpfr_clear(last_z_real); + mpfr_clear(last_z_imag); +} diff --git a/mpfr_fractal_lib.h b/mpfr_fractal_lib.h index 8eb154a..f2fee0c 100644 --- a/mpfr_fractal_lib.h +++ b/mpfr_fractal_lib.h @@ -7,3 +7,5 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec); +void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec); + diff --git a/mpfr_fractal_lib.o b/mpfr_fractal_lib.o new file mode 100644 index 0000000000000000000000000000000000000000..513745caf28f98f5009b53ba0e3b8a44abf57fce GIT binary patch literal 2984 zcmc&$ZD?Cn7`~ZZmu>A5WrK>!0;@8`NTv)bI;hmZ5pJQ(7MxqXOOtfT`mrn@TEsv# zT-W2}&VuBJiXe#l<&P*7$}m#2qLYE~3;M%B9SR+EQm9Iuy8685zBj$ODEQ9<=RWV} zbKdiw`}O6G>o<33THqE97tA)y;5J+$m>9QX&XD6@#`1Q@agcH_TpgSmP2!_r7{&Bd z(UBc}F#gowmhMXTd-zl>K}|bM&Ngk@jUhCQWTBYPPM%_;VYKqOJV;k#kc#mPF1Kj` z-40Ogt|$oZK$0HDSS%O+9C zt3pm9)zUJw<5}5cLAJg?_CrU`&1So?OyU*rg^WYW4gGu5>9&@slR7o-_%#?cBjhlhI1JZ&xhS>-Q<*{L<1UkiIq zC8IOa{PQn&ak8rR6{{I6fP_kp9yOKvg(wLrsm!bnM4I0>BU<~s z)M9rwNPYg+wR7vlc4`l9Wor7hSJVpC8i7?0EA8-3MAW#mj!~M8CXMZM&$6c}S_ZcA zz5=m7@A6h&xq(N@d5#1B)1!kP2vzl>u+)27Sa@6XuZf3051I5~5q5e5FDg}DXm{*^ zn0EJ3e(}Rg;|0us7N91!jA_>1!z=Tj{Cn9Sc>bssmt^ zuf}70!Y^;@>uir#=<}rF=tH)^akP#aZk%x9?!ms05Zwht$dTW1X@`ED@dw857%wr_ z8Q*7om+^JRS;ljWMPLZLBft*eaUfpl`yOTceQZC#_Fqv{C*rO!ehNg1(RYCOjz!P0 zo?%^Qi~%XHi~T=i5u8-C0i=8zNd6MzFc9C&XfI;~h)-&C1HC5u65~g}AoLFyFEGBr zc%Jbk#%CE%Gd{yO#Hcg&0r5hP?gwJ&=$kkswBX2D=d_|xAnf}hK!o6E96SIF!7xn* z4D?j#O2H+833qc8eEMrX{en;L_vu%BI&pjXYd-x$pHBaB-gwie_xbd1eEKn;t{D@# z(Y%qJ%obUkNKB^E<0JVqMWawm=L&2-T^i3OS}cV$MdaCEIO|Hu@pJ+*T&YqHKbrV8 z%#0O^V@4{G$FCt-3dKZnjLgPpB0KIf%K!iX literal 0 HcmV?d00001 diff --git a/mpfr_fractalmath.pyx b/mpfr_fractalmath.pyx index 03d8dd5..f6c7f0b 100644 --- a/mpfr_fractalmath.pyx +++ b/mpfr_fractalmath.pyx @@ -1,55 +1,231 @@ -# Lazily using flint's arb-from-string function here, though flint -# isn't really required... Should fix that... -# ACTUALLY, maybe flint is required, as an input parameter to start value? -import flint - -# Python usage: -# import flint -# import fractalmath_arb -# flintComplexValue = flint.arb(-1.76938, .00423) -# (answer, last_z, remaining_precision) = fractalmath_arb.mandelbrot_steps(flintComplexValue, 2.0, 255); - -# Organization Discussion -# -# arbfractalmath.h and arbfractalmath.h -# - compile into arbfractalmath.a -# -# arb_fractalmath.pyx -# - compiles into fractalmath_arb.so module via setup.py, invoked from makefile -# arb_fractalmath.pyx (defines python interface for arbfractalmath.a) -# - needs to accept python-flint arguments -# - needs to invoke arb_fractalmath's arb_mandelbrot_steps() with appropriate params + +import numpy as np +cimport numpy as np + +import decimal cdef extern from "mpfr_fractal_lib.h": void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) + void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) -def mandelbrot_steps(s, radius, max_iter): +def mandelbrot_steps(string_real, string_imag, escape_radius, max_iterations, precision): + # Temporaries used for every call cdef long answer = 0 - cdef char *last_z_real_str = NULL cdef char *last_z_imag_str = NULL + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + # Shuffle to keep temporary strings from becoming parameters # Techincally, this captures the 'bytes' from the .encode() # call, so the assignment to a cython char* is allowed. - cdef char *start_real_str = NULL - cdef char *start_imag_str = NULL - start_real_py_str = s.real.str(radius=False, more=True).encode('ascii') - start_imag_py_str = s.imag.str(radius=False, more=True).encode('ascii') + start_real_py_str = string_real.encode('ascii') + start_imag_py_str = string_imag.encode('ascii') start_real_str = start_real_py_str start_imag_str = start_imag_py_str #print(f"param's start_real_str \"{start_real_str}\"") #print(f"param's start_imag_str \"{start_imag_str}\"") - mpfr_mandelbrot_steps(&answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, float(radius), max_iter, flint.ctx.prec) + mpfr_mandelbrot_steps(&answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, float(escape_radius), max_iterations, precision) # Extra step for string conversion back from bytes to python string - answer_real_py_str = last_z_real_str.decode('ascii') - answer_imag_py_str = last_z_imag_str.decode('ascii') - last_z = flint.acb(answer_real_py_str, answer_imag_py_str) + #print(f"last_z_real_str \"{last_z_real_str}\"") + #print(f"last_z_imag_str \"{last_z_imag_str}\"") + last_z_real = last_z_real_str.decode('ascii') + last_z_imag = last_z_imag_str.decode('ascii') # TODO: Almost certainly need to free last_z_real_str # and last_z_imag_str, right? - return (answer, last_z) + #print(f"answer {answer}") + + return (answer, last_z_real, last_z_imag) + +def mandelbrot_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision): + if string_reals.shape != string_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = string_reals.shape # (rows, columns) + + # Python-create the numpy arrays. Originally thought we'd then + # cython-assign them to be used as memoryviews, but it's gnarly + # for strings, and really doesn't save much overall. + count_results = np.zeros(answer_shape, dtype=float) + last_z_reals = np.empty(answer_shape, dtype=object) + last_z_imags = np.empty(answer_shape, dtype=object) + + # Temporaries used for every call + cdef long answer = 0 + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + start_real_py_str = string_reals[x,y].encode('ascii') + start_imag_py_str = string_imags[x,y].encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + #print(f"param's start_real_str \"{start_real_str}\"") + #print(f"param's start_imag_str \"{start_imag_str}\"") + mpfr_mandelbrot_steps(&answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, float(escape_radius), max_iterations, precision) + + # Extra step for string conversion back from bytes to python string + #print(f"last_z_real_str \"{last_z_real_str}\"") + #print(f"last_z_imag_str \"{last_z_imag_str}\"") + last_z_reals[x,y] = last_z_real_str.decode('ascii') + last_z_imags[x,y] = last_z_imag_str.decode('ascii') + + count_results[x,y] = answer + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + #print(f"answer {answer}") + + return (count_results, last_z_reals, last_z_imags) + +def mandelbrot_2d_pydecimal_to_string(decimal_reals, decimal_imags, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + # TODO: Quite possibly want to set decimal's precision here? + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + return mandelbrot_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision) + +def mandelbrot_2d_pydecimal_to_decimal(decimal_reals, decimal_imags, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + (count_results, string_last_z_reals, string_last_z_imags) = mandelbrot_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision) + + decimal_last_z_reals = np.empty(answer_shape, dtype=object) + decimal_last_z_imags = np.empty(answer_shape, dtype=object) + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + decimal_last_z_reals[x,y] = decimal.Decimal(string_last_z_reals[x,y]) + decimal_last_z_imags[x,y] = decimal.Decimal(string_last_z_imags[x,y]) + + return (count_results, decimal_last_z_reals, decimal_last_z_imags) + +def julia_2d_string_to_string(string_reals, string_imags, string_julia_real, string_julia_imag, escape_radius, max_iterations, precision): + if string_reals.shape != string_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = string_reals.shape # (rows, columns) + + # Python-create the numpy arrays. Originally thought we'd then + # cython-assign them to be used as memoryviews, but it's gnarly + # for strings, and really doesn't save much overall. + count_results = np.zeros(answer_shape, dtype=float) + last_z_reals = np.empty(answer_shape, dtype=object) + last_z_imags = np.empty(answer_shape, dtype=object) + + # Temporaries used for every call + cdef long answer = 0 + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + + cdef char *julia_real_str = NULL + cdef char *julia_imag_str = NULL + julia_real_py_str = string_julia_real.encode('ascii') + julia_imag_py_str = string_julia_imag.encode('ascii') + julia_real_str = julia_real_py_str + julia_imag_str = julia_imag_py_str + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + start_real_py_str = string_reals[x,y].encode('ascii') + start_imag_py_str = string_imags[x,y].encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + #print(f"param's start_real_str \"{start_real_str}\"") + #print(f"param's start_imag_str \"{start_imag_str}\"") + mpfr_julia_steps(&answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, julia_real_str, julia_imag_str, float(escape_radius), max_iterations, precision) + + # Extra step for string conversion back from bytes to python string + #print(f"last_z_real_str \"{last_z_real_str}\"") + #print(f"last_z_imag_str \"{last_z_imag_str}\"") + last_z_reals[x,y] = last_z_real_str.decode('ascii') + last_z_imags[x,y] = last_z_imag_str.decode('ascii') + + count_results[x,y] = answer + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + #print(f"answer {answer}") + + return (count_results, last_z_reals, last_z_imags) + +def julia_2d_pydecimal_to_string(decimal_reals, decimal_imags, decimal_julia_real, decimal_julia_imag, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + # TODO: Quite possibly want to set decimal's precision here? + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + return julia_2d_string_to_string(string_reals, string_imags, str(decimal_julia_real), str(decimal_julia_imag), escape_radius, max_iterations, precision) + +def julia_2d_pydecimal_to_decimal(decimal_reals, decimal_imags, decimal_julia_real, decimal_julia_imag, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + (count_results, string_last_z_reals, string_last_z_imags) = julia_2d_string_to_string(string_reals, string_imags, str(decimal_julia_real), str(decimal_julia_imag), escape_radius, max_iterations, precision) + + decimal_last_z_reals = np.empty(answer_shape, dtype=object) + decimal_last_z_imags = np.empty(answer_shape, dtype=object) + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + decimal_last_z_reals[x,y] = decimal.Decimal(string_last_z_reals[x,y]) + decimal_last_z_imags[x,y] = decimal.Decimal(string_last_z_imags[x,y]) + return (count_results, decimal_last_z_reals, decimal_last_z_imags) + diff --git a/setup_mpfr.py b/setup_mpfr.py index 4854e91..bb26219 100644 --- a/setup_mpfr.py +++ b/setup_mpfr.py @@ -19,6 +19,7 @@ #default_include_dirs = default_include_dirs + [numpy.get_include()] mpfr_include_dirs = default_include_dirs.copy() +mpfr_include_dirs.append(numpy.get_include()) #arb_include_dirs.append(".") #for curr_dir in default_include_dirs: @@ -38,7 +39,7 @@ # The combination of the custom fractal library, and it's cython # wrapper, is caled "fractalmath_mpfr", and can be imported by that name # in a python script. - Extension('mpfr_fractalmath', ['mpfr_fractalmath.pyx'], libraries=["mpfr", "mpfrfractalmath"], library_dirs=mpfr_library_dirs, include_dirs=mpfr_include_dirs) + Extension('mpfr_fractalmath', ['mpfr_fractalmath.pyx'], libraries=["mpfr", "mpfrfractalmath"], library_dirs=mpfr_library_dirs, include_dirs=mpfr_include_dirs, define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')]) ] setup( diff --git a/timing_comparison.py b/timing_comparison.py index ae2d8d1..07ebcea 100644 --- a/timing_comparison.py +++ b/timing_comparison.py @@ -12,17 +12,17 @@ #mandelbrotMaxIter = 255 #timerRepeatCount = 1000 -decimalPlaces = 100 -mandelbrotMaxIter = 7000 -timerRepeatCount = 1000 +#decimalPlaces = 100 +#mandelbrotMaxIter = 7000 +#timerRepeatCount = 1000 #decimalPlaces = 500 #mandelbrotMaxIter = 35000 #timerRepeatCount = 10 -#decimalPlaces = 1000 -#mandelbrotMaxIter = 150000 -#timerRepeatCount = 10 +decimalPlaces = 1000 +mandelbrotMaxIter = 150000 +timerRepeatCount = 10 #decimalPlaces = 1500 #mandelbrotMaxIter = 290000 @@ -80,6 +80,11 @@ def mpfrMandelbrotter(): radius = 2.0 return flintCustomMathSupport.mandelbrot_mpfr(theCenter, radius, mandelbrotMaxIter) +def mpfrMandelbrot2d(): + theCenter = flintCustomMathSupport.createComplex(centerString) + radius = 2.0 + return flintCustomMathSupport.mandelbrot_mpfr_2d(theCenter, radius, mandelbrotMaxIter) + def decMandelbrotter(): theCenter = decMathSupport.createComplex(centerString) radius = 2.0 @@ -100,12 +105,16 @@ def main(): # print(timeit.Timer(flintMandelbrotter).timeit(number= timerRepeatCount)) # print("flint custom") # print(timeit.Timer(flintCustomMandelbrotter).timeit(number= timerRepeatCount)) - print("flint low magnification") - print(timeit.Timer(flintBeginningMandelbrotter).timeit(number= timerRepeatCount)) + +# print("flint low magnification") +# print(timeit.Timer(flintBeginningMandelbrotter).timeit(number= timerRepeatCount)) print("arb steps") print(timeit.Timer(arbMandelbrotter).timeit(number= timerRepeatCount)) print("mpfr steps") print(timeit.Timer(mpfrMandelbrotter).timeit(number= timerRepeatCount)) + print("mpfr steps 2d") + print(timeit.Timer(mpfrMandelbrot2d).timeit(number= timerRepeatCount)) + # print("decimal") # print(timeit.Timer(decMandelbrotter).timeit(number= timerRepeatCount)) From cc5186ee63b0a10af1a08df6cefea2c7d88e8a0b Mon Sep 17 00:00:00 2001 From: clint Date: Thu, 23 Sep 2021 14:34:01 -0700 Subject: [PATCH 41/44] hacked a non-adjustable version of julia set into the mesh explore - works for both solo and smooth algos. --- fractalmath.py | 8 +++--- julia_smooth.py | 67 +++++++++++++++++++++++++------------------------ julia_solo.py | 11 +++++--- mesh_explore.py | 26 +++++++++++++++++++ 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/fractalmath.py b/fractalmath.py index a952584..ca79f68 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -523,9 +523,9 @@ def interpolateLinear(self, startX, startY, endX, endY, targetX): def julia(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): currShape = realValues.shape - results = np.array(currShape, dtype=object) - lastRealValues = np.array(currShape, dtype=object) - lastImagValues = np.array(currShape, dtype=object) + results = np.empty(currShape, dtype=object) + lastRealValues = np.empty(currShape, dtype=object) + lastImagValues = np.empty(currShape, dtype=object) # numpyArray.shape returns (rows, columns) for y in range(currShape[0]): @@ -638,7 +638,7 @@ def smoothAfterCalculationSingle(self, lastReal, lastImag, endingIter, maxIter, return self.createFloat(maxIter) else: sqMagnitude = self.decimal.Decimal(lastReal) ** 2 + self.decimal.Decimal(lastImag) ** 2 - return endingIter - ((sqMagnitude.sqrt().ln().ln()) / (self.decimal.Decimal('2').ln())) + return self.decimal.Decimal(endingIter) - ((sqMagnitude.sqrt().ln().ln()) / (self.decimal.Decimal('2').ln())) def mandelbrotDistanceEstimate(self, realValues, imagValues, escapeRadius, maxIter): currShape = realValues.shape diff --git a/julia_smooth.py b/julia_smooth.py index ed6d90b..ccc3dea 100644 --- a/julia_smooth.py +++ b/julia_smooth.py @@ -29,38 +29,39 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) self.color = (.0,.6,1.0) # blue / yellow def process_counts(self): - smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) - self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) - - def generate_image(self): - # Capturing the transpose of our array, because it looks like I mixed - # up rows and cols somewhere along the way. - if self.use_smoothing == True: - pixel_values_2d = self.cache_frame.frame_info.smooth_values.T - else: - pixel_values_2d = self.cache_frame.frame_info.raw_values.T - #print("shape of things to come: %s" % str(pixel_values_2d.shape)) - -# TODO: Really, width and height are all kinda incorrect here - -# gotta spend some TLC on the array shape and transpose. - (image_width, image_height) = pixel_values_2d.shape - im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) - draw = ImageDraw.Draw(im) - - for x in range(0, image_width): - for y in range(0, image_height): - color = self.palette.map_value_to_color(pixel_values_2d[x,y]) - - # Plot the point - draw.point([x, y], color) - - if self.burn_in == True: - meta = self.get_frame_metadata() - if meta: - burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) - self.burn_text_to_drawing(burn_in_text, draw) - - return im + self.processed_array = self.dive_mesh.mathSupport.smoothAfterCalculation(self.last_values_real_array, self.last_values_imag_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + #smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) + #self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) + +# def generate_image(self): +# # Capturing the transpose of our array, because it looks like I mixed +# # up rows and cols somewhere along the way. +# if self.use_smoothing == True: +# pixel_values_2d = self.cache_frame.frame_info.smooth_values.T +# else: +# pixel_values_2d = self.cache_frame.frame_info.raw_values.T +# #print("shape of things to come: %s" % str(pixel_values_2d.shape)) +# +## TODO: Really, width and height are all kinda incorrect here - +## gotta spend some TLC on the array shape and transpose. +# (image_width, image_height) = pixel_values_2d.shape +# im = Image.new('RGB', (image_width, image_height), (0, 0, 0)) +# draw = ImageDraw.Draw(im) +# +# for x in range(0, image_width): +# for y in range(0, image_height): +# color = self.palette.map_value_to_color(float(pixel_values_2d[x,y])) +# +# # Plot the point +# draw.point([x, y], color) +# +# if self.burn_in == True: +# meta = self.get_frame_metadata() +# if meta: +# burn_in_text = u"%d center: %s\n realw: %s imagw: %s" % (meta['frame_number'], meta['mesh_center'], meta['complex_real_width'], meta['complex_imag_width']) +# self.burn_text_to_drawing(burn_in_text, draw) +# +# return im def generate_image(self): (image_height, image_width) = self.processed_array.shape @@ -70,7 +71,7 @@ def generate_image(self): # Note: Image's width,height is backwards from numpy's size (rows, cols) for x in range(0, image_width): for y in range(0, image_height): - color = self.map_value_to_color(self.processed_array[y,x]) + color = self.map_value_to_color(float(self.processed_array[y,x])) # Plot the point draw.point([x, y], color) diff --git a/julia_solo.py b/julia_solo.py index b9c1949..44607f3 100644 --- a/julia_solo.py +++ b/julia_solo.py @@ -30,8 +30,11 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) def generate_counts(self): math_support = self.dive_mesh.mathSupport - julia_function = np.vectorize(math_support.julia) - (self.counts_array, self.last_values_array) = julia_function(self.julia_center, self.mesh_array, self.escape_radius, self.max_escape_iterations) + + #julia_function = np.vectorize(math_support.julia) + #(self.counts_array, self.last_values_array) = julia_function(self.julia_center, self.mesh_array, self.escape_radius, self.max_escape_iterations) + + (self.counts_array, self.last_values_real_array, self.last_values_imag_array) = math_support.julia(self.mesh_real_array, self.mesh_imag_array, self.julia_center.real, self.julia_center.imag, self.escape_radius, self.max_escape_iterations) counts_name_base = u"%d.counts.pik" % self.frame_number counts_file_name = os.path.join(self.output_folder_name, counts_name_base) @@ -42,8 +45,8 @@ def pre_image_hook(self): hist = defaultdict(int) # numpyArray.shape returns (rows, columns) - for y in range(0, self.mesh_array.shape[0]): - for x in range(0, self.mesh_array.shape[1]): + for y in range(0, self.counts_array.shape[0]): + for x in range(0, self.counts_array.shape[1]): # Not using mathSupport's floor() here, because it should just be a normal-scale float if self.counts_array[y,x] < self.max_escape_iterations: #print("x: %d, y: %d, val: %s, floor: %s" % (x,y,str(counts_array[y,x]), str(math.floor(counts_array[y,x])))) diff --git a/mesh_explore.py b/mesh_explore.py index 318678c..a279be1 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -41,6 +41,9 @@ def parse_options(): "imag-width=", "zoom-factor=", "algo=", + # Explicit extra params, should probably + # get/set these like in fractal.py + "julia-center=", ]) # First-pass at params pulls out the project file, because @@ -106,6 +109,8 @@ def parse_options(): params['zoom_factor'] = float(arg) elif opt in ['--algo']: params['algo'] = arg + elif opt in ['--julia-center']: + params['julia_center'] = mathSupport.createComplex(arg) params['wholeCacheFolder'] = os.path.join(params['project_name'], params['project_params']['exploration_output_path']) @@ -196,11 +201,32 @@ def runFractalCallForCenter(center): fractalCallString = f"python3.9 ./fractal.py --project='{projectName}' --exploration --expl-algo={algoName} --burn --math-support={mathSupport.precisionType} --digits-precision={params['digits_precision']} --max-escape-iterations={params['escape_iterations']} --expl-frame-number={nextFrameNumber} --expl-real-width='{realWidthString}' --expl-imag-width='{imagWidthString}' --expl-center='{centerString}'" + # Just hacked on for now... + if algoName in ['julia_solo', 'julia_smooth']: + juliaCenter = params['julia_center'] + juliaCenterString = str(juliaCenter) + if juliaCenter.imag == 0: + trimmed = juliaCenterString + if juliaCenterString.endswith(')'): + trimmed = juliaCenterString[:-1] + if not trimmed.endswith('j'): + trimmed = trimmed + "+0j" + if juliaCenterString.endswith(')'): + juliaCenterString = trimmed + ")" + else: + juliaCenterString = trimmed + fractalCallString += f" --julia-center='{juliaCenterString}'" + print("Calling: %s" % fractalCallString) subprocess.call([fractalCallString], shell=True) print("(Call finished.)") print("Exploration invocation for this point:") explorationCallString = f"python3.9 ./mesh_explore.py --project='{projectName}' --algo={algoName} --math-support={mathSupport.precisionType} --digits-precision={params['digits_precision']} --escape-iterations={params['escape_iterations']} --next-frame-number={nextFrameNumber} --real-width='{realWidthString}' --imag-width='{imagWidthString}' --zoom-factor={zoomFactor} --center='{centerString}'" + + # Just hacked on for now... + if algoName in ['julia_solo', 'julia_smooth']: + explorationCallString += f" --julia-center='{juliaCenterString}'" + print(explorationCallString) print("") From 68bdaf902fcf5f109dc1078e9d68897bef2bf361 Mon Sep 17 00:00:00 2001 From: clint Date: Fri, 24 Sep 2021 13:03:04 -0700 Subject: [PATCH 42/44] trying to add a load-from-marker option to mesh explore --- mandelbrot_smooth.py | 2 +- mesh_explore.py | 148 +++++++++++++++++++++++++----------- time_tinker_explanation.txt | 2 + 3 files changed, 105 insertions(+), 47 deletions(-) diff --git a/mandelbrot_smooth.py b/mandelbrot_smooth.py index 3f55b89..bc9e248 100644 --- a/mandelbrot_smooth.py +++ b/mandelbrot_smooth.py @@ -54,7 +54,7 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) self.palette_index = extra_params.get('palette_scheme_index', 0) def process_counts(self): - print(f"process_counts says mathSupport is {self.dive_mesh.mathSupport.precisionType}") + #print(f"process_counts says mathSupport is {self.dive_mesh.mathSupport.precisionType}") self.processed_array = self.dive_mesh.mathSupport.smoothAfterCalculation(self.last_values_real_array, self.last_values_imag_array, self.counts_array, self.max_escape_iterations, self.escape_radius) #smoothing_function = np.vectorize(self.dive_mesh.mathSupport.smoothAfterCalculation) #self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.escape_radius) diff --git a/mesh_explore.py b/mesh_explore.py index a279be1..7d381e2 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -41,6 +41,8 @@ def parse_options(): "imag-width=", "zoom-factor=", "algo=", + # Special read-from-file inovcation + "load-marker-number=", # Explicit extra params, should probably # get/set these like in fractal.py "julia-center=", @@ -51,6 +53,8 @@ def parse_options(): for opt, arg in opts: if opt in ['--project']: params['project_name'] = arg + if opt in ['--load-marker-number']: + params['load_marker_number'] = arg # Require that a project has been specified. if 'project_name' not in params: @@ -58,6 +62,7 @@ def parse_options(): # Load project parameters out of the params file paramFileName = os.path.join(params['project_name'], 'params.json') + print(f"opening \"{paramFileName}\"") with open(paramFileName, 'rt') as paramHandle: params['project_params'] = json.load(paramHandle) @@ -71,58 +76,109 @@ def parse_options(): # maybe not complete?# 'decimal': fm.DiveMathSupportDecimal, # definitely not built yet.# 'libbf': fm.DiveMathSupportLibbf, } - mathSupportName = params['project_params'].get('math_support', 'native') - for opt, arg in opts: - if opt in ['--math-support']: - if arg in mathSupportClasses: - mathSupportName = arg - elif opt in ['--digits-precision']: - params['digits_precision'] = int(arg) - # Creates an instance - mathSupport = mathSupportClasses[mathSupportName]() - - # Important to also set expected precision before parsing param values - supportPrecision = params.get('digits_precision', 16) # 16 == native - mathSupport.setPrecision(round(supportPrecision * 3.32)) # ~3.32 bits per position - params['math_support'] = mathSupport - - # Defaults, overwritable by cmd line args - params['algo'] = 'mandelbrot_smooth' - params['next_frame_number'] = 1 - - # Third pass at params, now that math support is all set up - for opt, arg in opts: - if opt in ['--center']: - params['center'] = mathSupport.createComplex(arg) - elif opt in ['--next-frame-number']: - params['next_frame_number'] = int(arg) - elif opt in ['--real-width']: - params['real_width'] = mathSupport.createFloat(arg) - elif opt in ['--imag-width']: - params['imag_width'] = mathSupport.createFloat(arg) - elif opt in ['--escape-iterations']: - params['escape_iterations'] = int(arg) - elif opt in ['--zoom-factor']: - # No extra precision needed, but multiplied vs Decimal, so - # using native float, but have to convert when used. - params['zoom_factor'] = float(arg) - elif opt in ['--algo']: - params['algo'] = arg - elif opt in ['--julia-center']: - params['julia_center'] = mathSupport.createComplex(arg) + # Before getting into normal param load, shortcut when we're asked to + # load a marker file + if 'load_marker_number' in params: + markerNumber = params['load_marker_number'] + markerFileBase = f"{markerNumber}.marker.pik" + markerFileName = os.path.join(params['project_name'], params['project_params']['exploration_markers_path'], markerFileBase) + + marker = None + with open(markerFileName, 'rb') as markerHandle: + marker = pickle.load(markerHandle) + + if marker == None: + raise ValueError("Failed to load marker number \"{markerNumber}\"") + + diveMesh = marker.diveMesh + mathSupport = diveMesh.mathSupport + params['math_support'] = mathSupport + params['digits_precision'] = mathSupport.digitsPrecision() + + realCenter = diveMesh.realMeshGenerator.valuesCenter + imagCenter = diveMesh.imagMeshGenerator.valuesCenter + params['center'] = mathSupport.createComplex(realCenter, imagCenter) + params['real_width'] = diveMesh.realMeshGenerator.baseWidth + params['imag_width'] = diveMesh.imagMeshGenerator.baseWidth + + params['next_frame_number'] = marker.markerNumber + params['escape_iterations'] = marker.maxEscapeIterations + params['algo'] = marker.algorithmName + + # Guess we ignore the marker's mesh size, because we're using + # the project param's exploration size? + else: + # 'Normal' params, not marker load + mathSupportName = params['project_params'].get('math_support', 'native') + + for opt, arg in opts: + if opt in ['--math-support']: + if arg in mathSupportClasses: + mathSupportName = arg + elif opt in ['--digits-precision']: + params['digits_precision'] = int(arg) + # Creates an instance + mathSupport = mathSupportClasses[mathSupportName]() + + # Important to also set expected precision before parsing param values + supportPrecision = params.get('digits_precision', 16) # 16 == native + # May have been defaulted, so (re)set the param + params['digits_precision'] = supportPrecision + + mathSupport.setPrecision(round(supportPrecision * 3.32)) # ~3.32 bits per position + params['math_support'] = mathSupport + + # Defaults, overwritable by cmd line args + params['algo'] = 'mandelbrot_smooth' + params['next_frame_number'] = 1 + + # Third pass at params, now that math support is all set up + for opt, arg in opts: + if opt in ['--center']: + params['center'] = mathSupport.createComplex(arg) + elif opt in ['--next-frame-number']: + params['next_frame_number'] = int(arg) + elif opt in ['--real-width']: + params['real_width'] = mathSupport.createFloat(arg) + elif opt in ['--imag-width']: + params['imag_width'] = mathSupport.createFloat(arg) + elif opt in ['--escape-iterations']: + params['escape_iterations'] = int(arg) + elif opt in ['--zoom-factor']: + # No extra precision needed, but multiplied vs Decimal, so + # using native float, but have to convert when used. + params['zoom_factor'] = float(arg) + elif opt in ['--algo']: + params['algo'] = arg + elif opt in ['--julia-center']: + params['julia_center'] = mathSupport.createComplex(arg) + + escapeIterations = params.get('escape_iterations', 255) + # May have been defaulted, so (re)set the param + params['escape_iterations'] = escapeIterations + + # Heck - defaults for window widths and heights too, matched to the aspect + # ratio of the project image. + if 'real_width' not in params or 'imag_width' not in params: + explorationWidth = mathSupport.createFloat(params['project_params'].get('exploration_mesh_width', 160.0)) + explorationHeight = mathSupport.createFloat(params['project_params'].get('exploration_mesh_height', 120.0)) + explorationAspect = explorationWidth / explorationHeight + if 'real_width' not in params and 'imag_width' not in params: + params['real_width'] = mathSupport.createFloat('3.0') + params['imag_width'] = mathSupport.createFloat(params['real_width'] / explorationAspect) + elif 'real_width' not in params: + params['real_width'] = mathSupport.createFloat(params['imag_width'] * explorationAspect) + else: # 'imag_width' not in params: + params['imag_width'] = mathSupport.createFloat(params['real_width'] / explorationAspect) + + # Finally, params writing which should happen whether or not we loaded from a marker params['wholeCacheFolder'] = os.path.join(params['project_name'], params['project_params']['exploration_output_path']) projectDefaultZoom = float(params['project_params'].get('exploration_default_zoom_factor', 0.8)) params['zoom_factor'] = params.get('zoom_factor', projectDefaultZoom) - - # Whether an incoming parameter or not, make some values available - # as parameters. - params['digits_precision'] = supportPrecision - - escapeIterations = params.get('escape_iterations', 255) - params['escape_iterations'] = escapeIterations + return params def nextClicked(event): diff --git a/time_tinker_explanation.txt b/time_tinker_explanation.txt index 2616b07..34abfa7 100644 --- a/time_tinker_explanation.txt +++ b/time_tinker_explanation.txt @@ -35,6 +35,8 @@ python3.9 ./mesh_explore.py --project='time_tinker' --real-width=2.0 --imag-widt NOTE: As exploring, the current window leaves behind a command invocation that can start the exploration script at the same place again. This is helpful when working in phases, and also when trying to tinker with specific parameters. +NOTE 2: Currently, the 'markers_to_timeline.py' script doesn't accept markers from mixed math_support types, so stick to just one while exploring. + ## Timeline Creation From 65d83bf10a5e397819908158c2d3ece93996565d Mon Sep 17 00:00:00 2001 From: clint Date: Fri, 24 Sep 2021 14:11:54 -0700 Subject: [PATCH 43/44] Denying overwrite of existing exploration marker number. Now forcing all params to interpolate into Decimal, for the base class MathSupport. --- fractalmath.py | 6 ++++++ mesh_explore.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/fractalmath.py b/fractalmath.py index ca79f68..36a1e56 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -271,6 +271,12 @@ def interpolate(self, transitionType, startX, startY, endX, endY, targetX, extra """ print(f"DiveMathSuppport interpolation type {transitionType}") + startX = self.decimal.Decimal(startX) + startY = self.decimal.Decimal(startY) + endX = self.decimal.Decimal(endX) + endY = self.decimal.Decimal(endY) + targetX = self.decimal.Decimal(targetX) + if transitionType == 'log-to': return self.interpolateLogTo(startX, startY, endX, endY, targetX) elif transitionType == 'root-to': diff --git a/mesh_explore.py b/mesh_explore.py index 7d381e2..a5cae4a 100644 --- a/mesh_explore.py +++ b/mesh_explore.py @@ -302,6 +302,10 @@ def saveMarkerClicked(event): marker = MeshMarker(diveMesh, currFrameNumber, params['algo'], params['escape_iterations']) markerFileName = getMarkerFileNameForFrameNumber(currFrameNumber) + if os.path.exists(markerFileName): + print(f"ERROR - chickening out at creating a marker file \"{markerFileName}\", because it already exists") + return + with open(markerFileName, 'wb') as markerHandle: pickle.dump(marker, markerHandle) From 16ae4db6cbf6186ad75a60a889226851032d2d43 Mon Sep 17 00:00:00 2001 From: clint Date: Sat, 25 Sep 2021 17:20:03 -0700 Subject: [PATCH 44/44] Added an MPFR distance estimate implementation. Added all but the Algo for Julia distance estimate too. --- fractalmath.py | 38 ++++-- mandeldistance.py | 7 +- mpfr_fractal_lib.c | 297 +++++++++++++++++++++++++++++++++++++++++-- mpfr_fractal_lib.h | 8 ++ mpfr_fractal_lib.o | Bin 2984 -> 0 bytes mpfr_fractalmath.pyx | 195 ++++++++++++++++++++++++++++ 6 files changed, 523 insertions(+), 22 deletions(-) delete mode 100644 mpfr_fractal_lib.o diff --git a/fractalmath.py b/fractalmath.py index 36a1e56..e75472a 100644 --- a/fractalmath.py +++ b/fractalmath.py @@ -650,15 +650,16 @@ def mandelbrotDistanceEstimate(self, realValues, imagValues, escapeRadius, maxIt currShape = realValues.shape results = np.empty(currShape, dtype=object) + smoothResults = np.empty(currShape, dtype=object) lastRealValues = np.empty(currShape, dtype=object) lastImagValues = np.empty(currShape, dtype=object) # numpyArray.shape returns (rows, columns) for y in range(currShape[0]): for x in range(currShape[1]): - (results[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.mandelbrotDistanceEstimateSingle(realValues[y,x], imagValues[y,x], escapeRadius, maxIter) + (results[y,x], smoothResults[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.mandelbrotDistanceEstimateSingle(realValues[y,x], imagValues[y,x], escapeRadius, maxIter) - return (results, lastRealValues, lastImagValues) + return (results, smoothResults, lastRealValues, lastImagValues) def mandelbrotDistanceEstimateSingle(self, realValue, imagValue, escapeRadius, maxIter): """ @@ -704,12 +705,16 @@ def mandelbrotDistanceEstimateSingle(self, realValue, imagValue, escapeRadius, m zrSquared = zReal * zReal ziSquared = zImag * zImag + + # NOTE: Hey, return result AND smoothed result, right? + + # Tried to extract the distance smoothing from this a few times, # but it seems a lot messier to pass around the (needed) derivative # as well, so we just go ahead and return an extra term here, knowing # this theoretically could be better handled in the 'normal' two steps. if result == maxIter: - return (result, zReal, zImag) + return (result, result, zReal, zImag) else: realPart = zReal * zReal imagPart = zImag * zImag @@ -722,18 +727,25 @@ def mandelbrotDistanceEstimateSingle(self, realValue, imagValue, escapeRadius, m ## Can't take logs of zMagnitude when <= 0, ## and can't divide by zero-valued dzMagnitude. ## (though these shouldn't ever be, because of sqrt?) - #if zMagnitude <= 0 or dzMagnitude <= 0: - # return (result, zReal, zImag) + if zMagnitude <= 0 or dzMagnitude <= 0: + return (result, result, zReal, zImag) # # Current most likely: #return ((zMagnitude * (zMagnitude.ln()) / dzMagnitude), zReal, zImag) # But need to include the actual result, right? #return (result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude), zReal, zImag) - tmpAnswer = result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude) - if result > 0: - return (tmpAnswer, zReal, zImag) - else: - return (self.decimal.Decimal(0), zReal, zImag) + #tmpAnswer = result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude) + tmpAnswer = result + (zMagnitude * (zMagnitude.ln()) / dzMagnitude) + return (result, tmpAnswer, zReal, zImag) + +# if result == 0: +# return (self.decimal.Decimal(0), zReal, zImag) +# else: +# tmpAnswer = result - (zMagnitude * (zMagnitude.ln()) / dzMagnitude) +# return (tmpAnswer, zReal, zImag) + # from https://github.com/JRWynneIII/Mandelbrot/blob/master/serial_mandelbrot.c + # (looks the same, however, the surrounding conditions look clearer/better?) + #dist=(log(x2+y2)*sqrt(x2+y2))/sqrt(xder*xder+yder*yder); # More math, slightly less blown out? #return ((zMagnitude * zMagnitude).ln() * zMagnitude / dzMagnitude, zReal, zImag) @@ -812,6 +824,7 @@ def juliaDistanceEstimateSingle(self, realValue, imagValue, realJuliaValue, imag # but it seems a lot messier to pass around the (needed) derivative # as well, so we just go ahead and return an extra term here, knowing # this theoretically could be better handled in the 'normal' two steps. + if result == maxIter: return (result, zReal, zImag) else: @@ -934,6 +947,11 @@ def mandelbrotToString(self, realValues, imagValues, escapeRadius, maxIterations """ return self.mpfrlib.mandelbrot_2d_pydecimal_to_string(realValues, imagValues, escapeRadius, maxIterations, self.currPrecision) + def mandelbrotDistanceEstimate(self, realValues, imagValues, escapeRadius, maxIter): + return self.mpfrlib.mandelbrot_distance_2d_pydecimal_to_decimal(realValues, imagValues, escapeRadius, maxIter, self.currPrecision) + + def juliaDistanceEstimate(self, realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIter): + return self.mpfrlib.julia_distance_2d_pydecimal_to_decimal(realValues, imagValues, realJuliaValue, imagJuliaValue, escapeRadius, maxIterations, self.currPrecision) class DiveMathSupportFlint(DiveMathSupport): """ diff --git a/mandeldistance.py b/mandeldistance.py index 1159e0e..d376087 100644 --- a/mandeldistance.py +++ b/mandeldistance.py @@ -18,6 +18,8 @@ def __init__(self, dive_mesh, frame_number, output_folder_name, extra_params={}) super().__init__(dive_mesh, frame_number, output_folder_name, extra_params) self.algorithm_name = 'mandeldistance' + self.smooth_counts_array = None + self.escape_radius = 100000 # Because, who knows. def generate_counts(self): """ @@ -27,13 +29,16 @@ def generate_counts(self): """ #mandelbrot_function = np.vectorize(self.dive_mesh.mathSupport.mandelbrotDistanceEstimate) #(self.counts_array, self.last_values_array) = mandelbrot_function(self.mesh_array, self.escape_radius, self.max_escape_iterations) - (self.counts_array, self.last_values_real_array, self.last_values_imag_array) = self.dive_mesh.mathSupport.mandelbrotDistanceEstimate(self.mesh_real_array, self.mesh_imag_array, self.escape_radius, self.max_escape_iterations) + (self.counts_array, self.smooth_counts_array, self.last_values_real_array, self.last_values_imag_array) = self.dive_mesh.mathSupport.mandelbrotDistanceEstimate(self.mesh_real_array, self.mesh_imag_array, self.escape_radius, self.max_escape_iterations) counts_name_base = u"%d.counts.pik" % self.frame_number counts_file_name = os.path.join(self.output_folder_name, counts_name_base) with open(counts_file_name, 'wb') as counts_handle: pickle.dump(self.counts_array, counts_handle) + def process_counts(self): + self.processed_array = self.smooth_counts_array + # def process_counts(self): # smoothing_function = np.vectorize(self.dive_mesh.mathSupport.rescaleForRange) # self.processed_array = smoothing_function(self.last_values_array, self.counts_array, self.max_escape_iterations, self.dive_mesh.realMeshGenerator.baseWidth) diff --git a/mpfr_fractal_lib.c b/mpfr_fractal_lib.c index 2dd375e..b11c286 100644 --- a/mpfr_fractal_lib.c +++ b/mpfr_fractal_lib.c @@ -17,7 +17,10 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i // a single implementation. const char *zero_string = "0.0"; mpfr_julia_steps(result, last_z_real_str, last_z_imag_str, start_real_str, start_imag_str, zero_string, zero_string, radius, max_iter, prec); -/* +} + +void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) +{ // Step-wise algorithm modified from: // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ // Calculates (zreal + zimag)^2 to get @@ -32,7 +35,7 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i mpfr_t temp_magnitude; mpfr_t temp_sum_a; mpfr_t temp_sub_a; - mpfr_t rad_squared; + mpfr_t rad_squared; mpfr_t start_real; mpfr_t start_imag; @@ -52,7 +55,7 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i mpfr_init(temp_sub_a); mpfr_init(rad_squared); - mpfr_init2(start_real, prec); + mpfr_init(start_real); mpfr_init(start_imag); mpfr_init(last_z_real); mpfr_init(last_z_imag); @@ -73,17 +76,20 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i // r^2 mpfr_set_d(rad_squared, (double)(radius * radius), MPFR_RNDN); - // initialize Z as zero - mpfr_set_zero(last_z_real,1); - mpfr_set_zero(last_z_imag,1); - mpfr_set_zero(zr_squared,1); - mpfr_set_zero(zi_squared,1); + // Julia init is to the julia value, which means squares should be initialized too. + mpfr_set_str(last_z_real, julia_real_str, 10, MPFR_RNDN); // 10 = base, not precision + mpfr_set_str(last_z_imag, julia_imag_str, 10, MPFR_RNDN); + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); // Since the zero iteration is just setting the starting values, it's not // counted in the iterations count (so loop until *equal to* max_iter). + // This *seems* to still be reasonable for Julia iteration, even though the + // bit about 'just setting the starting values' doesn't seem as correct. for(long currIter = 0; currIter <= max_iter; currIter++) { *result = currIter; + mpfr_add(temp_magnitude, zr_squared, zi_squared, MPFR_RNDN); //mpfr_sprintf(print_buffer, "%Re", temp_magnitude); @@ -136,10 +142,12 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i mpfr_clear(start_imag); mpfr_clear(last_z_real); mpfr_clear(last_z_imag); -*/ } -void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) +// Notably, the type of 'result' is different for distance estimation, +// because the extra smoothing calculation is handled in-place where the +// derivative is available. +void mpfr_mandelbrot_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) { // Step-wise algorithm modified from: // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ @@ -161,6 +169,10 @@ void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_s mpfr_t start_imag; mpfr_t last_z_real; mpfr_t last_z_imag; + + mpfr_t dz_real; + mpfr_t dz_imag; + mpfr_t dz_real_temp; long print_buffer_size = 65536; char print_buffer[print_buffer_size]; @@ -179,10 +191,216 @@ void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_s mpfr_init(start_imag); mpfr_init(last_z_real); mpfr_init(last_z_imag); + + mpfr_init(dz_real); + mpfr_init(dz_imag); + mpfr_init(dz_real_temp); - mpfr_set_str(start_real, start_real_str, 10, MPFR_RNDN); // 10 = base, not precision + // for set_str, magic number param 10 = base, not precision + mpfr_set_str(start_real, start_real_str, 10, MPFR_RNDN); + mpfr_set_str(start_imag, start_imag_str, 10, MPFR_RNDN); + + // Mandelbrot derivative initializes to (0+0j), + // FYI: however, Julia derivative inittialize to (1+0j), + mpfr_set_d(dz_real, 0.0, MPFR_RNDN); + mpfr_set_d(dz_imag, 0.0, MPFR_RNDN); + + //printf("start_real_str \"%s\"\n", start_real_str); + //printf("start_imag_str \"%s\"\n", start_imag_str); + + //printf("start_real: "); + //mpfr_out_str(stdout, 10,100,start_real, MPFR_RNDN); + //printf("\nstart_imag: "); + //mpfr_out_str(stdout, 10,100,start_imag, MPFR_RNDN); + //printf("\n"); + //fflush(stdout); + + // r^2 + mpfr_set_d(rad_squared, (double)(radius * radius), MPFR_RNDN); + + // Mandelbrot inits are to zeroes + mpfr_set_d(last_z_real, 0.0, MPFR_RNDN); + mpfr_set_d(last_z_imag, 0.0, MPFR_RNDN); + + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); + + // Since the zero iteration is just setting the starting values, it's not + // counted in the iterations count (so loop until *equal to* max_iter). + // This *seems* to still be reasonable for Julia iteration, even though the + // bit about 'just setting the starting values' doesn't seem as correct. + for(long currIter = 0; currIter <= max_iter; currIter++) + { + *result = currIter; + + mpfr_add(temp_magnitude, zr_squared, zi_squared, MPFR_RNDN); + + //mpfr_sprintf(print_buffer, "%Re", temp_magnitude); + //printf("%s\n", print_buffer); + //fflush(stdout); + + if(mpfr_cmp(temp_magnitude, rad_squared) > 0) + { + break; + } + + // Mandelbrot derivative is Z' -> 2*Z*Z' + 1 + // FYI: Julia derivative is Z' -> 2*Z*Z', without the extra "+1" + mpfr_mul(temp_sum_a, last_z_real, dz_real, MPFR_RNDN); + mpfr_mul(temp_sub_a, last_z_imag, dz_imag, MPFR_RNDN); + mpfr_sub(temp, temp_sum_a, temp_sub_a, MPFR_RNDN); + mpfr_add(temp_sum_a, temp, temp, MPFR_RNDN); + mpfr_add_d(dz_real_temp, temp_sum_a, 1.0, MPFR_RNDN); + + mpfr_mul(temp_sum_a, last_z_real, dz_imag, MPFR_RNDN); + mpfr_mul(temp, last_z_imag, dz_real, MPFR_RNDN); + mpfr_add(dz_imag, temp_sum_a, temp, MPFR_RNDN); + mpfr_swap(dz_real, dz_real_temp); + + // Looks like ~4 percent increase in speed by using mpfr_sqr where + // possible, instead of mpfr_mul (which is interesting, because + // arb_sqr just redirects to mul). + // + // The mpfr docs claim in-place operations work too (where I'm pretty + // sure I had bugs trying to use arb_add(a,a,b) functions), and + // should be preferred over temporary values... but I haven't run + // any comparisons on that yet, so don't know? + mpfr_add(temp_sum_a, last_z_real, last_z_imag, MPFR_RNDN); + mpfr_sqr(temp, temp_sum_a, MPFR_RNDN); + mpfr_sub(temp_sub_a, temp, zr_squared, MPFR_RNDN); + mpfr_sub(temp, temp_sub_a, zi_squared, MPFR_RNDN); + + mpfr_add(last_z_imag, temp, start_imag, MPFR_RNDN); + + mpfr_sub(temp_sub_a, zr_squared, zi_squared, MPFR_RNDN); + mpfr_add(last_z_real, temp_sub_a, start_real, MPFR_RNDN); + + mpfr_sqr(zr_squared, last_z_real, MPFR_RNDN); + mpfr_sqr(zi_squared, last_z_imag, MPFR_RNDN); + } + + // Print the value to the print buffer, then create a new string + // of appropriate length from there? + mpfr_sprintf(print_buffer, "%Re", last_z_real); + *last_z_real_str = strdup(print_buffer); + mpfr_sprintf(print_buffer, "%Re", last_z_imag); + *last_z_imag_str = strdup(print_buffer); + + if(*result == max_iter) + { + *distance_result = *result; + //sprintf(print_buffer, "%lu", *result); + //*distance_result = strdup(print_buffer); + } + else + { + // Just reusing variables here, even though they're named + // for the main loop above. Sorry. + mpfr_add(temp, zr_squared, zi_squared, MPFR_RNDN); + mpfr_sqrt(temp_magnitude, temp, MPFR_RNDN); + + mpfr_sqr(dz_real_temp, dz_real, MPFR_RNDN); + mpfr_sqr(temp_sum_a, dz_imag, MPFR_RNDN); + mpfr_add(temp, dz_real_temp, temp_sum_a, MPFR_RNDN); + mpfr_sqrt(dz_real_temp, temp, MPFR_RNDN); + + // Now, temp_magnitude is zMagnitude, + // and dz_real_temp is dzMagnitude. + if(mpfr_cmp_d(temp_magnitude, 0.0) <= 0 || + mpfr_cmp_d(dz_real_temp, 0.0) <= 0) + { + *distance_result = *result; + //sprintf(print_buffer, "%lu", *result); + //*distance_result = strdup(print_buffer); + } + else + { + mpfr_log(temp_sum_a, temp_magnitude, MPFR_RNDN); + mpfr_mul(temp, temp_sum_a, temp_magnitude, MPFR_RNDN); + mpfr_div(temp_sum_a, temp, dz_real_temp, MPFR_RNDN); + mpfr_add_si(temp, temp_sum_a, *result, MPFR_RNDN); + + *distance_result = mpfr_get_ld(temp, MPFR_RNDN); + //mpfr_sprintf(print_buffer, "%Re", temp); + //*distance_result = strdup(print_buffer); + } + } + + mpfr_clear(temp); + mpfr_clear(zr_squared); + mpfr_clear(zi_squared); + mpfr_clear(temp_magnitude); + mpfr_clear(temp_sum_a); + mpfr_clear(temp_sub_a); + mpfr_clear(rad_squared); + + mpfr_clear(start_real); + mpfr_clear(start_imag); + mpfr_clear(last_z_real); + mpfr_clear(last_z_imag); + + mpfr_clear(dz_real); + mpfr_clear(dz_imag); + mpfr_clear(dz_real_temp); +} + +void mpfr_julia_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) +{ + // Step-wise algorithm modified from: + // https://randomascii.wordpress.com/2011/08/13/faster-fractals-through-algebra/ + // Calculates (zreal + zimag)^2 to get + // zr^2 + 2*zr*zi + zi^2 + // Which is helpful, because for the iteration, zr^2 and zi^2 + // were already calculated. + // So subtract them, and the remaining term is just 2*zr*zi. + + mpfr_t temp; + mpfr_t zr_squared; + mpfr_t zi_squared; + mpfr_t temp_magnitude; + mpfr_t temp_sum_a; + mpfr_t temp_sub_a; + mpfr_t rad_squared; + + mpfr_t start_real; + mpfr_t start_imag; + mpfr_t last_z_real; + mpfr_t last_z_imag; + + mpfr_t dz_real; + mpfr_t dz_imag; + mpfr_t dz_real_temp; + + long print_buffer_size = 65536; + char print_buffer[print_buffer_size]; + + mpfr_set_default_prec(prec); + + mpfr_init(temp); + mpfr_init(zr_squared); + mpfr_init(zi_squared); + mpfr_init(temp_magnitude); + mpfr_init(temp_sum_a); + mpfr_init(temp_sub_a); + mpfr_init(rad_squared); + + mpfr_init(start_real); + mpfr_init(start_imag); + mpfr_init(last_z_real); + mpfr_init(last_z_imag); + + mpfr_init(dz_real); + mpfr_init(dz_imag); + mpfr_init(dz_real_temp); + + // for set_str, magic number param 10 = base, not precision + mpfr_set_str(start_real, start_real_str, 10, MPFR_RNDN); mpfr_set_str(start_imag, start_imag_str, 10, MPFR_RNDN); + // Julia derivative inittialize to (1+0j), but mandelbrot is (0+0j), FYI + mpfr_set_d(dz_real, 1.0, MPFR_RNDN); + mpfr_set_d(dz_imag, 0.0, MPFR_RNDN); + //printf("start_real_str \"%s\"\n", start_real_str); //printf("start_imag_str \"%s\"\n", start_imag_str); @@ -221,6 +439,19 @@ void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_s break; } + // Julia derivative is Z' -> 2*Z*Z' + // FYI: There's an extra "+1" for mandelbrot dzReal, but + // julia is just 2*Z*Z' + mpfr_mul(temp_sum_a, last_z_real, dz_real, MPFR_RNDN); + mpfr_mul(temp_sub_a, last_z_imag, dz_imag, MPFR_RNDN); + mpfr_sub(temp, temp_sum_a, temp_sub_a, MPFR_RNDN); + mpfr_add(dz_real_temp, temp, temp, MPFR_RNDN); + + mpfr_mul(temp_sum_a, last_z_real, dz_imag, MPFR_RNDN); + mpfr_mul(temp, last_z_imag, dz_real, MPFR_RNDN); + mpfr_add(dz_imag, temp_sum_a, temp, MPFR_RNDN); + mpfr_swap(dz_real, dz_real_temp); + // Looks like ~4 percent increase in speed by using mpfr_sqr where // possible, instead of mpfr_mul (which is interesting, because // arb_sqr just redirects to mul). @@ -250,6 +481,46 @@ void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_s mpfr_sprintf(print_buffer, "%Re", last_z_imag); *last_z_imag_str = strdup(print_buffer); + if(*result == max_iter) + { + *distance_result = *result; + //sprintf(print_buffer, "%lu", *result); + //*distance_result = strdup(print_buffer); + } + else + { + // Just reusing variables here, even though they're named + // for the main loop above. Sorry. + mpfr_add(temp, zr_squared, zi_squared, MPFR_RNDN); + mpfr_sqrt(temp_magnitude, temp, MPFR_RNDN); + + mpfr_sqr(dz_real_temp, dz_real, MPFR_RNDN); + mpfr_sqr(temp_sum_a, dz_imag, MPFR_RNDN); + mpfr_add(temp, dz_real_temp, temp_sum_a, MPFR_RNDN); + mpfr_sqrt(dz_real_temp, temp, MPFR_RNDN); + + // Now, temp_magnitude is zMagnitude, + // and dz_real_temp is dzMagnitude. + if(mpfr_cmp_d(temp_magnitude, 0.0) <= 0 || + mpfr_cmp_d(dz_real_temp, 0.0) <= 0) + { + *distance_result = *result; + //sprintf(print_buffer, "%lu", *result); + //distance_result = strdup(print_buffer); + } + else + { + mpfr_log(temp_sum_a, temp_magnitude, MPFR_RNDN); + mpfr_mul(temp, temp_sum_a, temp_magnitude, MPFR_RNDN); + mpfr_div(temp_sum_a, temp, dz_real_temp, MPFR_RNDN); + mpfr_add_si(temp, temp_sum_a, *result, MPFR_RNDN); + + //mpfr_sprintf(print_buffer, "%Re", temp); + //*distance_result = strdup(print_buffer); + *distance_result = mpfr_get_ld(temp, MPFR_RNDN); + } + } + mpfr_clear(temp); mpfr_clear(zr_squared); mpfr_clear(zi_squared); @@ -262,5 +533,9 @@ void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_s mpfr_clear(start_imag); mpfr_clear(last_z_real); mpfr_clear(last_z_imag); + + mpfr_clear(dz_real); + mpfr_clear(dz_imag); + mpfr_clear(dz_real_temp); } diff --git a/mpfr_fractal_lib.h b/mpfr_fractal_lib.h index f2fee0c..2a97f10 100644 --- a/mpfr_fractal_lib.h +++ b/mpfr_fractal_lib.h @@ -9,3 +9,11 @@ void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_i void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec); +// Notably, the type of 'result' is different for distance estimation, +// because the extra smoothing calculation is handled in-place where the +// derivative is available. +// Also, can't just call 'julia' from 'mandelbrot' for distance estimate, +// because the derivative iteration is subtly different. +void mpfr_mandelbrot_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec); + +void mpfr_julia_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec); diff --git a/mpfr_fractal_lib.o b/mpfr_fractal_lib.o deleted file mode 100644 index 513745caf28f98f5009b53ba0e3b8a44abf57fce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2984 zcmc&$ZD?Cn7`~ZZmu>A5WrK>!0;@8`NTv)bI;hmZ5pJQ(7MxqXOOtfT`mrn@TEsv# zT-W2}&VuBJiXe#l<&P*7$}m#2qLYE~3;M%B9SR+EQm9Iuy8685zBj$ODEQ9<=RWV} zbKdiw`}O6G>o<33THqE97tA)y;5J+$m>9QX&XD6@#`1Q@agcH_TpgSmP2!_r7{&Bd z(UBc}F#gowmhMXTd-zl>K}|bM&Ngk@jUhCQWTBYPPM%_;VYKqOJV;k#kc#mPF1Kj` z-40Ogt|$oZK$0HDSS%O+9C zt3pm9)zUJw<5}5cLAJg?_CrU`&1So?OyU*rg^WYW4gGu5>9&@slR7o-_%#?cBjhlhI1JZ&xhS>-Q<*{L<1UkiIq zC8IOa{PQn&ak8rR6{{I6fP_kp9yOKvg(wLrsm!bnM4I0>BU<~s z)M9rwNPYg+wR7vlc4`l9Wor7hSJVpC8i7?0EA8-3MAW#mj!~M8CXMZM&$6c}S_ZcA zz5=m7@A6h&xq(N@d5#1B)1!kP2vzl>u+)27Sa@6XuZf3051I5~5q5e5FDg}DXm{*^ zn0EJ3e(}Rg;|0us7N91!jA_>1!z=Tj{Cn9Sc>bssmt^ zuf}70!Y^;@>uir#=<}rF=tH)^akP#aZk%x9?!ms05Zwht$dTW1X@`ED@dw857%wr_ z8Q*7om+^JRS;ljWMPLZLBft*eaUfpl`yOTceQZC#_Fqv{C*rO!ehNg1(RYCOjz!P0 zo?%^Qi~%XHi~T=i5u8-C0i=8zNd6MzFc9C&XfI;~h)-&C1HC5u65~g}AoLFyFEGBr zc%Jbk#%CE%Gd{yO#Hcg&0r5hP?gwJ&=$kkswBX2D=d_|xAnf}hK!o6E96SIF!7xn* z4D?j#O2H+833qc8eEMrX{en;L_vu%BI&pjXYd-x$pHBaB-gwie_xbd1eEKn;t{D@# z(Y%qJ%obUkNKB^E<0JVqMWawm=L&2-T^i3OS}cV$MdaCEIO|Hu@pJ+*T&YqHKbrV8 z%#0O^V@4{G$FCt-3dKZnjLgPpB0KIf%K!iX diff --git a/mpfr_fractalmath.pyx b/mpfr_fractalmath.pyx index f6c7f0b..caa6de4 100644 --- a/mpfr_fractalmath.pyx +++ b/mpfr_fractalmath.pyx @@ -8,6 +8,9 @@ cdef extern from "mpfr_fractal_lib.h": void mpfr_mandelbrot_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) void mpfr_julia_steps(long *result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) + void mpfr_mandelbrot_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, float radius, const long max_iter, long prec) + void mpfr_julia_distance_estimate(long *result, long double *distance_result, char **last_z_real_str, char **last_z_imag_str, const char *start_real_str, const char *start_imag_str, const char *julia_real_str, const char *julia_imag_str, float radius, const long max_iter, long prec) + def mandelbrot_steps(string_real, string_imag, escape_radius, max_iterations, precision): # Temporaries used for every call cdef long answer = 0 @@ -228,4 +231,196 @@ def julia_2d_pydecimal_to_decimal(decimal_reals, decimal_imags, decimal_julia_re decimal_last_z_imags[x,y] = decimal.Decimal(string_last_z_imags[x,y]) return (count_results, decimal_last_z_reals, decimal_last_z_imags) + +def mandelbrot_distance_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision): + if string_reals.shape != string_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = string_reals.shape # (rows, columns) + + # Python-create the numpy arrays. Originally thought we'd then + # cython-assign them to be used as memoryviews, but it's gnarly + # for strings, and really doesn't save much overall. + count_results = np.zeros(answer_shape, dtype=float) + smooth_results = np.zeros(answer_shape, dtype=float) + last_z_reals = np.empty(answer_shape, dtype=object) + last_z_imags = np.empty(answer_shape, dtype=object) + + # Temporaries used for every call + cdef long answer = 0 + cdef long double smooth_answer = 0.0 + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + start_real_py_str = string_reals[x,y].encode('ascii') + start_imag_py_str = string_imags[x,y].encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + #print(f"param's start_real_str \"{start_real_str}\"") + #print(f"param's start_imag_str \"{start_imag_str}\"") + mpfr_mandelbrot_distance_estimate(&answer, &smooth_answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, float(escape_radius), max_iterations, precision) + + # Extra step for string conversion back from bytes to python string + #print(f"last_z_real_str \"{last_z_real_str}\"") + #print(f"last_z_imag_str \"{last_z_imag_str}\"") + last_z_reals[x,y] = last_z_real_str.decode('ascii') + last_z_imags[x,y] = last_z_imag_str.decode('ascii') + + count_results[x,y] = answer + smooth_results[x,y] = smooth_answer + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + #print(f"answer {answer}") + + return (count_results, smooth_results, last_z_reals, last_z_imags) + +def mandelbrot_distance_2d_pydecimal_to_string(decimal_reals, decimal_imags, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + # TODO: Quite possibly want to set decimal's precision here? + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + return mandelbrot_distance_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision) + +def mandelbrot_distance_2d_pydecimal_to_decimal(decimal_reals, decimal_imags, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + (count_results, smooth_results, string_last_z_reals, string_last_z_imags) = mandelbrot_distance_2d_string_to_string(string_reals, string_imags, escape_radius, max_iterations, precision) + + decimal_last_z_reals = np.empty(answer_shape, dtype=object) + decimal_last_z_imags = np.empty(answer_shape, dtype=object) + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + decimal_last_z_reals[x,y] = decimal.Decimal(string_last_z_reals[x,y]) + decimal_last_z_imags[x,y] = decimal.Decimal(string_last_z_imags[x,y]) + + return (count_results, smooth_results, decimal_last_z_reals, decimal_last_z_imags) + +def julia_distance_2d_string_to_string(string_reals, string_imags, string_julia_real, string_julia_imag, escape_radius, max_iterations, precision): + if string_reals.shape != string_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = string_reals.shape # (rows, columns) + + # Python-create the numpy arrays. Originally thought we'd then + # cython-assign them to be used as memoryviews, but it's gnarly + # for strings, and really doesn't save much overall. + count_results = np.zeros(answer_shape, dtype=float) + smooth_results = np.zeros(answer_shape, dtype=float) + last_z_reals = np.empty(answer_shape, dtype=object) + last_z_imags = np.empty(answer_shape, dtype=object) + + # Temporaries used for every call + cdef long answer = 0 + cdef long double smooth_answer = 0.0 + cdef char *last_z_real_str = NULL + cdef char *last_z_imag_str = NULL + + cdef char *start_real_str = NULL + cdef char *start_imag_str = NULL + + cdef char *julia_real_str = NULL + cdef char *julia_imag_str = NULL + julia_real_py_str = string_julia_real.encode('ascii') + julia_imag_py_str = string_julia_imag.encode('ascii') + julia_real_str = julia_real_py_str + julia_imag_str = julia_imag_py_str + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + # Shuffle to keep temporary strings from becoming parameters + # Techincally, this captures the 'bytes' from the .encode() + # call, so the assignment to a cython char* is allowed. + start_real_py_str = string_reals[x,y].encode('ascii') + start_imag_py_str = string_imags[x,y].encode('ascii') + start_real_str = start_real_py_str + start_imag_str = start_imag_py_str + + #print(f"param's start_real_str \"{start_real_str}\"") + #print(f"param's start_imag_str \"{start_imag_str}\"") + mpfr_julia_distance_estimate(&answer, &smooth_answer, &last_z_real_str, &last_z_imag_str, start_real_str, start_imag_str, julia_real_str, julia_imag_str, float(escape_radius), max_iterations, precision) + + # Extra step for string conversion back from bytes to python string + #print(f"last_z_real_str \"{last_z_real_str}\"") + #print(f"last_z_imag_str \"{last_z_imag_str}\"") + last_z_reals[x,y] = last_z_real_str.decode('ascii') + last_z_imags[x,y] = last_z_imag_str.decode('ascii') + + count_results[x,y] = answer + smooth_results[x,y] = smooth_answer + # TODO: Almost certainly need to free last_z_real_str + # and last_z_imag_str, right? + + #print(f"answer {answer}") + + return (count_results, smooth_results, last_z_reals, last_z_imags) + +def julia_distance_2d_pydecimal_to_string(decimal_reals, decimal_imags, decimal_julia_real, decimal_julia_imag, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + # TODO: Quite possibly want to set decimal's precision here? + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + return julia_distance_2d_string_to_string(string_reals, string_imags, str(decimal_julia_real), str(decimal_julia_imag), escape_radius, max_iterations, precision) + +def julia_distance_2d_pydecimal_to_decimal(decimal_reals, decimal_imags, decimal_julia_real, decimal_julia_imag, escape_radius, max_iterations, precision): + if decimal_reals.shape != decimal_imags.shape: + raise ValueError("Array of real string shape doesn't match array of imag string shape.") + + answer_shape = decimal_reals.shape # (rows, columns) + string_reals = np.empty(answer_shape, dtype=object) + string_imags = np.empty(answer_shape, dtype=object) + + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + string_reals[x,y] = str(decimal_reals[x,y]) + string_imags[x,y] = str(decimal_imags[x,y]) + + (count_results, smooth_results, string_last_z_reals, string_last_z_imags) = julia_2d_string_to_string(string_reals, string_imags, str(decimal_julia_real), str(decimal_julia_imag), escape_radius, max_iterations, precision) + + decimal_last_z_reals = np.empty(answer_shape, dtype=object) + decimal_last_z_imags = np.empty(answer_shape, dtype=object) + for y in range(answer_shape[1]): + for x in range(answer_shape[0]): + decimal_last_z_reals[x,y] = decimal.Decimal(string_last_z_reals[x,y]) + decimal_last_z_imags[x,y] = decimal.Decimal(string_last_z_imags[x,y]) + + return (count_results, smooth_results, decimal_last_z_reals, decimal_last_z_imags)