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/README.md b/README.md index 8144f0a..e373f82 100644 --- a/README.md +++ b/README.md @@ -1 +1,112 @@ # 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. + - 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. + + +# 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/algo.py b/algo.py new file mode 100644 index 0000000..bf18e0a --- /dev/null +++ b/algo.py @@ -0,0 +1,224 @@ + + +import os +import pickle + +import fractalcache as fc +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? + +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. + + 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 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 load_options_with_math_support(opts, math_support): + return {} + + 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 + + # 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 + + 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.getCenter()), + '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 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 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. + """ + # 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()') + + def pre_process_hook(self): + pass + + 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. + + 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): + """ + 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 EscapeAlgo(Algo): + + @staticmethod + def options_list(): + whole_list = Algo.options_list() + + whole_list.extend(["escape-radius=", + "max-escape-iterations=", + "burn", + "palette=", + ]) + + return whole_list + + @staticmethod + 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']: + options['escape_radius'] = float(arg) + elif opt in ['--max-escape-iterations']: + options['max_escape_iterations'] = int(arg) + elif opt in ['--burn']: + options['burn_in'] = True + elif opt in ['--palette']: + options['palette'] = arg + + 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) + + # Load, with optional default values + 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()) + + 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 JuliaAlgo(EscapeAlgo): + + @staticmethod + def options_list(): + whole_list = EscapeAlgo.options_list() + + whole_list.extend(["julia-center="]) + + return whole_list + + @staticmethod + 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.load_options_with_math_support(opts, math_support) + + for opt,arg in opts: + if opt in ['--julia-center']: + options['julia_center'] = math_support.createComplex(arg) + + 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.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) + + 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/batch_build.sh b/batch_build.sh new file mode 100755 index 0000000..450a9bb --- /dev/null +++ b/batch_build.sh @@ -0,0 +1,81 @@ +#!/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. + + +# 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& + + +# 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=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=0 +lastNumber=21 + +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' + + + + +# 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& +#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/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/clint_runscript.sh b/clint_runscript.sh new file mode 100755 index 0000000..c14af75 --- /dev/null +++ b/clint_runscript.sh @@ -0,0 +1,53 @@ + +#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 + +#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 + +#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 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 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 +# to render on the second invocation. + +## 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 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/compile_video.py b/compile_video.py new file mode 100644 index 0000000..9950127 --- /dev/null +++ b/compile_video.py @@ -0,0 +1,89 @@ + +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 new file mode 100644 index 0000000..553f0a7 --- /dev/null +++ b/divemesh.py @@ -0,0 +1,456 @@ +# -- +# 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, 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.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 + + # 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 + 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): + """ + 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 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 + were saved at. + """ + mathSupportClasses = {"DiveMathSupportFlintCustom":fm.DiveMathSupportFlintCustom, + "DiveMathSupportFlint":fm.DiveMathSupportFlint, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, + "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 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) + 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 + # 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) + # 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): + 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} ]\ +""".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 __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, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, + "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") + +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) + #print(f"making MeshGeneratorUniform with {mathSupport.digitsPrecision()} at center {valuesCenter}") + + 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)) + #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 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) + + #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(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(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] + + return mesh + + def __repr__(self): + return """\ +[MeshGeneratorUniform valuesCenter:{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, tiltAngle, tiltMaxRatio=10.0): + """ + Tilt is symmetric about the baseWidth. + + 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. + """ + super().__init__(mathSupport, varyingAxis) + self.valuesCenter = valuesCenter + self.baseWidth = baseWidth + self.maxAbsTiltAngle = 45 + self.tiltAngle = tiltAngle + + 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? + + 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 + # 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) + +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/env.txt b/env.txt index b25a969..a36c8f1 100644 --- a/env.txt +++ b/env.txt @@ -10,3 +10,72 @@ 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 + + +################################################## +#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 +brew install mpir + +# MPFR (4.x kinda matters here) +#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 + +# Flint library +# Flint2 takes a while to build +git clone https://github.com/fredrik-johansson/flint2 +cd flint2 +#./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 +cd .. + +# Python wrapper +git clone https://github.com/python-flint/python-flint +cd python-flint +python3 setup.py build_ext +sudo python3 setup.py install +cd .. + + + diff --git a/fractal.py b/fractal.py new file mode 100644 index 0000000..fdbc9b1 --- /dev/null +++ b/fractal.py @@ -0,0 +1,942 @@ +# -- +# File: mandelbrot.py +# +# Driver file for playing around with the Mandelbrot 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 getopt +import sys +import math +import os + +import pickle +import json # For params file reading and writing + +import multiprocessing # Can't actually make this work yet - gonna need pickling? + +from collections import defaultdict + +import numpy as np + +import moviepy.editor as mpy +from scipy.stats import norm + +from moviepy.audio.tools.cuts import find_audio_period + +from PIL import Image, ImageDraw, ImageFont + +# -- our files +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 mandelbrot_solo import MandelbrotSolo +from mandelbrot_smooth import MandelbrotSmooth +from mandeldistance import MandelDistance +from julia_solo import JuliaSolo +from julia_smooth import JuliaSmooth + +MANDL_VER = "0.1" + +class DiveTimeline: + """ + 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 + - DiveSpanCustomKeyframe (not implemented yet) + + # TODO: Seems like algorithm should be a per-span property, instead of + # per-timeline, doesn't it? + """ + + @staticmethod + def algorithm_map(): + return {'mandelbrot_solo' : MandelbrotSolo, + 'mandelbrot_smooth' : MandelbrotSmooth, + 'mandeldistance' : MandelDistance, + 'julia_solo' : JuliaSolo, + 'julia_smooth' : JuliaSmooth, + #'smooth': Smooth, + } + + @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.algorithmName = algorithmName + + 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 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] + + 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 + + def getSpansForTime(self, targetTime): + overlappingSpans = [] + for currSpan in self.timelineSpans: + 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. + + Order of operations: + 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 + + #print(f"parameters near the end of getMeshForTime(): \"{parameterValues}\"") + + if meshRealTilt == 0.0: + realMeshGenerator = mesh.MeshGeneratorUniform(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenter.real, baseWidth=meshRealWidth) + else: + realMeshGenerator = mesh.MeshGeneratorTilt(mathSupport=self.mathSupport, varyingAxis='width', valuesCenter=meshCenter.real, baseWidth=meshRealWidth, tiltAngle=float(meshRealTilt)) + + 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) + + #print(diveMesh) + return diveMesh + + 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 + + return pickleInfo + + 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, + "DiveMathSupportMPFR":fm.DiveMathSupportMPFR, + "DiveMathSupport":fm.DiveMathSupport} + + (mathSupportClassName, precisionString) = state['mathSupport'].split(':') + #print("mathSupport reads as: %s" % mathSupportClassName) + + 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.projectFolderName = state['projectFolderName'] + self.algorithmName = state['algorithmName'] + self.framerate = float(state['framerate']) + self.frameWidth = int(state['frameWidth']) + self.frameHeight = int(state['frameHeight']) + + for storedSpanInfo in state['timelineSpans']: + newSpan = self.addNewSpan(storedSpanInfo['time'], storedSpanInfo['duration']) + newSpan.__setstate__(storedSpanInfo) + + +class DiveTimelineSpan: + """ + + """ + def __init__(self, timeline, timePosition, duration): + self.timeline = timeline + self.time = int(timePosition) + self.duration = int(duration) + + # 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 getParamValuesAtTime(self, targetTime, currentParameters): + """ + 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. + """ + 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 targetTime in keyframesByTime: + targetKeyframe = keyframesByTime[targetTime] + targetKeyframe.lastObservedTime = targetTime + answerKeyframes.append((currParamName, targetKeyframe, targetKeyframe)) + continue + + previousKeyframe = None + nextKeyframe = None + 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.append((currParamName, previousKeyframe, nextKeyframe)) + + #print("found: %s" % str(answerKeyframes)) + return answerKeyframes + + def interpolateBetweenKeyframes(self, targetTime, leftKeyframe, rightKeyframe): + """ + Relies heavily on the stashed/cached 'lastObservedTime' of a keyframe + """ + print("interpolating %s -> %s at time %s" % (str(leftKeyframe), str(rightKeyframe), str(targetTime))) + + 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)) + + # 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}\"") + + + #### + # 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 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, 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)) + +# 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.lastObservedTime = 0 # For stashing times in + + def __repr__(self): + return(f"DiveSpanKeyframe {self.lastObservedTime} {self.dataType} {self.value}, in={self.transitionIn}, out={self.transitionOut}") + +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): + """ + 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_image_width": "1024", + "render_image_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 loads the values + from params.json into params['project_params'] + """ + 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-file=", + ] + + # 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, + 'mpfr': fm.DiveMathSupportMPFR, + 'flint': fm.DiveMathSupportFlint, + 'flintcustom': fm.DiveMathSupportFlintCustom, + } + math_support = math_support_classes['native']() # Creates an instance + for opt, arg in opts: + 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(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 + # 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].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. + + elif params['mode'] == 'timeline': + for opt, arg in opts: + 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_file'] = arg + + return params + +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, 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') + + 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'] + 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']) + + # 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() + +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) +# +# # 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) +# +# timeline = debugTimelineMaker(params) +#### + +def debugTimelineMaker(params): + project_folder_name = params['project_name'] + math_support = params['math_support'] + + 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): + """ + 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 = params['batch_frame_file'] + with open(batch_frame_file, 'rt') as batch_handle: + for currLine in batch_handle: + frame_numbers.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) + elif mode == 'timeline': + run_timeline(params) + elif mode == 'batch_timeline': + run_batch_timeline(params) + else: + raise ValueError("Run mode is unrecognized - abandoning run") + + diff --git a/fractalcache.py b/fractalcache.py index 8769d17..92a8f44 100644 --- a/fractalcache.py +++ b/fractalcache.py @@ -12,118 +12,182 @@ import struct import pickle -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)) +class FrameInfo: + 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.raw_values = raw_values + self.raw_histogram = raw_histogram + self.smooth_values = smooth_values + self.smooth_histogram = smooth_histogram + + 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)) + + 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 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 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) + return len(self.raw_values) != 0 and len(self.raw_histogram) != 0 + + 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 + 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_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, project_folder_name, shared_cache_path, algorithm_name, dive_mesh, frame_info, frame_number=-1): + self.cache_version = CACHE_VER + + 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 + + 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.empty_copy()) + 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.shared_cache_path + else: + 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.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 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)) + #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: + raise ValueError("Aborting cache file write of missing data to \"%s\"" % filename) with open(filename, 'wb') as fd: - pickle.dump(frame,fd) + pickle.dump(self.frame_info.pickle_copy(),fd) - def read_cache(self): - - if not self.check_cache(): - return None, None + def read_results_cache(self): + filename = self.create_results_file_name() + if not os.path.exists(filename) or not os.path.isfile(filename): + return - filename = self.subdir + "/" + self.create_file_name() + #print("+ frame from cache file %s ... "%(filename)) + #print("Reading: %s" % str(self.frame_info)) - print("+ frame from cache file %s ... "%(filename)) - frame = None + frame_data = None with open(filename, 'rb') as fd: - frame = pickle.load(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 +# 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 = frame_data + + 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) + # 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 + 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 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 + + 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) + + mesh_filename = self.create_mesh_file_name() + with open(mesh_filename, 'wb') as mesh_handle: + pickle.dump(self.dive_mesh, mesh_handle) - 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 + return filename - 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 - return frame.values,frame.histogram diff --git a/fractalmath.py b/fractalmath.py new file mode 100644 index 0000000..e75472a --- /dev/null +++ b/fractalmath.py @@ -0,0 +1,1465 @@ +# -- +# File: fractalmath.py +# +# Home for math operations that should be high-precision aware +# +# -- + +import math + +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 + +#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) +#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 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 + 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 __init__(self): + self.precisionType = 'native' + + 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 self.digitsToBits(self.decimal.getcontext().prec) + + def setDigitsPrecision(self, newDigits): + oldPrecision = self.decimal.getcontext().prec + self.decimal.getcontext().prec = newDigits + return oldPrecision + + def digitsPrecision(self): + 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() + """ + 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: + 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 self.decimal.Decimal(floatValue) + + def stringFromFloat(self, paramFloat): + return str(paramFloat) + + def shorterStringFromFloat(self, paramFloat, places): + 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, + 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 + """ + return startValue * (overallZoomFactor ** iterations) + + def createLinspace(self, paramFirst, paramLast, quantity): + """ + 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) + + # 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 / oneLessQuantity) + + 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 + """ + #print(f"createLinspaceAroundValuesCenter {valuesCenter}, {spreadWidth}, {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={}): + """ + 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? + """ + 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': + 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) + 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): + """ + 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 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 = abs(endX - startX) + scaleFactor = (endY / startY) ** (1 / root) + 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): + """ + 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 julia(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.juliaSingle(realValues[y,x], imagValues[y,x], realJuliaValue, imagJuliaValue, escapeRadius, maxIter) + + return (results, lastRealValues, lastImagValues) + + 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 + + # 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. + + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) + + zReal = self.decimal.Decimal(realJuliaValue) + zImag = self.decimal.Decimal(imagJuliaValue) + zrSquared = zReal * zReal + ziSquared = zImag * zImag + + for currIter in range(maxIter + 1): + result = currIter + + if (zrSquared + ziSquared) > radSquared: + break + + # 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 + + return (result, zReal, zImag) + + def mandelbrot(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.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 + """ + return self.juliaSingle(realValue, imagValue, 0, 0, escapeRadius, maxIter) + + 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 + + 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 self.decimal.Decimal(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) + 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], smoothResults[y,x], lastRealValues[y,x], lastImagValues[y,x]) = self.mandelbrotDistanceEstimateSingle(realValues[y,x], imagValues[y,x], escapeRadius, maxIter) + + return (results, smoothResults, 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. + """ + radSquared = escapeRadius * escapeRadius + result = 0 + + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) + + 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): + result = currIter + + if (zrSquared + ziSquared) > radSquared: + break + + # 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 + # 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 + + + # 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, 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, 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) + 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) + # 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) + + 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 + + realDecimal = self.decimal.Decimal(realValue) + imagDecimal = self.decimal.Decimal(imagValue) + + 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) +# +# 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 squared_modulus(self, z): +# return ((z.real*z.real)+(z.imag*z.imag)) + +class DiveMathSupportMPFR(DiveMathSupport): + def __init__(self): + super().__init__() + + # Only imports if you instantiate this DiveMathSupport subclass. + self.mpfrlib = __import__('mpfr_fractalmath') + + # 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' + + # 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 setPrecision(self, newPrecision): + super().setPrecision(newPrecision) # Also set Decimal's precision + oldPrecision = self.currPrecision + self.currPrecision = newPrecision + return oldPrecision + + def precision(self): + return self.currPrecision + + def setDigitsPrecision(self, newDigits): + super().setDigitsPrecision(newDigits) + newPrecision = self.digitsToBits(newDigits) + oldPrecision = self.currPrecision + self.currPrecision = newPrecision + return oldPrecision + + 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 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): + """ + 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.defaultPrecisionSize = FLINT_HIGH_PRECISION_SIZE + self.flint.ctx.prec = self.defaultPrecisionSize # Sets flint's precision (in bits) + self.precisionType = 'flint' + + def setPrecision(self, newPrecision): + #print(f"Setting precision: {newPrecision}") + oldPrecision = self.flint.ctx.prec + 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 + """ + 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] + + preparedReal = '0.0' + preparedImag = '0.0' + + #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): + 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 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]) + + preparedReal = "" + preparedImag = "" + + 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) + + 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() + + 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): +# """ +# """ +# 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, + 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) + #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): + """ + 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 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) + 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): + """ + 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 + 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? + (In retrospect, the extra call isn't worth tabulating at higher + iteration counts) + """ + z = self.flint.acb(0,0) + n = 0 + + for currIter in range(maxIter + 1): + n = currIter + + if float(z.abs_lower()) > escapeRadius: + 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) + + #print("input: %s" % (str(c))) + #print("answer: %s, lastZ: %s" % (str(n), str(z))) + 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. + 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) + 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 - 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() + + 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?! + + # 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 + z = self.flint.acb(0,0) + 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 + + 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() + + dzMag = dz.abs_lower() + if n == maxIter or dzMag == 0.0: + return (n, 0.0) + else: + 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) + +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))) + #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_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): + 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 """ + #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 """ + #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): + """ 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) + diff --git a/fractalpalette.py b/fractalpalette.py index 939500b..19e9593 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,57 @@ 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: + 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: + 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 +94,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 +111,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 +130,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 +139,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 +148,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 +157,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 +170,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 +189,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 +201,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 +235,55 @@ 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 + +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..ccc3dea --- /dev/null +++ b/julia_smooth.py @@ -0,0 +1,120 @@ +# -- +# 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): + 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 + 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(float(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/julia_solo.py b/julia_solo.py new file mode 100644 index 0000000..44607f3 --- /dev/null +++ b/julia_solo.py @@ -0,0 +1,83 @@ +# -- +# 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) + + (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) + 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.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])))) + 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() + diff --git a/mandelbrot.py b/mandelbrot.py deleted file mode 100644 index 15ff2d5..0000000 --- a/mandelbrot.py +++ /dev/null @@ -1,665 +0,0 @@ -# -- -# File: mandelbrot.py -# -# Driver file for playing around with the Mandelbrot 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 getopt -import sys -import math - -from collections import defaultdict - -import numpy as np -import mpmath as mp -import moviepy.editor as mpy - -from scipy.stats import norm - -from moviepy.audio.tools.cuts import find_audio_period - -from PIL import Image, ImageDraw, ImageFont - -# -- our files -import fractalcache as fc -import fractalpalette as fp - -MANDL_VER = "0.1" - - -class MandlContext: - """ - The context for a single dive - """ - - def __init__(self, ctxf = None, ctxc = None, mp = None): - - self.ver = MANDL_VER # used to version cash - - if not ctxf: - self.ctxf = float - else: - self.ctxf = ctxf - if not ctxc: - self.ctxc = complex - else: - self.ctxc = ctxc - if not mp: - self.mp = math - - 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 - - # point we're going to dive into - self.cmplx_center = complex(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.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.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.julia_c = None - self.julia_orig = None - self.julia_walk_c = None - - self.julia_list = None - - self.palette = None - self.burn_in = False - - 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.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)) <= squared_escape 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.log2(abs(z))) - return mu - else: - return n - - 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.)) - - 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 - # 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 = {} - - if snapshot_filename: - print("Generating image [", end="") - - # -- - # 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): - # map from pixels to complex coordinates - Re_x = self.ctxf(re_start) + (self.ctxf(x) / self.ctxf(self.img_width)) * \ - self.ctxf(re_end - re_start) - Im_y = self.ctxf(im_start) + (self.ctxf(y) / self.ctxf(self.img_height)) * \ - self.ctxf(im_end - im_start) - - c = self.ctxc(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 - - if snapshot_filename: - print(".",end="") - sys.stdout.flush() - - if snapshot_filename: - print("]") - - return values, hist - - def draw_image_PIL(self, values, hues): - - 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 = 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)) - - 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) - - 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) - 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 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:.20f} -Cmplx H:{ch:.20f} Complx Center:{cc:s} Scaling:{s:f} Smoothing:{sm:b} Epochs:{e:d} Max iter:{mx:d}]\ -""".format( - w=self.img_width,h=self.img_height,cw=self.cmplx_width,ch=self.cmplx_height, - cc=str(self.cmplx_center),s=self.scaling_factor,e=self.num_epochs,mx=self.max_iter,sm=self.smoothing); - -class MediaView: - """ - Handle displaying to gif / mp4 / screen etc. - """ - - def make_frame(self, t): - return self.ctx.next_epoch(t) - - def __init__(self, duration, fps, ctx): - self.duration = duration - self.fps = fps - self.ctx = ctx - self.banner = False - self.vfilename = None - - self.ctx.duration = duration - self.ctx.fps = 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) - - if self.ctx.cache: - self.ctx.cache.setup() - - - - 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.clip = mpy.VideoClip(self.make_frame, duration=self.duration) - - if self.banner: - self.clip = self.intro_banner() - - if not self.vfilename: - self.clip.preview(fps=1) #fps 1 is really all that works - elif self.vfilename.endswith(".gif"): - 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 __repr__(self): - return """\ -[MediaView duration {du:f} FPS:{f:d} Output:{vf:s}]\ -""".format(du=self.duration,f=self.fps,vf=str(self.vfilename)) - -# For now, use global context for a single dive per run - -mandl_ctx = MandlContext() -view_ctx = MediaView(16, 16, mandl_ctx) - - -# -- -# Default settings for the dive. All of these can be overridden from the -# command line -# -- -def set_default_params(): - global mandl_ctx - - mandl_ctx.img_width = 1024 - mandl_ctx.img_height = 768 - - mandl_ctx.cmplx_width = mandl_ctx.ctxf(5.0) - mandl_ctx.cmplx_height = mandl_ctx.ctxf(3.5) - - # 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) - - mandl_ctx.scaling_factor = .97 - mandl_ctx.num_epochs = 0 - - mandl_ctx.max_iter = 255 - mandl_ctx.escape_rad = 32768. - - mandl_ctx.precision = 100 - - view_ctx.duration = 16 - view_ctx.fps = 16 - - -def set_preview_mode(): - global mandl_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.scaling_factor = .75 - mandl_ctx.escape_rad = 32768. - - view_ctx.duration = 4 - view_ctx.fps = 4 - -def set_snapshot_mode(): - global mandl_ctx - - print("+ Running in snapshot mode ") - - mandl_ctx.snapshot = True - - 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.scaling_factor = .99 # set so we can zoom in more accurately - mandl_ctx.escape_rad = 32768. - - view_ctx.duration = 0 - view_ctx.fps = 0 - - -def parse_options(): - global mandl_ctx - - argv = sys.argv[1:] - - - opts, args = getopt.getopt(argv, "pd:m:s:f:z:w:h:c:", - ["preview", - "duration=", - "max-iter=", - "img-w=", - "img-h=", - "cmplx-w=", - "cmplx-h=", - "center=", - "scaling-factor=", - "snapshot=", - "zoom=", - "fps=", - "gif=", - "mpeg=", - "verbose=", - "julia=", - "julia-walk=", - "center=", - "palette-test=", - "color=", - "burn", - "banner", - "cache", - "smooth"]) - - for opt,arg in opts: - if opt in ['-p', '--preview']: - set_preview_mode() - if opt in ['-s', '--snapshot']: - set_snapshot_mode() - - for opt, arg in opts: - if opt in ['-d', '--duration']: - view_ctx.duration = float(arg) - mandl_ctx.duration = float(arg) - elif opt in ['-m', '--max-iter']: - mandl_ctx.max_iter = int(arg) - elif opt in ['-w', '--img-w']: - mandl_ctx.img_width = int(arg) - elif opt in ['-h', '--img-h']: - mandl_ctx.img_height = int(arg) - elif opt in ['--cmplx-w']: - mandl_ctx.cmplx_width = float(arg) - elif opt in ['--cmplx-h']: - mandl_ctx.cmplx_height = float(arg) - elif opt in ['-c', '--center']: - mandl_ctx.cmplx_center= complex(arg) - elif opt in ['-h', '--img-h']: - mandl_ctx.img_height = int(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: - 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] - elif opt in ['--center']: - mandl_ctx.cmplx_center = complex(arg) - elif opt in ['--palette-test']: - m = fp.MandlPalette() - if str(arg) == "gauss": - m.create_gauss_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp": - m.create_exp_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp2": - m.create_exp2_gradient((0,0,0),(128,128,128)) - elif str(arg) == "list": - m.create_gradient_from_list() - else: - print("Error: --palette-test arg must be one of gauss|exp|list") - sys.exit(0) - m.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)) - elif str(arg) == "exp": - m.create_exp_gradient((255,255,255),(0,0,0)) - elif str(arg) == "exp2": - m.create_exp2_gradient((0,0,0),(128,128,128)) - elif str(arg) == "list": - m.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 - 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) - mandl_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 - - -if __name__ == "__main__": - - print("++ mandlebort.py version %s" % (MANDL_VER)) - - set_default_params() - parse_options() - - view_ctx.setup() - view_ctx.run() diff --git a/mandelbrot_smooth.py b/mandelbrot_smooth.py new file mode 100644 index 0000000..bc9e248 --- /dev/null +++ b/mandelbrot_smooth.py @@ -0,0 +1,65 @@ +# -- +# 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 + +from fractalpalette import FractalPaletteWithSchemes + +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' + + # 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}") + 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) + super().generate_image() + diff --git a/mandelbrot_solo.py b/mandelbrot_solo.py new file mode 100644 index 0000000..06125c6 --- /dev/null +++ b/mandelbrot_solo.py @@ -0,0 +1,100 @@ +# -- +# 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 os +import pickle + +import math +import numpy as np + +from collections import defaultdict + +from PIL import Image, ImageDraw + +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) + + self.algorithm_name = 'mandelbrot_solo' + + 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") + + (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: + 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.counts_array.shape[0]): + #print(f"first col {self.counts_array[y,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])))) + 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(float(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() + + diff --git a/mandeldistance.py b/mandeldistance.py new file mode 100644 index 0000000..d376087 --- /dev/null +++ b/mandeldistance.py @@ -0,0 +1,53 @@ +# -- +# File: mandeldistance.py +# +# +# -- + +import os +import pickle + +import numpy as np + +from PIL import Image, ImageDraw # Should be removable if palette performed the lookup + +from mandelbrot_solo import MandelbrotSolo + +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' + self.smooth_counts_array = None + self.escape_radius = 100000 # Because, who knows. + + 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) + (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) + +# 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 new file mode 100644 index 0000000..79f5d88 --- /dev/null +++ b/markers_to_timeline.py @@ -0,0 +1,196 @@ +# -- +# 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. + +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 new file mode 100644 index 0000000..a5cae4a --- /dev/null +++ b/mesh_explore.py @@ -0,0 +1,576 @@ +# -- +# File: mesh_explore.py +# +# Viewer to explore results generated by fractal.py. +# 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-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. +# 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 = {} + 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=", + # Special read-from-file inovcation + "load-marker-number=", + # Explicit extra params, should probably + # get/set these like in fractal.py + "julia-center=", + ]) + + # 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 + if opt in ['--load-marker-number']: + params['load_marker_number'] = 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') + print(f"opening \"{paramFileName}\"") + 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, + '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, + } + + # 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) + + return params + +def nextClicked(event): + global params + 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 + sameTypeZoomFactor = params['math_support'].createFloat(params['zoom_factor']) + params['real_width'] = params['real_width'] / sameTypeZoomFactor + params['imag_width'] = params['imag_width'] / sameTypeZoomFactor + + updateView() + +def updateView(): + global params + global uiElements + + clickedLocus = uiElements.get('lastClickedLocus', None) + if clickedLocus == None: + return + + runFractalCallForCenter(clickedLocus) + + frameNumber = params['next_frame_number'] + + imageFileTitle = "%d.tiff" % frameNumber + imageFileName = os.path.join(params['wholeCacheFolder'], imageFileTitle) + imageData = mpl.image.imread(imageFileName) + + clickableImage = uiElements['clickableImage'] + clickableImage.set_data(imageData) + + meshFileName = getMeshFileNameForFrameNumber(frameNumber) + with open(meshFileName, 'rb') as meshHandle: + diveMesh = pickle.load(meshHandle) + + uiElements['diveMesh'] = diveMesh + + updateTitle() + plt.draw() + +def runFractalCallForCenter(center): + global params + + # 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}'" + + # 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("") + +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) + 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) + + 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 + 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 + params['zoom_factor'] = round(params['zoom_factor'] - .01, 2) + if params['zoom_factor'] < .01: + params['zoom_factor'] = .01 + updateAdvanceText() + plt.draw() + +def updateAdvanceText(): + global uiElements + global params + screenText = uiElements['advanceText'] + screenText.set_text(str(params['zoom_factor'])) + +def updateTitle(): + global uiElements + diveMesh = uiElements['diveMesh'] + + widthString = diveMesh.mathSupport.shorterStringFromFloat(diveMesh.realMeshGenerator.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 + +def getMeshFileNameForFrameNumber(frameNumber): + global params + + meshFileTitle = "%d.mesh.pik" % frameNumber + return os.path.join(params['wholeCacheFolder'], meshFileTitle) + +def getMarkerFileNameForFrameNumber(frameNumber): + global params + + meshFileTitle = "%d.marker.pik" % frameNumber + return os.path.join(params['project_name'], params['project_params']['exploration_markers_path'], meshFileTitle) + +if __name__ == '__main__': + + global params + params = parse_options() + + 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. + runFractalCallForCenter(center) + + global uiElements + uiElements = {} + + # Start off with the parameterized locus as the center. + uiElements['lastClickedLocus'] = center + + imageFileTitle = "%d.tiff" % frameNumber + meshFileName = getMeshFileNameForFrameNumber(frameNumber) + + 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) + + 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]) + 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]) + 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 + + screenText1 = plt.text(positionX + .8, positionY, "(00)", horizontalalignment='left') + uiElements['precisionText'] = screenText1 + + 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 + + button10Axes = plt.axes([positionX,positionY,smallerButtonWidth,buttonHeight]) + button = mpl.widgets.Button(button10Axes, label="-") + button.on_clicked(minusClicked) + uiElements['minusButton'] = button + + positionX += smallerButtonWidth + gutter + + screenText3 = plt.text(positionX + .1, positionY, "(scale)", horizontalalignment='left') + uiElements['advanceText'] = screenText3 + + updatePrecisionText() + updateIterationsText() + updateAdvanceText() + + updateTitle() + + plt.show() + diff --git a/mpfr_fractal_lib.c b/mpfr_fractal_lib.c new file mode 100644 index 0000000..b11c286 --- /dev/null +++ b/mpfr_fractal_lib.c @@ -0,0 +1,541 @@ + +#include +#include +#include +#include +#include +#include +#include + +#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); +} + +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); +} + +// 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/ + // 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); + + // 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); + + //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; + } + + // 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). + // + // 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); + + //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); + 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); +} + diff --git a/mpfr_fractal_lib.h b/mpfr_fractal_lib.h new file mode 100644 index 0000000..2a97f10 --- /dev/null +++ b/mpfr_fractal_lib.h @@ -0,0 +1,19 @@ + +#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); + +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_fractalmath.pyx b/mpfr_fractalmath.pyx new file mode 100644 index 0000000..caa6de4 --- /dev/null +++ b/mpfr_fractalmath.pyx @@ -0,0 +1,426 @@ + +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) + + 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 + 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. + 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(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_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? + + #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) + +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) 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..bb26219 --- /dev/null +++ b/setup_mpfr.py @@ -0,0 +1,47 @@ + +""" +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() +mpfr_include_dirs.append(numpy.get_include()) +#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, define_macros=[('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')]) + ] + +setup( + ext_modules = cythonize(extensions, language_level="3") + ) diff --git a/smooth.py b/smooth.py new file mode 100644 index 0000000..3e37d88 --- /dev/null +++ b/smooth.py @@ -0,0 +1,126 @@ +# -- +# 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 + +from algo import Algo, 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) + self.color = extra_params.get('color', (.1,.2,.3)) + + 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) + + #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) + + 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): + # 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]) + #print("will print color: (%d,%d,%d)" % (color[0], color[1], color[2])) + + # 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, val): + 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/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_divemesh.py b/test_divemesh.py new file mode 100644 index 0000000..e50b08e --- /dev/null +++ b/test_divemesh.py @@ -0,0 +1,143 @@ + +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_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.7693831791955' + 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.7693831791955' + 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.7693831791955' + centerHeightString = '0.0042368479187' + + 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) + + meshWidth = 320 + meshHeight = 240 + + diveMesh = DiveMesh(meshWidth, meshHeight, 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)) + + #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.7693831791955' + centerHeightString = '0.0042368479187' + + 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) + + meshWidth = 320 + meshHeight = 240 + + diveMesh = DiveMesh(meshWidth, meshHeight, 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)) + + #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 new file mode 100644 index 0000000..60a8119 --- /dev/null +++ b/test_fractalmath.py @@ -0,0 +1,287 @@ +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_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 + 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_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. + radius = 2.0 + + maxIterations = 100 + placesToMatch = 8 + + # 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') + (iterations, lastZee) = self.mathSupport.mandelbrot(center, radius, maxIterations) + #print(str(iterations)) + #print(str(lastZee)) + self.assertEqual(100, iterations) + 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') + (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) + + + # 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(-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 + 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 + + 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() + # 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 + + def test_differentMandelbrots(self): + """ + This checks results between different custom 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()) + + 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 = 8 + radius = 2.0 + + maxIterations = 100 + + + 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(center, radius, maxIterations) + #print(str(iterations)) + #print("lastZee as string: %s" % str(lastZee)) + self.assertEqual(100, iterations) + 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') + (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) + + 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(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') + (iterations, lastZee) = self.mathSupport.mandelbrot_beginning(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) + + #print("Done comparing") + +if __name__ == '__main__': + unittest.main() + diff --git a/time_tinker/edit/timelines/._time_tinker_demo b/time_tinker/edit/timelines/._time_tinker_demo new file mode 100644 index 0000000..3b0db62 Binary files /dev/null and b/time_tinker/edit/timelines/._time_tinker_demo differ diff --git a/time_tinker/edit/timelines/time_tinker_demo b/time_tinker/edit/timelines/time_tinker_demo new file mode 100644 index 0000000..de6b1bb --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo @@ -0,0 +1,983 @@ +{ + "algorithmName": "mandelbrot_smooth", + "framerate": 23.976, + "frameWidth": 1024, + "frameHeight": 768, + "mathSupport": "DiveMathSupportFlintCustom:196", + "timelineSpans": [ + { + "time": "0", + "duration": "75033", + "keyframes": [ + [ + "0", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "0", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "0", + "meshImagWidth", + "float", + "3.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + [ + "0", + "meshRealWidth", + "float", + "4.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + +# 9000 waypoint end of start, begin of decel + [ + "9000", + "max_escape_iterations", + "int", + "1232", + "linear", + "linear" + ], + [ + "9000", + "meshImagWidth", + "float", + "[1.387704739643e-8 +/- 6.19e-21]", + "root-to", + "root-to-ease-in" + ], + [ + "9000", + "meshRealWidth", + "float", + "[1.850272986191e-8 +/- 3.92e-21]", + "root-to", + "root-to-ease-in" + ], + + +# 12119 First halt, backwards begins + [ + "12119", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "12119", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "12119", + "meshImagWidth", + "float", + "[5.07810357813e-10 +/- 5.41e-22]", + "root-to-ease-in", + "root-from-ease-out" + ], + [ + "12119", + "meshRealWidth", + "float", + "[6.77080477085e-10 +/- 5.15e-22]", + "root-to-ease-in", + "root-from-ease-out" + ], + + +# Second stop, zoomed out 15355, accel/decel back towards dive +# Center point copied in from higher precision keyframe later. + [ + "15355", + "max_escape_iterations", + "int", + "1232", + "linear", + "linear" + ], + [ + "15355", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "15355", + "meshImagWidth", + "float", + "[1.387704739643e-8 +/- 6.19e-21]", + "root-from-ease-out", + "root-to-ease-out" + ], + [ + "15355", + "meshRealWidth", + "float", + "[1.850272986191e-8 +/- 3.92e-21]", + "root-from-ease-out", + "root-to-ease-out" + ], + + +# Waypoint, back to full speed + [ + "18492", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "18492", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "linear", + "linear" + ], + [ + "18492", + "meshImagWidth", + "float", + "[4.68350349630e-10 +/- 4.65e-22]", + "root-to-ease-out", + "root-to" + ], + [ + "18492", + "meshRealWidth", + "float", + "[6.24467132839e-10 +/- 5.96e-22]", + "root-to-ease-out", + "root-to" + ], + +# Just setting values, so we can modify them in another span layer?! +# Not exactly ideal, but also not outright crazy. + [ + "21895", + "meshRealTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "21895", + "meshImagTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "26600", + "meshRealTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "26600", + "meshImagTilt", + "float", + "0.0", + "linear", + "linear" + ], + + + +# Fast snap up. +# 34708 + [ + "34708", + "meshRealTilt", + "float", + "0.0", + "step", + "quadratic-to" + ], + + [ + "34833", + "meshRealTilt", + "float", + "2.0", + "quadratic-to", + "linear" + ], + + + +# Fast snap down. +# 37744 + [ + "37744", + "meshRealTilt", + "float", + "2.0", + "linear", + "quadratic-to-from" + ], + [ + "37994", + "meshRealTilt", + "float", + "-2.0", + "quadratic-to-from", + "linear" + ], + + +# Settle back to flat over 2 bars + [ + "40981", + "meshRealTilt", + "float", + "-2.0", + "linear", + "quadratic-to" + ], + + [ + "44117", + "meshRealTilt", + "float", + "0.0", + "quadratic-to", + "step" + ], + + + +# Waypoint, about to stop + [ + "47454", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "47454", + "meshImagWidth", + "float", + "[4.59834888727e-36 +/- 3.62e-48]", + "root-to", + "root-to-ease-in" + ], + [ + "47454", + "meshRealWidth", + "float", + "[6.131131849697e-36 +/- 8.88e-49]", + "root-to", + "root-to-ease-in" + ], + + +#(top mid) +# "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + #[0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", +# +#(bottom right) +# "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + #[0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", +# +#(bottom left) +# "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + #[0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", +# +# +#(center) +# "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + #[0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + + +# Full stop. +# Now, centered on the top-of-3 triangle points as the next segment's dive center + [ + "50524", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "50524", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "linear", + "quadratic-to-from" + ], + [ + "50524", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to-ease-in", + "root-to" + ], + [ + "50524", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to-ease-in", + "root-to" + ], + + +# bottom right + [ + "51491", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# bottom left + [ + "52259", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# top middle + [ + "53060", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# bottom right + [ + "53827", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# bottom left + [ + "54217", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# top middle + [ + "54667", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], +# bottom right + [ + "55087", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# bottom left + [ + "55507", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + +# center + [ + "55507", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + +# Triangle mid point +# future center copied in from later keyframe + [ + "55829", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "55829", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "quadratic-to-from", + "linear" + ], + [ + "55829", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "55829", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "55829", + "palette_scheme_index", + "int", + "0", + "step", + "step" + ], + + +# Waypoint, before breathe-in + [ + "59600", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "59600", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-from-ease-in" + ], + +# Breathe-in frame (this is the furthest-out point, before re-accelling in) + [ + "59733", + "meshImagWidth", + "float", + "[2.418404214230e-38 +/- 9.31e-51]", + "root-from-ease-in", + "root-to" + ], + [ + "59733", + "meshRealWidth", + "float", + "[3.224538952307e-38 +/- 7.38e-51]", + "root-from-ease-in", + "root-to" + ], + [ + "59733", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ], + + + +# Full speed to end + [ + "59866", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "59866", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "59866", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + + + [ + "65806", + "palette_scheme_index", + "int", + "2", + "step", + "step" + ], + [ + "66239", + "palette_scheme_index", + "int", + "0", + "step", + "step" + ], + [ + "66676", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ], + + +# End point + + [ + "74991", + "max_escape_iterations", + "int", + "5376", + "linear", + "linear" + ], + [ + "74991", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "linear", + "linear" + ], + [ + "74991", + "meshImagWidth", + "float", + "[2.5332336614901e-54 +/- 6.70e-68]", + "root-to", + "root-to" + ], + [ + "74991", + "meshRealWidth", + "float", + "[3.3776448819868e-54 +/- 8.79e-68]", + "root-to", + "root-to" + ], + [ + "74991", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ] + ] + }, + { + "time": "21895", + "duration": "4705", + "keyframes": [ + +# Spiral distort of tilt angles. Should be pretty simple to calculate, and +# sure is a pain to specify manually. + + [ + "21895", + "meshRealTilt_modifyPlus", + "float", + "0.0", + "linear", + "quadratic-to-from" + ], + [ + "21895", + "meshImagTilt_modifyPlus", + "float", + "0.0", + "linear", + "quadratic-to-from" + ], + + +# First cycle at 24798, so each 1/4 rotation is ~ (2903/4) = ~725 + +# Get-going takes an 1/8th rotation up, half-the way + [ + "22257", + "meshRealTilt_modifyPlus", + "float", + "1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# First 1/4 rotation at ~21895+725= 22620 + [ + "22620", + "meshImagTilt_modifyPlus", + "float", + "2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# 2nd 1/4 rotation at ~21895+(2*725)= 23345 + [ + "23345", + "meshRealTilt_modifyPlus", + "float", + "-2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# 3rd 1/4 rotation at ~21895+(3*725)= 24070 + [ + "24070", + "meshImagTilt_modifyPlus", + "float", + "-2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# 4th 1/4 rotation at ~21895+(4*725)= 24795 + [ + "24795", + "meshRealTilt_modifyPlus", + "float", + "2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# 5th 1/4 rotation at ~21895+(5*725)= 25520 + [ + "25520", + "meshImagTilt_modifyPlus", + "float", + "1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# 6th 1/4 rotation at ~21895+(6*725)= 25520 + [ + "25520", + "meshRealTilt_modifyPlus", + "float", + "-1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + +# Let's add another 1/8th rotation stop as overshoot before settling. +# ~21895+(6.25*725)= 26426 + [ + "26426", + "meshImagTilt_modifyPlus", + "float", + "-0.5", + "quadratic-to-from", + "quadratic-to-from" + ], + +# Back to centered at end + [ + "26600", + "meshRealTilt_modifyPlus", + "float", + "0.0", + "quadratic-to-from", + "linear" + ], + [ + "26600", + "meshImagTilt_modifyPlus", + "float", + "0.0", + "quadratic-to-from", + "linear" + ] + ] + }, + { + "time": "60667", + "duration": "5813", + "keyframes": [ + +# All these escape iteration definitions are modifications timed with the music. +# Seems like the extra midpoint is excessive here, but it's not the worst +# thing to have ramp-up and ramp-down control, so maybe it's fine. +# +# Using +160ms as the timing for the peak. + [ + "60667", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "61007", + "max_escape_iterations_modifyPlus", + "int", + "-400", + "linear", + "quadratic-to" + ], + [ + "61330", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "62335", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "62495", + "max_escape_iterations_modifyPlus", + "int", + "-400", + "linear", + "quadratic-to" + ], + [ + "62702", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "63503", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "63663", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "63870", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + + [ + "66538", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "66698", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "65705", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "66673", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "66833", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "67040", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "68041", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "68201", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "69041", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "69910", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "70070", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "71077", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "71078", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "72245", + "max_escape_iterations_modifyPlus", + "int", + "-600", + "linear", + "step" + ], + + [ + "73013", + "max_escape_iterations_modifyPlus", + "int", + "-250", + "step", + "step" + ], + + [ + "73480", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "step" + ] + ] + } + + ] +} diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean b/time_tinker/edit/timelines/time_tinker_demo_clean new file mode 100644 index 0000000..1fcd20e --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean @@ -0,0 +1,923 @@ +{ + "algorithmName": "mandelbrot_smooth", + "framerate": 23.976, + "frameWidth": 1024, + "frameHeight": 768, + "mathSupport": "DiveMathSupportFlintCustom:196", + "timelineSpans": [ + { + "time": "0", + "duration": "75033", + "keyframes": [ + [ + "0", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "0", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "0", + "meshImagWidth", + "float", + "3.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + [ + "0", + "meshRealWidth", + "float", + "4.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + + [ + "9000", + "max_escape_iterations", + "int", + "1232", + "linear", + "linear" + ], + [ + "9000", + "meshImagWidth", + "float", + "[1.387704739643e-8 +/- 6.19e-21]", + "root-to", + "root-to-ease-in" + ], + [ + "9000", + "meshRealWidth", + "float", + "[1.850272986191e-8 +/- 3.92e-21]", + "root-to", + "root-to-ease-in" + ], + + + [ + "12119", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "12119", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "12119", + "meshImagWidth", + "float", + "[5.07810357813e-10 +/- 5.41e-22]", + "root-to-ease-in", + "root-from-ease-out" + ], + [ + "12119", + "meshRealWidth", + "float", + "[6.77080477085e-10 +/- 5.15e-22]", + "root-to-ease-in", + "root-from-ease-out" + ], + + + [ + "15355", + "max_escape_iterations", + "int", + "1232", + "linear", + "linear" + ], + [ + "15355", + "meshCenter", + "complex", + "[-0.912581720937148 +/- 3.12e-16] + [0.254713063224225 +/- 2.79e-16]j", + "linear", + "linear" + ], + [ + "15355", + "meshImagWidth", + "float", + "[1.387704739643e-8 +/- 6.19e-21]", + "root-from-ease-out", + "root-to-ease-out" + ], + [ + "15355", + "meshRealWidth", + "float", + "[1.850272986191e-8 +/- 3.92e-21]", + "root-from-ease-out", + "root-to-ease-out" + ], + + + [ + "18492", + "max_escape_iterations", + "int", + "1234", + "linear", + "linear" + ], + [ + "18492", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "linear", + "linear" + ], + [ + "18492", + "meshImagWidth", + "float", + "[4.68350349630e-10 +/- 4.65e-22]", + "root-to-ease-out", + "root-to" + ], + [ + "18492", + "meshRealWidth", + "float", + "[6.24467132839e-10 +/- 5.96e-22]", + "root-to-ease-out", + "root-to" + ], + + [ + "21895", + "meshRealTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "21895", + "meshImagTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "26600", + "meshRealTilt", + "float", + "0.0", + "linear", + "linear" + ], + [ + "26600", + "meshImagTilt", + "float", + "0.0", + "linear", + "linear" + ], + + + + [ + "34708", + "meshRealTilt", + "float", + "0.0", + "step", + "quadratic-to" + ], + + [ + "34833", + "meshRealTilt", + "float", + "2.0", + "quadratic-to", + "linear" + ], + + + + [ + "37744", + "meshRealTilt", + "float", + "2.0", + "linear", + "quadratic-to-from" + ], + [ + "37994", + "meshRealTilt", + "float", + "-2.0", + "quadratic-to-from", + "linear" + ], + + + [ + "40981", + "meshRealTilt", + "float", + "-2.0", + "linear", + "quadratic-to" + ], + + [ + "44117", + "meshRealTilt", + "float", + "0.0", + "quadratic-to", + "step" + ], + + + + [ + "47454", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "47454", + "meshImagWidth", + "float", + "[4.59834888727e-36 +/- 3.62e-48]", + "root-to", + "root-to-ease-in" + ], + [ + "47454", + "meshRealWidth", + "float", + "[6.131131849697e-36 +/- 8.88e-49]", + "root-to", + "root-to-ease-in" + ], + + + + + [ + "50524", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "50524", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "linear", + "quadratic-to-from" + ], + [ + "50524", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to-ease-in", + "root-to" + ], + [ + "50524", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to-ease-in", + "root-to" + ], + + + [ + "51491", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "52259", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "53060", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "53827", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "54217", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "54667", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + [ + "55087", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "55507", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "55507", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "quadratic-to-from", + "quadratic-to-from" + ], + + + [ + "55829", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "55829", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "quadratic-to-from", + "linear" + ], + [ + "55829", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "55829", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "55829", + "palette_scheme_index", + "int", + "0", + "step", + "step" + ], + + + [ + "59600", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-from-ease-in" + ], + [ + "59600", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-from-ease-in" + ], + + [ + "59733", + "meshImagWidth", + "float", + "[2.418404214230e-38 +/- 9.31e-51]", + "root-from-ease-in", + "root-to" + ], + [ + "59733", + "meshRealWidth", + "float", + "[3.224538952307e-38 +/- 7.38e-51]", + "root-from-ease-in", + "root-to" + ], + [ + "59733", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ], + + + + [ + "59866", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "59866", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "59866", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + + + [ + "65806", + "palette_scheme_index", + "int", + "2", + "step", + "step" + ], + [ + "66239", + "palette_scheme_index", + "int", + "0", + "step", + "step" + ], + [ + "66676", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ], + + + + [ + "74991", + "max_escape_iterations", + "int", + "5376", + "linear", + "linear" + ], + [ + "74991", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "linear", + "linear" + ], + [ + "74991", + "meshImagWidth", + "float", + "[2.5332336614901e-54 +/- 6.70e-68]", + "root-to", + "root-to" + ], + [ + "74991", + "meshRealWidth", + "float", + "[3.3776448819868e-54 +/- 8.79e-68]", + "root-to", + "root-to" + ], + [ + "74991", + "palette_scheme_index", + "int", + "1", + "step", + "step" + ] + ] + }, + { + "time": "21895", + "duration": "4705", + "keyframes": [ + + + [ + "21895", + "meshRealTilt_modifyPlus", + "float", + "0.0", + "linear", + "quadratic-to-from" + ], + [ + "21895", + "meshImagTilt_modifyPlus", + "float", + "0.0", + "linear", + "quadratic-to-from" + ], + + + + [ + "22257", + "meshRealTilt_modifyPlus", + "float", + "1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "22620", + "meshImagTilt_modifyPlus", + "float", + "2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "23345", + "meshRealTilt_modifyPlus", + "float", + "-2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "24070", + "meshImagTilt_modifyPlus", + "float", + "-2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "24795", + "meshRealTilt_modifyPlus", + "float", + "2.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "25520", + "meshImagTilt_modifyPlus", + "float", + "1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "25520", + "meshRealTilt_modifyPlus", + "float", + "-1.0", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "26426", + "meshImagTilt_modifyPlus", + "float", + "-0.5", + "quadratic-to-from", + "quadratic-to-from" + ], + + [ + "26600", + "meshRealTilt_modifyPlus", + "float", + "0.0", + "quadratic-to-from", + "linear" + ], + [ + "26600", + "meshImagTilt_modifyPlus", + "float", + "0.0", + "quadratic-to-from", + "linear" + ] + ] + }, + { + "time": "60667", + "duration": "5813", + "keyframes": [ + + [ + "60667", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "61007", + "max_escape_iterations_modifyPlus", + "int", + "-400", + "linear", + "quadratic-to" + ], + [ + "61330", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "62335", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "62495", + "max_escape_iterations_modifyPlus", + "int", + "-400", + "linear", + "quadratic-to" + ], + [ + "62702", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "63503", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "63663", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "63870", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + + [ + "66538", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "66698", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "65705", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "66673", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "66833", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "67040", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "68041", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "68201", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "69041", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "69910", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "70070", + "max_escape_iterations_modifyPlus", + "int", + "-500", + "linear", + "quadratic-to" + ], + [ + "71077", + "max_escape_iterations_modifyPlus", + "int", + "0", + "quadratic-to", + "step" + ], + + [ + "71078", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "linear" + ], + [ + "72245", + "max_escape_iterations_modifyPlus", + "int", + "-600", + "linear", + "step" + ], + + [ + "73013", + "max_escape_iterations_modifyPlus", + "int", + "-250", + "step", + "step" + ], + + [ + "73480", + "max_escape_iterations_modifyPlus", + "int", + "0", + "step", + "step" + ] + ] + } + + ] +} diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_0.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_0.txt new file mode 100644 index 0000000..d38c2ff --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_0.txt @@ -0,0 +1,180 @@ +0 +10 +20 +30 +40 +50 +60 +70 +80 +90 +100 +110 +120 +130 +140 +150 +160 +170 +180 +190 +200 +210 +220 +230 +240 +250 +260 +270 +280 +290 +300 +310 +320 +330 +340 +350 +360 +370 +380 +390 +400 +410 +420 +430 +440 +450 +460 +470 +480 +490 +500 +510 +520 +530 +540 +550 +560 +570 +580 +590 +600 +610 +620 +630 +640 +650 +660 +670 +680 +690 +700 +710 +720 +730 +740 +750 +760 +770 +780 +790 +800 +810 +820 +830 +840 +850 +860 +870 +880 +890 +900 +910 +920 +930 +940 +950 +960 +970 +980 +990 +1000 +1010 +1020 +1030 +1040 +1050 +1060 +1070 +1080 +1090 +1100 +1110 +1120 +1130 +1140 +1150 +1160 +1170 +1180 +1190 +1200 +1210 +1220 +1230 +1240 +1250 +1260 +1270 +1280 +1290 +1300 +1310 +1320 +1330 +1340 +1350 +1360 +1370 +1380 +1390 +1400 +1410 +1420 +1430 +1440 +1450 +1460 +1470 +1480 +1490 +1500 +1510 +1520 +1530 +1540 +1550 +1560 +1570 +1580 +1590 +1600 +1610 +1620 +1630 +1640 +1650 +1660 +1670 +1680 +1690 +1700 +1710 +1720 +1730 +1740 +1750 +1760 +1770 +1780 +1790 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_1.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_1.txt new file mode 100644 index 0000000..40e9544 --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_1.txt @@ -0,0 +1,180 @@ +1 +11 +21 +31 +41 +51 +61 +71 +81 +91 +101 +111 +121 +131 +141 +151 +161 +171 +181 +191 +201 +211 +221 +231 +241 +251 +261 +271 +281 +291 +301 +311 +321 +331 +341 +351 +361 +371 +381 +391 +401 +411 +421 +431 +441 +451 +461 +471 +481 +491 +501 +511 +521 +531 +541 +551 +561 +571 +581 +591 +601 +611 +621 +631 +641 +651 +661 +671 +681 +691 +701 +711 +721 +731 +741 +751 +761 +771 +781 +791 +801 +811 +821 +831 +841 +851 +861 +871 +881 +891 +901 +911 +921 +931 +941 +951 +961 +971 +981 +991 +1001 +1011 +1021 +1031 +1041 +1051 +1061 +1071 +1081 +1091 +1101 +1111 +1121 +1131 +1141 +1151 +1161 +1171 +1181 +1191 +1201 +1211 +1221 +1231 +1241 +1251 +1261 +1271 +1281 +1291 +1301 +1311 +1321 +1331 +1341 +1351 +1361 +1371 +1381 +1391 +1401 +1411 +1421 +1431 +1441 +1451 +1461 +1471 +1481 +1491 +1501 +1511 +1521 +1531 +1541 +1551 +1561 +1571 +1581 +1591 +1601 +1611 +1621 +1631 +1641 +1651 +1661 +1671 +1681 +1691 +1701 +1711 +1721 +1731 +1741 +1751 +1761 +1771 +1781 +1791 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_2.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_2.txt new file mode 100644 index 0000000..6eee4fb --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_2.txt @@ -0,0 +1,180 @@ +2 +12 +22 +32 +42 +52 +62 +72 +82 +92 +102 +112 +122 +132 +142 +152 +162 +172 +182 +192 +202 +212 +222 +232 +242 +252 +262 +272 +282 +292 +302 +312 +322 +332 +342 +352 +362 +372 +382 +392 +402 +412 +422 +432 +442 +452 +462 +472 +482 +492 +502 +512 +522 +532 +542 +552 +562 +572 +582 +592 +602 +612 +622 +632 +642 +652 +662 +672 +682 +692 +702 +712 +722 +732 +742 +752 +762 +772 +782 +792 +802 +812 +822 +832 +842 +852 +862 +872 +882 +892 +902 +912 +922 +932 +942 +952 +962 +972 +982 +992 +1002 +1012 +1022 +1032 +1042 +1052 +1062 +1072 +1082 +1092 +1102 +1112 +1122 +1132 +1142 +1152 +1162 +1172 +1182 +1192 +1202 +1212 +1222 +1232 +1242 +1252 +1262 +1272 +1282 +1292 +1302 +1312 +1322 +1332 +1342 +1352 +1362 +1372 +1382 +1392 +1402 +1412 +1422 +1432 +1442 +1452 +1462 +1472 +1482 +1492 +1502 +1512 +1522 +1532 +1542 +1552 +1562 +1572 +1582 +1592 +1602 +1612 +1622 +1632 +1642 +1652 +1662 +1672 +1682 +1692 +1702 +1712 +1722 +1732 +1742 +1752 +1762 +1772 +1782 +1792 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_3.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_3.txt new file mode 100644 index 0000000..825368b --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_3.txt @@ -0,0 +1,180 @@ +3 +13 +23 +33 +43 +53 +63 +73 +83 +93 +103 +113 +123 +133 +143 +153 +163 +173 +183 +193 +203 +213 +223 +233 +243 +253 +263 +273 +283 +293 +303 +313 +323 +333 +343 +353 +363 +373 +383 +393 +403 +413 +423 +433 +443 +453 +463 +473 +483 +493 +503 +513 +523 +533 +543 +553 +563 +573 +583 +593 +603 +613 +623 +633 +643 +653 +663 +673 +683 +693 +703 +713 +723 +733 +743 +753 +763 +773 +783 +793 +803 +813 +823 +833 +843 +853 +863 +873 +883 +893 +903 +913 +923 +933 +943 +953 +963 +973 +983 +993 +1003 +1013 +1023 +1033 +1043 +1053 +1063 +1073 +1083 +1093 +1103 +1113 +1123 +1133 +1143 +1153 +1163 +1173 +1183 +1193 +1203 +1213 +1223 +1233 +1243 +1253 +1263 +1273 +1283 +1293 +1303 +1313 +1323 +1333 +1343 +1353 +1363 +1373 +1383 +1393 +1403 +1413 +1423 +1433 +1443 +1453 +1463 +1473 +1483 +1493 +1503 +1513 +1523 +1533 +1543 +1553 +1563 +1573 +1583 +1593 +1603 +1613 +1623 +1633 +1643 +1653 +1663 +1673 +1683 +1693 +1703 +1713 +1723 +1733 +1743 +1753 +1763 +1773 +1783 +1793 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_4.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_4.txt new file mode 100644 index 0000000..ded456f --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_4.txt @@ -0,0 +1,180 @@ +4 +14 +24 +34 +44 +54 +64 +74 +84 +94 +104 +114 +124 +134 +144 +154 +164 +174 +184 +194 +204 +214 +224 +234 +244 +254 +264 +274 +284 +294 +304 +314 +324 +334 +344 +354 +364 +374 +384 +394 +404 +414 +424 +434 +444 +454 +464 +474 +484 +494 +504 +514 +524 +534 +544 +554 +564 +574 +584 +594 +604 +614 +624 +634 +644 +654 +664 +674 +684 +694 +704 +714 +724 +734 +744 +754 +764 +774 +784 +794 +804 +814 +824 +834 +844 +854 +864 +874 +884 +894 +904 +914 +924 +934 +944 +954 +964 +974 +984 +994 +1004 +1014 +1024 +1034 +1044 +1054 +1064 +1074 +1084 +1094 +1104 +1114 +1124 +1134 +1144 +1154 +1164 +1174 +1184 +1194 +1204 +1214 +1224 +1234 +1244 +1254 +1264 +1274 +1284 +1294 +1304 +1314 +1324 +1334 +1344 +1354 +1364 +1374 +1384 +1394 +1404 +1414 +1424 +1434 +1444 +1454 +1464 +1474 +1484 +1494 +1504 +1514 +1524 +1534 +1544 +1554 +1564 +1574 +1584 +1594 +1604 +1614 +1624 +1634 +1644 +1654 +1664 +1674 +1684 +1694 +1704 +1714 +1724 +1734 +1744 +1754 +1764 +1774 +1784 +1794 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_5.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_5.txt new file mode 100644 index 0000000..cacd6fd --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_5.txt @@ -0,0 +1,180 @@ +5 +15 +25 +35 +45 +55 +65 +75 +85 +95 +105 +115 +125 +135 +145 +155 +165 +175 +185 +195 +205 +215 +225 +235 +245 +255 +265 +275 +285 +295 +305 +315 +325 +335 +345 +355 +365 +375 +385 +395 +405 +415 +425 +435 +445 +455 +465 +475 +485 +495 +505 +515 +525 +535 +545 +555 +565 +575 +585 +595 +605 +615 +625 +635 +645 +655 +665 +675 +685 +695 +705 +715 +725 +735 +745 +755 +765 +775 +785 +795 +805 +815 +825 +835 +845 +855 +865 +875 +885 +895 +905 +915 +925 +935 +945 +955 +965 +975 +985 +995 +1005 +1015 +1025 +1035 +1045 +1055 +1065 +1075 +1085 +1095 +1105 +1115 +1125 +1135 +1145 +1155 +1165 +1175 +1185 +1195 +1205 +1215 +1225 +1235 +1245 +1255 +1265 +1275 +1285 +1295 +1305 +1315 +1325 +1335 +1345 +1355 +1365 +1375 +1385 +1395 +1405 +1415 +1425 +1435 +1445 +1455 +1465 +1475 +1485 +1495 +1505 +1515 +1525 +1535 +1545 +1555 +1565 +1575 +1585 +1595 +1605 +1615 +1625 +1635 +1645 +1655 +1665 +1675 +1685 +1695 +1705 +1715 +1725 +1735 +1745 +1755 +1765 +1775 +1785 +1795 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_6.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_6.txt new file mode 100644 index 0000000..2b39d96 --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_6.txt @@ -0,0 +1,180 @@ +6 +16 +26 +36 +46 +56 +66 +76 +86 +96 +106 +116 +126 +136 +146 +156 +166 +176 +186 +196 +206 +216 +226 +236 +246 +256 +266 +276 +286 +296 +306 +316 +326 +336 +346 +356 +366 +376 +386 +396 +406 +416 +426 +436 +446 +456 +466 +476 +486 +496 +506 +516 +526 +536 +546 +556 +566 +576 +586 +596 +606 +616 +626 +636 +646 +656 +666 +676 +686 +696 +706 +716 +726 +736 +746 +756 +766 +776 +786 +796 +806 +816 +826 +836 +846 +856 +866 +876 +886 +896 +906 +916 +926 +936 +946 +956 +966 +976 +986 +996 +1006 +1016 +1026 +1036 +1046 +1056 +1066 +1076 +1086 +1096 +1106 +1116 +1126 +1136 +1146 +1156 +1166 +1176 +1186 +1196 +1206 +1216 +1226 +1236 +1246 +1256 +1266 +1276 +1286 +1296 +1306 +1316 +1326 +1336 +1346 +1356 +1366 +1376 +1386 +1396 +1406 +1416 +1426 +1436 +1446 +1456 +1466 +1476 +1486 +1496 +1506 +1516 +1526 +1536 +1546 +1556 +1566 +1576 +1586 +1596 +1606 +1616 +1626 +1636 +1646 +1656 +1666 +1676 +1686 +1696 +1706 +1716 +1726 +1736 +1746 +1756 +1766 +1776 +1786 +1796 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_7.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_7.txt new file mode 100644 index 0000000..fe20557 --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_7.txt @@ -0,0 +1,180 @@ +7 +17 +27 +37 +47 +57 +67 +77 +87 +97 +107 +117 +127 +137 +147 +157 +167 +177 +187 +197 +207 +217 +227 +237 +247 +257 +267 +277 +287 +297 +307 +317 +327 +337 +347 +357 +367 +377 +387 +397 +407 +417 +427 +437 +447 +457 +467 +477 +487 +497 +507 +517 +527 +537 +547 +557 +567 +577 +587 +597 +607 +617 +627 +637 +647 +657 +667 +677 +687 +697 +707 +717 +727 +737 +747 +757 +767 +777 +787 +797 +807 +817 +827 +837 +847 +857 +867 +877 +887 +897 +907 +917 +927 +937 +947 +957 +967 +977 +987 +997 +1007 +1017 +1027 +1037 +1047 +1057 +1067 +1077 +1087 +1097 +1107 +1117 +1127 +1137 +1147 +1157 +1167 +1177 +1187 +1197 +1207 +1217 +1227 +1237 +1247 +1257 +1267 +1277 +1287 +1297 +1307 +1317 +1327 +1337 +1347 +1357 +1367 +1377 +1387 +1397 +1407 +1417 +1427 +1437 +1447 +1457 +1467 +1477 +1487 +1497 +1507 +1517 +1527 +1537 +1547 +1557 +1567 +1577 +1587 +1597 +1607 +1617 +1627 +1637 +1647 +1657 +1667 +1677 +1687 +1697 +1707 +1717 +1727 +1737 +1747 +1757 +1767 +1777 +1787 +1797 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_8.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_8.txt new file mode 100644 index 0000000..e569966 --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_8.txt @@ -0,0 +1,180 @@ +8 +18 +28 +38 +48 +58 +68 +78 +88 +98 +108 +118 +128 +138 +148 +158 +168 +178 +188 +198 +208 +218 +228 +238 +248 +258 +268 +278 +288 +298 +308 +318 +328 +338 +348 +358 +368 +378 +388 +398 +408 +418 +428 +438 +448 +458 +468 +478 +488 +498 +508 +518 +528 +538 +548 +558 +568 +578 +588 +598 +608 +618 +628 +638 +648 +658 +668 +678 +688 +698 +708 +718 +728 +738 +748 +758 +768 +778 +788 +798 +808 +818 +828 +838 +848 +858 +868 +878 +888 +898 +908 +918 +928 +938 +948 +958 +968 +978 +988 +998 +1008 +1018 +1028 +1038 +1048 +1058 +1068 +1078 +1088 +1098 +1108 +1118 +1128 +1138 +1148 +1158 +1168 +1178 +1188 +1198 +1208 +1218 +1228 +1238 +1248 +1258 +1268 +1278 +1288 +1298 +1308 +1318 +1328 +1338 +1348 +1358 +1368 +1378 +1388 +1398 +1408 +1418 +1428 +1438 +1448 +1458 +1468 +1478 +1488 +1498 +1508 +1518 +1528 +1538 +1548 +1558 +1568 +1578 +1588 +1598 +1608 +1618 +1628 +1638 +1648 +1658 +1668 +1678 +1688 +1698 +1708 +1718 +1728 +1738 +1748 +1758 +1768 +1778 +1788 +1798 diff --git a/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_9.txt b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_9.txt new file mode 100644 index 0000000..e32e325 --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_demo_clean_batches/batch_9.txt @@ -0,0 +1,179 @@ +9 +19 +29 +39 +49 +59 +69 +79 +89 +99 +109 +119 +129 +139 +149 +159 +169 +179 +189 +199 +209 +219 +229 +239 +249 +259 +269 +279 +289 +299 +309 +319 +329 +339 +349 +359 +369 +379 +389 +399 +409 +419 +429 +439 +449 +459 +469 +479 +489 +499 +509 +519 +529 +539 +549 +559 +569 +579 +589 +599 +609 +619 +629 +639 +649 +659 +669 +679 +689 +699 +709 +719 +729 +739 +749 +759 +769 +779 +789 +799 +809 +819 +829 +839 +849 +859 +869 +879 +889 +899 +909 +919 +929 +939 +949 +959 +969 +979 +989 +999 +1009 +1019 +1029 +1039 +1049 +1059 +1069 +1079 +1089 +1099 +1109 +1119 +1129 +1139 +1149 +1159 +1169 +1179 +1189 +1199 +1209 +1219 +1229 +1239 +1249 +1259 +1269 +1279 +1289 +1299 +1309 +1319 +1329 +1339 +1349 +1359 +1369 +1379 +1389 +1399 +1409 +1419 +1429 +1439 +1449 +1459 +1469 +1479 +1489 +1499 +1509 +1519 +1529 +1539 +1549 +1559 +1569 +1579 +1589 +1599 +1609 +1619 +1629 +1639 +1649 +1659 +1669 +1679 +1689 +1699 +1709 +1719 +1729 +1739 +1749 +1759 +1769 +1779 +1789 diff --git a/time_tinker/edit/timelines/time_tinker_sequence_01 b/time_tinker/edit/timelines/time_tinker_sequence_01 new file mode 100644 index 0000000..fb7f91c --- /dev/null +++ b/time_tinker/edit/timelines/time_tinker_sequence_01 @@ -0,0 +1,303 @@ +{ + "algorithmName": "mandelbrot_smooth", + "framerate": 23.976, + "frameWidth": 1024, + "frameHeight": 768, + "mathSupport": "DiveMathSupportFlintCustom:196", + "timelineSpans": [ + { + "time": "0", + "duration": "75033", + "keyframes": [ + [ + "0", + "max_escape_iterations", + "int", + "512", + "linear", + "linear" + ], + [ + "0", + "meshCenter", + "complex", + "[-0.912581720138687 +/- 8.31e-16] + [0.254713057701388 +/- 5.83e-16]j", + "linear", + "linear" + ], + [ + "0", + "meshImagWidth", + "float", + "3.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + [ + "0", + "meshRealWidth", + "float", + "4.000000000000000000000000000000000000000000000000000000000", + "root-to", + "root-to" + ], + [ + "9373", + "max_escape_iterations", + "int", + "1232", + "linear", + "linear" + ], + [ + "9373", + "meshCenter", + "complex", + "[-0.912581720138687 +/- 7.11e-16] + [0.254713057701388 +/- 4.95e-16]j", + "linear", + "linear" + ], + [ + "9373", + "meshImagWidth", + "float", + "[5.07810357813e-10 +/- 5.41e-22]", + "root-to", + "root-to" + ], + [ + "9373", + "meshRealWidth", + "float", + "[6.77080477085e-10 +/- 5.15e-22]", + "root-to", + "root-to" + ], + [ + "18747", + "max_escape_iterations", + "int", + "624", + "linear", + "linear" + ], + [ + "18747", + "meshCenter", + "complex", + "[-0.912581723893859 +/- 8.48e-16] + [0.254713061348462 +/- 5.89e-16]j", + "linear", + "linear" + ], + [ + "18747", + "meshImagWidth", + "float", + "[1.387704739643e-8 +/- 6.19e-21]", + "root-to", + "root-to" + ], + [ + "18747", + "meshRealWidth", + "float", + "[1.850272986191e-8 +/- 3.92e-21]", + "root-to", + "root-to" + ], + [ + "28121", + "max_escape_iterations", + "int", + "864", + "linear", + "linear" + ], + [ + "28121", + "meshCenter", + "complex", + "[-0.912581723893859 +/- 8.48e-16] + [0.254713061348462 +/- 5.89e-16]j", + "linear", + "linear" + ], + [ + "28121", + "meshImagWidth", + "float", + "[4.68350349630e-10 +/- 4.65e-22]", + "root-to", + "root-to" + ], + [ + "28121", + "meshRealWidth", + "float", + "[6.24467132839e-10 +/- 5.96e-22]", + "root-to", + "root-to" + ], + [ + "37495", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "37495", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095643 +/- 7.44e-43] + [0.254713061348462420839633952640232889299955 +/- 7.08e-43]j", + "linear", + "linear" + ], + [ + "37495", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "37495", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + [ + "46869", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "46869", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440092378 +/- 6.63e-43] + [0.254713061348462420839633952640232889304777 +/- 2.97e-43]j", + "linear", + "linear" + ], + [ + "46869", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "46869", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + [ + "56243", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "56243", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440098717 +/- 8.10e-43] + [0.254713061348462420839633952640232889304970 +/- 6.69e-43]j", + "linear", + "linear" + ], + [ + "56243", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "56243", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + [ + "65617", + "max_escape_iterations", + "int", + "3408", + "linear", + "linear" + ], + [ + "65617", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452 +/- 7.51e-43] + [0.254713061348462420839633952640232889302462 +/- 5.71e-43]j", + "linear", + "linear" + ], + [ + "65617", + "meshImagWidth", + "float", + "[2.276182699200e-38 +/- 4.41e-51]", + "root-to", + "root-to" + ], + [ + "65617", + "meshRealWidth", + "float", + "[3.034910265600e-38 +/- 4.27e-51]", + "root-to", + "root-to" + ], + [ + "74991", + "max_escape_iterations", + "int", + "5376", + "linear", + "linear" + ], + [ + "74991", + "meshCenter", + "complex", + "[-0.912581723893859340429749910373421440095452138042847910946 +/- 4.84e-58] + [0.254713061348462420839633952640232889302461665571684355096 +/- 3.49e-58]j", + "linear", + "linear" + ], + [ + "74991", + "meshImagWidth", + "float", + "[2.5332336614901e-54 +/- 6.70e-68]", + "root-to", + "root-to" + ], + [ + "74991", + "meshRealWidth", + "float", + "[3.3776448819868e-54 +/- 8.79e-68]", + "root-to", + "root-to" + ] + ] + } + ] +} \ No newline at end of file diff --git a/time_tinker/exploration/markers/1.marker.pik b/time_tinker/exploration/markers/1.marker.pik new file mode 100644 index 0000000..7efee05 Binary files /dev/null and b/time_tinker/exploration/markers/1.marker.pik differ diff --git a/time_tinker/exploration/markers/2.marker.pik b/time_tinker/exploration/markers/2.marker.pik new file mode 100644 index 0000000..8e8c8fb Binary files /dev/null and b/time_tinker/exploration/markers/2.marker.pik differ diff --git a/time_tinker/exploration/markers/3.marker.pik b/time_tinker/exploration/markers/3.marker.pik new file mode 100644 index 0000000..975fadb Binary files /dev/null and b/time_tinker/exploration/markers/3.marker.pik differ diff --git a/time_tinker/exploration/markers/4.marker.pik b/time_tinker/exploration/markers/4.marker.pik new file mode 100644 index 0000000..d452b73 Binary files /dev/null and b/time_tinker/exploration/markers/4.marker.pik differ diff --git a/time_tinker/exploration/markers/5.marker.pik b/time_tinker/exploration/markers/5.marker.pik new file mode 100644 index 0000000..0f06027 Binary files /dev/null and b/time_tinker/exploration/markers/5.marker.pik differ diff --git a/time_tinker/exploration/markers/6.marker.pik b/time_tinker/exploration/markers/6.marker.pik new file mode 100644 index 0000000..4813702 Binary files /dev/null and b/time_tinker/exploration/markers/6.marker.pik differ diff --git a/time_tinker/exploration/markers/7.marker.pik b/time_tinker/exploration/markers/7.marker.pik new file mode 100644 index 0000000..adafe50 Binary files /dev/null and b/time_tinker/exploration/markers/7.marker.pik differ diff --git a/time_tinker/exploration/markers/8.marker.pik b/time_tinker/exploration/markers/8.marker.pik new file mode 100644 index 0000000..f154d43 Binary files /dev/null and b/time_tinker/exploration/markers/8.marker.pik differ diff --git a/time_tinker/exploration/markers/9.marker.pik b/time_tinker/exploration/markers/9.marker.pik new file mode 100644 index 0000000..8bfe03e Binary files /dev/null and b/time_tinker/exploration/markers/9.marker.pik differ 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..34abfa7 --- /dev/null +++ b/time_tinker_explanation.txt @@ -0,0 +1,98 @@ +# 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. + +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 + +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 + + diff --git a/timing_comparison.py b/timing_comparison.py new file mode 100644 index 0000000..07ebcea --- /dev/null +++ b/timing_comparison.py @@ -0,0 +1,123 @@ +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 = 1000 + +#decimalPlaces = 100 +#mandelbrotMaxIter = 7000 +#timerRepeatCount = 1000 + +#decimalPlaces = 500 +#mandelbrotMaxIter = 35000 +#timerRepeatCount = 10 + +decimalPlaces = 1000 +mandelbrotMaxIter = 150000 +timerRepeatCount = 10 + +#decimalPlaces = 1500 +#mandelbrotMaxIter = 290000 +#timerRepeatCount = 10 + +#decimalPlaces = 2200 +#mandelbrotMaxIter = 2700000 +#timerRepeatCount = 3 + + +#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 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 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 + 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 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)) + +if __name__ == '__main__': + main() +