-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathattractor.py
246 lines (184 loc) · 7.9 KB
/
attractor.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
from argparse import ArgumentParser, ArgumentTypeError
from PIL import Image
import numpy as np
import ctypes
import time
MAX_ATTRACTORS = 1 # number of attractors to search for
T_SEARCH = 2000 # number of iterations to perform during search
T_RENDER = int(10e6) # number of iterations to perform during render
T_IDX = int(0.01 * T_RENDER) # first index after transient
MODE = "Cubic"
""" import external C helper functions """
dll = ctypes.cdll.LoadLibrary("./helper.so")
c_iterator = dll.iterator
c_sum_alpha = dll.sum_alpha
def iterator(x, y, z, coeff, repeat, radius=0):
""" compute an array of positions visited by recurrence relation """
c_iterator.restype = ctypes.POINTER(ctypes.c_double * (3 * repeat))
start = to_double_ctype(np.array([x, y, z]))
coeff = to_double_ctype(coeff)
out = to_double_ctype(np.zeros(3 * repeat))
res = c_iterator(start, coeff, repeat, ctypes.c_double(radius), out).contents
return np.array(res).reshape((repeat, 3)).T
def sum_alpha(yres, xres, Is, Js, rx, ry, rz):
""" compute the sum of zalpha values at each pixel """
c_sum_alpha.restype = ctypes.POINTER(ctypes.c_double * (yres * xres * 3))
size = len(Is)
out = to_double_ctype(np.zeros(yres * xres * 3))
Is, Js = to_int_ctype(Is), to_int_ctype(Js)
rx = to_double_ctype(rx)
ry = to_double_ctype(ry)
rz = to_double_ctype(rz)
res = c_sum_alpha(yres, xres, size, Is, Js, rx, ry, rz, out).contents
return np.array(res).reshape((yres, xres, 3))
def to_double_ctype(arr):
""" convert arr to a ctype array of doubles """
arr_type = ctypes.POINTER(ctypes.c_double * len(arr))
return arr.astype(np.float64).ctypes.data_as(arr_type)
def to_int_ctype(arr):
""" convert arr to a ctype array of ints """
arr_type = ctypes.POINTER(ctypes.c_int32 * len(arr))
return arr.astype(np.int32).ctypes.data_as(arr_type)
def coeff_to_string(coeff):
"""convert coefficients to alphabetical values (see Sprott)"""
att_string = "".join([chr(int((c + 7.7)*10)) for c in coeff])
return att_string
def pixel_density(xdata, ydata, coeff, xres=320, yres=180):
""" check for density of points in image """
xmin, ymin, xrng, yrng = set_aspect(xdata, ydata, xres, yres)
render = np.zeros((yres, xres))
try:
for x, y in zip(xdata, ydata):
J = get_index(x, xmin, xrng, xres)
I = get_index(y, ymin, yrng, yres)
render[I, J] += 1
except ValueError:
print("Invalid value (pixel density)")
return False
return check_density(render, coeff)
def check_density(render, coeff, min_fill=1.5):
""" check if pixel density exceeds threshold """
filled_pixels = np.count_nonzero(render)
fill_percentage = 100 * filled_pixels/np.size(render)
if fill_percentage > min_fill:
print(coeff_to_string(coeff))
print("Non-zero points: {} ({:.2f}%)".format(filled_pixels, fill_percentage))
print("")
return True
return False
def set_aspect(xdata, ydata, width, height, debug=False, margin=1.1):
""" get boundaries for given aspect ratio w/h """
xmin, xrng = get_minmax_rng(xdata)
ymin, yrng = get_minmax_rng(ydata)
if debug:
print("Data range | X: {:.2f} | Y: {:.2f} | Intrinsic aspect ratio: {:.2f}".format(xrng, yrng, xrng/yrng))
xmid = xmin + xrng/2
ymid = ymin + yrng/2
if xrng/yrng < width/height:
xrng = width/height * yrng
else:
yrng = height/width * xrng
xrng *= margin
yrng *= margin
xmin = xmid - xrng/2.
ymin = ymid - yrng/2
if debug:
print("Rescaled data range | X: {:.2f} | Y: {:.2f} | New aspect ratio: {:.2f}".format(xrng, yrng, xrng/yrng))
return xmin, ymin, xrng, yrng
def get_minmax_rng(data):
max_val = data.max()
min_val = data.min()
data_range = max_val - min_val
return min_val, data_range
def get_index(x, xmin, xrng, xres):
""" map coordinate to array index """
return int((x-xmin) * (xres-1) / xrng)
def get_dx(xdata):
dx = abs(xdata - np.roll(xdata, 1))[1:]
mdx = np.amax(dx)
return dx, mdx
def zalpha(z, zmin, zrng, a_min=0):
""" return alpha based on z depth """
alpha = a_min + (1-a_min)*(z-zmin)/zrng
return alpha
def save_image(xdata, ydata, zdata, coeff, plane, alpha=0.025, xres=3200, yres=1800):
start = time.time()
xmin, ymin, xrng, yrng = set_aspect(xdata, ydata, xres, yres, debug=True)
zmin, zrng = get_minmax_rng(zdata)
dxs, mdx = get_dx(xdata)
dys, mdy = get_dx(ydata)
dzs, mdz = get_dx(zdata)
print("Calculating pixel values")
xscaled = (xdata[1:]-xmin) * (xres-1) / xrng
yscaled = (ydata[1:]-ymin) * (yres-1) / yrng
clip = np.logical_and(xscaled < xres, yscaled < yres)
xscaled = xscaled.astype(int)[clip]
yscaled = yscaled.astype(int)[clip]
a_min = 0.25
zscaled = (zdata[1:]-zmin) * (1-a_min) / zrng + a_min
xpix = (1-dxs/mdx)*alpha*zscaled[clip]
ypix = (1-dys/mdy)*alpha*zscaled[clip]
zpix = (1-dzs/mdz)*alpha*zscaled[clip]
render = sum_alpha(yres, xres, yscaled, xscaled, xpix, ypix, zpix)
render = np.clip(render, None, 1)
fname = "{}-{}K-{}.png".format(coeff_to_string(coeff), T_RENDER//1000, plane)
Image.fromarray((render * 255).astype(np.uint8)).save(fname, compress_level=1)
end = time.time()
print("Saved " + fname)
print("{:.2f} sec".format(end-start))
def coeff_from_str(word):
"""convert alphabetical values to coefficients"""
return np.array([(ord(c)-ord("A")-12)/10 for c in word.upper()])
def search_attractors(max_attractors):
print("Searching for attractors | Mode: {}".format(MODE))
att_coeffs = []
n_attractors = 0
while n_attractors < max_attractors:
# pick random coefficients in the range (-1.2,1.2)
if MODE == "Cubic":
coeff = np.random.randint(-12, 13, 60)/10
n_coeff = 20
else:
raise ValueError("Only 'Cubic' mode is currently supported")
x, y, z = 0, 0, 0
xl, yl, zl = iterator(x, y, z, coeff, T_SEARCH, 10)
if zl[-1] <= 10:
if pixel_density(xl, yl, coeff):
att_coeffs.append(coeff)
n_attractors += 1
print("")
return att_coeffs
def plot_attractors(att_coeffs):
for i, coeff in enumerate(att_coeffs, 1):
print("\nAttractor: {} | {}/{}".format(coeff_to_string(coeff), i, len(att_coeffs)))
print("Iterating {} steps".format(T_RENDER))
start = time.time()
x, y, z = 0, 0, 0
xl, yl, zl = iterator(x, y, z, coeff, T_IDX)
x, y, z = xl[-1], yl[-1], zl[-1]
if np.isnan(x+y+z):
print("Error during calculation")
continue
xl, yl, zl = iterator(x, y, z, coeff, T_RENDER - T_IDX)
end = time.time()
print("Finished iteration: {:.1f} sec | {} iterations per second".format((end-start), T_RENDER/(end-start)))
save_image(xl[T_IDX:], yl[T_IDX:], zl[T_IDX:], coeff, plane="xy")
save_image(xl[T_IDX:], zl[T_IDX:], yl[T_IDX:], coeff, plane="xz")
save_image(yl[T_IDX:], zl[T_IDX:], xl[T_IDX:], coeff, plane="yz")
def seed_check(seed):
symbols_valid = all(ord("A") <= ord(c) <= ord("Y") for c in seed.upper())
if symbols_valid and len(seed) == 60:
return seed
raise ArgumentTypeError("Seed must contain exactly 60 characters in range A-Y inclusive")
def main():
parser = ArgumentParser(description="Plots strange attractors")
parser.add_argument("--seed", dest="seed", action="store", nargs=1, type=seed_check,
help="an alphabetical seed representing the coefficients of the attractor")
args = parser.parse_args()
if args.seed:
att_coeffs = [coeff_from_str(args.seed[0])]
else:
att_coeffs = search_attractors(MAX_ATTRACTORS)
plot_attractors(att_coeffs)
if __name__ == "__main__":
main()