-
Notifications
You must be signed in to change notification settings - Fork 0
/
hopalong_advanced.py
162 lines (127 loc) · 5.62 KB
/
hopalong_advanced.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import numpy as np
from numba import njit
from math import copysign, sqrt, fabs
import time
def get_validated_input(prompt, input_type=float, check_non_zero=False, check_positive=False):
while True:
user_input = input(prompt)
try:
value = input_type(user_input)
if check_non_zero and value == 0:
print("Invalid input. The value cannot be zero.")
continue
if check_positive and value <= 0:
print("Invalid input. The value must be a positive number.")
continue
return value
except ValueError:
print(f"Invalid input. Please enter a valid {input_type.__name__} value.")
def get_attractor_parameters():
a = get_validated_input('Enter a float value for "a": ', float)
b = get_validated_input('Enter a float value for "b": ', float)
c = get_validated_input('Enter a float value for "c": ', float)
num = get_validated_input('Enter a positive integer value for "num": ', int, check_non_zero=True, check_positive=True)
params = {'a': a, 'b': b, 'c': c, 'num': num}
return a, b, c, num, params
@njit
def compute_trajectory_extents(a, b, c, num):
# Compute the x and y extents of the Hopalong attractor trajectory.
x = y = np.float64(0)
min_x = min_y = np.inf
max_x = max_y = -np.inf
for _ in range(num):
min_x = min(min_x, x)
max_x = max(max_x, x)
min_y = min(min_y, y)
max_y = max(max_y, y)
# signum function respecting the behavior of floating point numbers according to IEEE 754 (signed zero)
xx, yy = y - copysign(1.0, x) * sqrt(fabs(b * x - c)), a - x
x, y = xx, yy
return min_x, max_x, min_y, max_y
@njit
def compute_trajectory_and_image(a, b, c, num, extents, image_size):
# Compute the trajectory and populate the image with trajectory points
img_width, img_height = image_size
image = np.zeros((img_height, img_width), dtype=np.uint64)
# pre-compute imsge scale factors
min_x, max_x, min_y, max_y = extents
scale_x = (img_width - 1) / (max_x - min_x)
scale_y = (img_height - 1) / (max_y - min_y)
x = y = np.float64(0)
for _ in range(num):
# map trajectory points to image pixel coordinates
px = np.uint64((x - min_x) * scale_x)
py = np.uint64((y - min_y) * scale_y)
#populate the image
image[py, px] += 1 # respecting row/column convention
# update the trajectory
xx, yy = y - copysign(1.0, x) * sqrt(fabs(b * x - c)), a - x
x, y = xx, yy
return image
def calculate_hit_metrics(img):
hit, count = np.unique(img[img > 0], return_counts=True)
max_count_index = np.argmax(count)
hit_for_max_count = hit[max_count_index]
max_hit_index = np.argmax(hit)
count_for_max_hit = count[max_hit_index]
hit_pixel = np.sum(count)
img_pixels = np.prod(img.shape)
hit_ratio = '{:.2f}'.format(hit_pixel / img_pixels * 100)
hit_metrics = {
"hit": hit,
"count": count,
"hit_for_max_count": hit_for_max_count,
"count_for_max_hit": count_for_max_hit,
"hit_pixel": hit_pixel,
"img_points": img_pixels,
"hit_ratio": hit_ratio,
}
return hit_metrics
def render_trajectory_image(ax, img, extents, params, color_map):
ax.imshow(img, origin="lower", cmap=color_map, extent=extents)
ax.set_title(
"Hopalong Attractor@ratwolf@2024\nParams: a={a}, b={b}, c={c}, num={num:_}".format(**params))
ax.set_xlabel('X (Cartesian)')
ax.set_ylabel('Y (Cartesian)')
def plot_hit_metrics(ax, hit_metrics, scale='log'):
ax.plot(hit_metrics["hit"], hit_metrics["count"], 'o-', color="navy", markersize=1, linewidth=0.6)
ax.set_xlabel('# of hits (n)')
ax.set_ylabel('# of pixels hit n-times')
title_text = (
f'Distribution of pixel hit count. \n'
f'{hit_metrics["hit_pixel"]} pixels out of {hit_metrics["img_points"]} image pixels = {hit_metrics["hit_ratio"]}% have been hit. \n'
f'The highest number of pixels with the same number of hits is {np.max(hit_metrics["count"])} with {hit_metrics["hit_for_max_count"]} hits. \n'
f'The highest number of hits is {np.max(hit_metrics["hit"])} with {hit_metrics["count_for_max_hit"]} pixels hit')
ax.set_title(title_text, fontsize=10)
ax.set_xscale(scale)
ax.set_yscale(scale)
ax.set_xlim(left=0.9)
ax.set_ylim(bottom=0.9)
ax.set_facecolor("lightgrey")
ax.grid(True, which="both")
def visualize_trajectory_image_and_hit_metrics(img, extents, params, color_map, hit_metrics):
fig = plt.figure(figsize=(18, 8))
ax1 = fig.add_subplot(1, 2, 1, aspect='auto')
render_trajectory_image(ax1, img, extents, params, color_map)
ax2 = fig.add_subplot(1, 2, 2, aspect='auto')
plot_hit_metrics(ax2, hit_metrics)
plt.show()
def main(image_size=(1000, 1000), color_map='hot'):
# Main execution process
try:
a, b, c, num, params = get_attractor_parameters()
start_time = time.time() # Start the timer
extents = compute_trajectory_extents(a, b, c, num)
img = compute_trajectory_and_image(a, b, c, num, extents, image_size)
hit_metrics = calculate_hit_metrics(img)
end_time = time.time() # End the timer
print(f"Execution time: {end_time - start_time} seconds") # Print the execution time
visualize_trajectory_image_and_hit_metrics(img, extents, params, color_map, hit_metrics)
except Exception as e:
print(f"An error occurred: {e}")
# Main execution
if __name__ == "__main__":
main()