From 911d4d83010a3c051f6092c26e223ac12c165e84 Mon Sep 17 00:00:00 2001 From: Braden Best Date: Tue, 3 May 2022 01:30:40 -0600 Subject: [PATCH] Refactored the code and improved the output format chiefly, I removed a lot of duplicate code and improved the output format to print out a table (here's a [screenshot](https://imgur.com/DT4enHV)), which allowed for much more compact and easy-to-scan output. This was only done for the data printouts, so the "personality" is preserved. The refactored code is heavily stateful, like the original. The state is now encapsulated within a class, which while not ideal is better than having it scattered throughout the function, and several inefficient constructs (like that "yes" variable) have been simplified. --- JigsawInferenceGizmo.py | 252 ++++++++++++++-------------------------- 1 file changed, 88 insertions(+), 164 deletions(-) diff --git a/JigsawInferenceGizmo.py b/JigsawInferenceGizmo.py index 64f825d..2a1ad15 100644 --- a/JigsawInferenceGizmo.py +++ b/JigsawInferenceGizmo.py @@ -1,191 +1,115 @@ # JIG code from Stand-up Maths video "Why don't Jigsaw Puzzles have the correct number of pieces?" - -def low_factors(n): - # all the factors which are the lower half of each factor pair - lf = [] - for i in range(1, int(n**0.5)+1): - if n % i == 0: - lf.append(i) - return lf - - -def jig(w,h,n,b=0): - +# Usage: +# from jig import * +# jig(PhysicalWidth, PhysicalHeight, NumberOfPieces, MoreVerboseOutput? (default = 1)) +# jig_v0(a, b, c) is the same as jig(a, b, c, verbose = 0) + +class JigClass: + """ + Since the bulk of the code is heavily state reliant, this refactoring + makes use of some basic encapsulation. + """ # percentage we'll check in either direction - threshold = 0.1 - + JIG_THRESHOLD = 0.1 # the extra badness per piece - penalty = 1.005 - - ratio = max(w,h)/min(w,h) # switched to be greater than 1 - - print("") - print(f"{w} by {h} is picture ratio {round(ratio,4)}") - print("") - - max_cap = int((1+threshold)*n) - min_cap = int((1-threshold)*n) - - up_range = [i for i in range(n,max_cap+1)] - down_range = [i for i in range(min_cap,n)] # do not want n included again - down_range.reverse() - - # start at 100 which is silly high and then move down. - up_best = 100 - up_best_deets = [] - down_best = 100 - down_best_deets = [] - - # I am using the run marker so I know if looking above or below n - run = 0 - - for dis_range in [up_range,down_range]: + PIECE_PENALTY = 1.005 + + def __init__(self, w, h, n, verbose): + self.w = w + self.h = h + self.n = n + self.verbose = verbose + self.ratio = max(w, h) / min(w, h) + self.dir_best = [100, 100] + self.dir_best_stats = [None, None] + self.dir_range = ( + range(n, int((1 + self.JIG_THRESHOLD) * n) + 1), + range(n-1, int((1 - self.JIG_THRESHOLD) * n) - 1, -1) + ) + + def search_range(self, direction): best_n = 0 best_n_ratio = 0 best_n_sides = [] - - if run == 0: - print(f"Looking for >= {n} solutions:") - print("") - else: - print("") - print("Just out of interest, here are smaller options:") - print("") - - for i in dis_range: + best_stats_list = [] + + for i in self.dir_range[direction]: this_best = 0 + for j in low_factors(i): - j2 = int(i/j) # must be a whole number anyway + j2 = int(i/j) this_ratio = j2/j - if this_best == 0: - this_best = this_ratio - best_sides = [j,j2] - else: - if abs(this_ratio/ratio - 1) < abs(this_best/ratio - 1): - this_best = this_ratio - best_sides = [j,j2] - yes = 0 - if best_n == 0: - yes = 1 - else: - if abs(this_best/ratio - 1) < abs(best_n_ratio/ratio - 1): - yes = 1 - if yes == 1: + + if this_best == 0 or abs(this_ratio / self.ratio - 1) < abs(this_best / self.ratio - 1): + this_best = this_ratio + best_sides = [j, j2] + + if best_n == 0 or abs(this_best / self.ratio - 1) < abs(best_n_ratio / self.ratio - 1): best_n = i best_n_ratio = this_best best_n_sides = best_sides - piece_ratio = max(ratio,this_best)/min(ratio,this_best) - badness_score = (penalty**(abs(i-n)))*piece_ratio - if run == 0: - if badness_score < up_best: - up_best = badness_score - up_best_deets = [best_n,best_n_sides,best_n_ratio] - else: - if badness_score < down_best: - down_best = badness_score - down_best_deets = [best_n,best_n_sides,best_n_ratio] - print(f"{best_n} pieces in {best_n_sides} (grid ratio {round(best_n_ratio,4)}) needs piece ratio {round(piece_ratio,4)}") - if b==1: - print(f"[badness = {round(badness_score,5)}]") - - - print(f"for {n} the best is {best_n} pieces with size {best_n_sides}") - - run += 1 - print("") - print(f"If I had to guess: I think it's {up_best_deets[0]} pieces.") + piece_ratio = max(self.ratio, this_best) / min(self.ratio, this_best) + badness_score = (self.PIECE_PENALTY ** (abs(i - self.n))) * piece_ratio - if down_best < up_best: - print("") - print(f"BUT, fun fact, {down_best_deets[0]} would be even better.") + if badness_score < self.dir_best[direction]: + self.dir_best[direction] = badness_score + self.dir_best_stats[direction] = (best_n, best_n_sides, best_n_ratio) - print("") - return 'DONE' + best_stats_list.append(( + best_n, + dimstr(best_n_sides), + round(best_n_ratio, 4), + round(piece_ratio, 4), + round(badness_score, 5) + )) -# I duplicated jig_v0 to make is easier to show in the video -def jig_v0(w,h,n,b=0): - - # percentage we'll check in either direction - threshold = 0.1 + # I changed the printout format to be a table and used the + # extra space to include a badness score column + # -Braden + print("pieces size g. ratio p. ratio badness") - penalty = 1.005 + for selstat in best_stats_list: + print("%-6d %-10s %6.4f %6.4f %.5f" % selstat) - ratio = max(w,h)/min(w,h) # switched to be greater than 1 - - print("") - print(f"{w} by {h} is picture ratio {round(ratio,4)}") - print("") - - max_cap = int((1+threshold)*n) - min_cap = int((1-threshold)*n) + if self.verbose: + print(f"for {self.n} the best is {best_n} pieces with size {dimstr(best_n_sides)}") - up_range = [i for i in range(n,max_cap+1)] - down_range = [i for i in range(min_cap,n)] # do not want n included again - down_range.reverse() + def report_pr(self): + print(f"\n{self.w} by {self.h} is picture ratio {round(self.ratio, 4)}\n") - # start at 100 which is silly high and then move down. - up_best = 100 - up_best_deets = [] - down_best = 100 - down_best_deets = [] + def report_end(self): + if not self.verbose: + return - run = 0 + print(f"\nIf I had to guess: I think it's {self.dir_best_stats[0][0]} pieces.") - for dis_range in [up_range,down_range]: - best_n = 0 - best_n_ratio = 0 - best_n_sides = [] + if self.dir_best[1] < self.dir_best[0]: + print(f"\nBUT, fun fact, {self.dir_best_stats[1][0]} would be even better.") - if run == 0: - print(f"Looking for >= {n} solutions:") - print("") - else: - print("") - print("Just out of interest, here are smaller options:") - print("") - - for i in dis_range: - this_best = 0 - for j in low_factors(i): - j2 = int(i/j) # must be a whole number anyway - this_ratio = j2/j - if this_best == 0: - this_best = this_ratio - best_sides = [j,j2] - else: - if abs(this_ratio/ratio - 1) < abs(this_best/ratio - 1): - this_best = this_ratio - best_sides = [j,j2] - yes = 0 - if best_n == 0: - yes = 1 - else: - if abs(this_best/ratio - 1) < abs(best_n_ratio/ratio - 1): - yes = 1 - if yes == 1: - best_n = i - best_n_ratio = this_best - best_n_sides = best_sides - piece_ratio = max(ratio,this_best)/min(ratio,this_best) - badness_score = (penalty**(abs(i-n)))*piece_ratio - if run == 0: - if badness_score < up_best: - up_best = badness_score - up_best_deets = [best_n,best_n_sides,best_n_ratio] - else: - if badness_score < down_best: - down_best = badness_score - down_best_deets = [best_n,best_n_sides,best_n_ratio] - print(f"{best_n} pieces in {best_n_sides} (grid ratio {round(best_n_ratio,4)}) needs piece ratio {round(piece_ratio,4)}") - if b==1: - print(f"[badness = {round(badness_score,5)}]") - - - - run += 1 +def dimstr(dim): + """Turns tuple or list (A, B) into string repr 'AxB'""" + return f"{dim[0]}x{dim[1]}" + +def low_factors(n): + # all the factors which are the lower half of each factor pair + lf = [] + for i in range(1, int(n**0.5)+1): + if n % i == 0: + lf.append(i) + return lf +def jig(w, h, n, verbose = 1): + jc = JigClass(w, h, n, verbose) + jc.report_pr() + print(f"Looking for >= {n} solutions:\n") + jc.search_range(0) + print("\nJust out of interest, here are smaller options:\n") + jc.search_range(1) + jc.report_end() print("") return 'DONE' - +# I duplicated jig_v0 to make is easier to show in the video +def jig_v0(w, h, n): + return jig(w, h, n, 0)