diff --git a/Project.toml b/Project.toml index 448d6da..ab8c239 100644 --- a/Project.toml +++ b/Project.toml @@ -4,11 +4,19 @@ authors = ["jofrevalles "] version = "0.1.0" [deps] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CoherentNoise = "3547b4d8-3bdb-4c10-a099-d01a31e852e0" +ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" +DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" +ExprTools = "e2ba6199-217a-4e67-a87a-7c52f15ade04" FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" -ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +Luxor = "ae8d54c2-7ccd-5906-9d76-62fc9837b5bc" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" diff --git a/README.md b/README.md index 9301871..b631d02 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,12 @@ Here are some examples of textures generated with GeneticTextures.jl: Examples +## Benchmarks +We also benchmarked the performance of the different function backends available in GeneticTextures.jl. +
+ Benchmarks +
+ ## Acknowledgments GeneticTextures.jl makes use of the following third-party packages: diff --git a/combined_performance_analysis_compared_minimum.png b/combined_performance_analysis_compared_minimum.png new file mode 100644 index 0000000..cf09542 Binary files /dev/null and b/combined_performance_analysis_compared_minimum.png differ diff --git a/examples/animation10.jl b/examples/animation10.jl new file mode 100644 index 0000000..77de1dc --- /dev/null +++ b/examples/animation10.jl @@ -0,0 +1,15 @@ +using GeneticTextures +F_A0 = :(Complex(x, y)) +F_dA = :(x_grad(A) + y_grad(A) + 0.1 * laplacian(A)) + + +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> abs(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +# B = VariableDynamics(:B, F_B0, F_dB) +# ds = DynamicalSystem([A,B]) + +ds = DynamicalSystem([A]) +w = h = 64 +animate_system_2(ds, w, h, 100.0, 0.2; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation11.jl b/examples/animation11.jl new file mode 100644 index 0000000..3f91f8c --- /dev/null +++ b/examples/animation11.jl @@ -0,0 +1,15 @@ +using GeneticTextures +F_A0 = :(Complex(x, y)) +F_dA = :((x_grad(A) + y_grad(A)) * A + 0.1 * laplacian(A)) + + +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> abs(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +# B = VariableDynamics(:B, F_B0, F_dB) +# ds = DynamicalSystem([A,B]) + +ds = DynamicalSystem([A]) +w = h = 64 +animate_system_2(ds, w, h, 100.0, 0.5; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation12.jl b/examples/animation12.jl new file mode 100644 index 0000000..0adb4a3 --- /dev/null +++ b/examples/animation12.jl @@ -0,0 +1,16 @@ +using GeneticTextures +F_A0 = :(Complex(x, y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +F_dA = :(A^2 + Complex(0.14, 0.13)) + + +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +# B = VariableDynamics(:B, F_B0, F_dB) +# ds = DynamicalSystem([A,B]) + +ds = DynamicalSystem([A]) +w = h = 512 +animate_system_2(ds, w, h, 200.0, 0.04; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation13.jl b/examples/animation13.jl new file mode 100644 index 0000000..30de47b --- /dev/null +++ b/examples/animation13.jl @@ -0,0 +1,21 @@ +using GeneticTextures +F_A0 = :(y) +F_dA = :(C) + +F_B0 = :(1.0) +F_dB = :(x_grad(C)) + +F_C = :(1 - ifs(rand_scalar() > 0.97, 0.0, 1.0) * y) +F_dC = :(neighbor_ave(grad_direction(B * 0.25))) + + +color_expr = :((a, b, c) -> RGB(abs(a.r), abs(b.g), abs(c.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +w = h = 32 +animate_system_2(ds, w, h, 10.0, 0.01; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation14.jl b/examples/animation14.jl new file mode 100644 index 0000000..f132ff7 --- /dev/null +++ b/examples/animation14.jl @@ -0,0 +1,19 @@ +using GeneticTextures + +F_A0 = :(Complex(4*x, 4*y)) +F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.14, 0.13)) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 0.1, B)) + + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A,B]) + +w = h = 512 +animate_system_2(ds, w, h, 200.0, 0.02; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation15.jl b/examples/animation15.jl new file mode 100644 index 0000000..0a87f8a --- /dev/null +++ b/examples/animation15.jl @@ -0,0 +1,40 @@ +using GeneticTextures +using Colors + +# Define a colormap +colormap_ = colormap("RdBu", 100) # Choose any available colormap + +# Color mapping function +function map_to_color(b, colormap) + idx = min(floor(Int, abs(b) * length(colormap)) + 1, length(colormap)) + return colormap[idx] +end + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +F_da = :(A^2 + Complex(0.74543, 0.11301)) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 0.1, B)) + +color_expr = :(begin + using ColorSchemes + + # Access the viridis color scheme + color_scheme = ColorSchemes.viridis + + # Function to map a value between 0 and 1 to a color in the viridis scheme + function map_to_color(value) + return get(color_scheme, value) + end + + (a, b) -> map_to_color(b.r) + end) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A,B]) + +w = h = 128 +animate_system_2(ds, w, h, 200.0, 0.02; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation16.jl b/examples/animation16.jl new file mode 100644 index 0000000..34e23ce --- /dev/null +++ b/examples/animation16.jl @@ -0,0 +1,34 @@ +using GeneticTextures + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +F_da = :(A^2 + Complex(0.74543, 0.11301)) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 1, B)) + +F_C0 = :(0) # C will be used as a counter +F_dC = :(C + 1) + +color_expr = :(begin + using ColorSchemes + + # Access the viridis color scheme + color_scheme = ColorSchemes.viridis + + # Function to map a value between 0 and 1 to a color in the viridis scheme + function map_to_color(value) + return get(color_scheme, value) + end + + (a, b, c) -> map_to_color(b.r/c.r) + end) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 128 +animate_system_2(ds, w, h, 200.0, 0.3; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation17.jl b/examples/animation17.jl new file mode 100644 index 0000000..6d81874 --- /dev/null +++ b/examples/animation17.jl @@ -0,0 +1,51 @@ +using GeneticTextures + +F_A0 = :(Complex(4*x, 4*y)) +F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 0.1, B)) + +F_C0 = :(0) # C will be used as a counter +F_dC = :(C + 0.1) + + +# Define the color expression +color_expr = :(begin + using ColorSchemes + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + # Access the viridis color scheme + color_scheme = ColorSchemes.viridis + + # Function to map a value between 0 and 1 to a color in the viridis scheme + function map_to_color(value) + return get(color_scheme, value) + end + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = smooth_modifier(c.r, a.r) + + # Normalize using the counter C + normalized_k = modified_k / c.r + + # Map to color + map_to_color(normalized_k) + end +end) + +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 1024 +animate_system_2(ds, w, h, 2.0, 0.01; +color_expr, +complex_expr) + diff --git a/examples/animation18.jl b/examples/animation18.jl new file mode 100644 index 0000000..021cef2 --- /dev/null +++ b/examples/animation18.jl @@ -0,0 +1,37 @@ +using GeneticTextures +F_A0 = :(Complex(4*x, y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +F_dA = :(A^2 + Complex(0.74543, 0.11301)) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 1, B)) + +F_C0 = :(0) # C will be used as a counter +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = angle_modifier(c.r, a.r) + end +end) + +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 32 +GeneticTextures.animate_system_3(ds, w, h, 200.0, 0.01; +cmap = :viridis, +value_expr, +complex_expr) + diff --git a/examples/animation19.jl b/examples/animation19.jl new file mode 100644 index 0000000..cc30dce --- /dev/null +++ b/examples/animation19.jl @@ -0,0 +1,39 @@ +using GeneticTextures + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +F_dA = :(min(A, 1.0) / A + laplacian(A) * max(A, Complex(0.74543,0.11301))^2) + +F_B0 = :(00.0+0.0) +F_dB = :(ifs(abs(A) > 2, B + 1, B)) + +F_C0 = :(0.0+0.0) # C will be used as a counter +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = smooth_modifier(c.r, a.r) + end +end) + +complex_expr = :((c) -> abs(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 1024 +GeneticTextures.animate_system_3(ds, w, h, 200.0, 0.01; +cmap = :viridis, +value_expr, +complex_expr) + diff --git a/examples/animation20.jl b/examples/animation20.jl new file mode 100644 index 0000000..bb284ec --- /dev/null +++ b/examples/animation20.jl @@ -0,0 +1,72 @@ +using GeneticTextures + +dt = 1 + +F_A0 = :(4*x) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +# F_dA = :(neighbor_min(A; Δx=4, Δy=4)^2 + A^2 + Complex(0.74543,0.11301)) +# F_dA = :(A^2 + Complex(0.74543,0.11301) - A/$dt) +F_dA = :(A*x - A/$dt) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 1, B)) + +F_C0 = :(0) # C will be used as a counter +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + continuous_potential(k, z) = abs(z) > 2 ? 1 + k - log(log(abs(z))) / log(2) : k + + tester(k, z) = real(z) + + orbit_trap_modifier(k, z, trap_radius, max_iter, c) = begin + trapped = false + z_trap = z + for i in 1:k + z_trap = z_trap^2 + c + if abs(z_trap) <= trap_radius + trapped = true + break + end + end + + if trapped + return abs(z_trap) / trap_radius + else + return k / max_iter + end + end + + # (a, b, c) -> begin + # # Include orbit trap modifier + # trap_radius = 1 # Set this to whatever trap radius you want + # max_iter = 1 # Set this to your maximum number of iterations + # orbit_value = orbit_trap_modifier(c.r, a.r, trap_radius, max_iter, Complex(0.74543, 0.11301)) # c is your Julia constant + # end + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = tester(c.r, a.r) + end +end) + +complex_expr = :((c) -> (c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 64 +GeneticTextures.animate_system_3(ds, w, h, 200.0, dt; +cmap = :viridis, +value_expr, +complex_expr) + diff --git a/examples/animation21.jl b/examples/animation21.jl new file mode 100644 index 0000000..bb284ec --- /dev/null +++ b/examples/animation21.jl @@ -0,0 +1,72 @@ +using GeneticTextures + +dt = 1 + +F_A0 = :(4*x) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +# F_dA = :(neighbor_min(A; Δx=4, Δy=4)^2 + A^2 + Complex(0.74543,0.11301)) +# F_dA = :(A^2 + Complex(0.74543,0.11301) - A/$dt) +F_dA = :(A*x - A/$dt) + +F_B0 = :(0) +F_dB = :(ifs(abs(A) > 2, B + 1, B)) + +F_C0 = :(0) # C will be used as a counter +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + continuous_potential(k, z) = abs(z) > 2 ? 1 + k - log(log(abs(z))) / log(2) : k + + tester(k, z) = real(z) + + orbit_trap_modifier(k, z, trap_radius, max_iter, c) = begin + trapped = false + z_trap = z + for i in 1:k + z_trap = z_trap^2 + c + if abs(z_trap) <= trap_radius + trapped = true + break + end + end + + if trapped + return abs(z_trap) / trap_radius + else + return k / max_iter + end + end + + # (a, b, c) -> begin + # # Include orbit trap modifier + # trap_radius = 1 # Set this to whatever trap radius you want + # max_iter = 1 # Set this to your maximum number of iterations + # orbit_value = orbit_trap_modifier(c.r, a.r, trap_radius, max_iter, Complex(0.74543, 0.11301)) # c is your Julia constant + # end + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = tester(c.r, a.r) + end +end) + +complex_expr = :((c) -> (c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + +w = h = 64 +GeneticTextures.animate_system_3(ds, w, h, 200.0, dt; +cmap = :viridis, +value_expr, +complex_expr) + diff --git a/examples/animation22.jl b/examples/animation22.jl new file mode 100644 index 0000000..a1ddc1c --- /dev/null +++ b/examples/animation22.jl @@ -0,0 +1,21 @@ +using GeneticTextures +F_A0 = :(y) +F_dA = :(neighbor_max(neighbor_max(C))) + +F_B0 = :(1.0) +F_dB = :(x_grad(C)) + +F_C = :((1 - rand_scalar()) + y) +F_dC = :(neighbor_ave(grad_direction(B * 0.25))) + + +color_expr = :((a, b, c) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +w = h = 128 +animate_system_2(ds, w, h, 10.0, 0.1; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation23.jl b/examples/animation23.jl new file mode 100644 index 0000000..ee0ea8a --- /dev/null +++ b/examples/animation23.jl @@ -0,0 +1,21 @@ +using GeneticTextures +F_A0 = :(y) +F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +F_B0 = :(1.0) +F_dB = :(x_grad(C)) + +F_C = :((1 - rand_scalar()*1.68+0.12) + y) +F_dC = :(neighbor_ave(grad_direction(B * 0.25; Δx=4, Δy=4))) + + +color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +w = h = 64 +animate_system_2threaded(ds, w, h, 10.0, 0.1; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation3.jl b/examples/animation3.jl new file mode 100644 index 0000000..c2f53c0 --- /dev/null +++ b/examples/animation3.jl @@ -0,0 +1,16 @@ + +# Example usage: +using GeneticTextures + +F_A0= :(ifs(rand_scalar() > 0.97, 0.0, 1.0)) +F_B0= :(Complex(0.4, 1.0)) +F_dA= :(neighbor_min(A; Δx = 2, Δy = 2)) +F_dB= :(4.99 * laplacian(A)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +complex_expr = :((c) -> abs(c)) +color_expr = :((a, b) -> RGB(a.r * 0.8, abs(b.g - a.g), ((a.b * b.b) * 0.2) % 1.0)) +animate_system_2(ds, 256, 256, 2.0, 0.002; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation4.jl b/examples/animation4.jl new file mode 100644 index 0000000..d110388 --- /dev/null +++ b/examples/animation4.jl @@ -0,0 +1,22 @@ + +# Example usage: +using GeneticTextures + +# F_A0= :(Color(Complex(x, y), Complex(y, x), Complex(abs(x - y), abs(x + y)))) +# F_dA= :(min(A, Complex(1.0, 0.2)) * A + exp(laplacian(A * Complex(-1.2, 1.0)))) + +F_A0 = :(ifs(rand_scalar() > 0.97, 0.0, 1.0)) +F_dA = :(x * y + exp(laplacian(A * B * Complex(-1.2, 1.0)))) + +F_B0 = :(Complex(y, x)) +F_dB = :(4.99 * laplacian(A + neighbor_min(A; Δx = 2, Δy = 2))) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a, b) -> RGB(a.r * 0.8, a.g, ((a.b) * 0.2) % 1.0)) + +animate_system_2(ds, 64, 64, 2.0, 0.002; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation5.jl b/examples/animation5.jl new file mode 100644 index 0000000..5d16bb8 --- /dev/null +++ b/examples/animation5.jl @@ -0,0 +1,22 @@ + +# Example usage: +using GeneticTextures + +# F_A0= :(Color(Complex(x, y), Complex(y, x), Complex(abs(x - y), abs(x + y)))) +# F_dA= :(min(A, Complex(1.0, 0.2)) * A + exp(laplacian(A * Complex(-1.2, 1.0)))) + +F_A0 = :(-1 * ifs(rand_scalar() > 0.992, 0.0, 1.0)) +F_dA = :(neighbor_min(A; Δx = 2 + Int(round(t*200)), Δy = 2 + Int(round(t*300))) + exp(laplacian(B))) + +F_B0 = :(-0.24 * x) +F_dB = :(4.99 * laplacian(A * 10 * t)* 10 * t) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + +animate_system_2(ds, 128, 128, 2.0, 0.0005; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation6.jl b/examples/animation6.jl new file mode 100644 index 0000000..afb7509 --- /dev/null +++ b/examples/animation6.jl @@ -0,0 +1,22 @@ + +# Example usage: +using GeneticTextures + +# F_A0= :(Color(Complex(x, y), Complex(y, x), Complex(abs(x - y), abs(x + y)))) +# F_dA= :(min(A, Complex(1.0, 0.2)) * A + exp(laplacian(A * Complex(-1.2, 1.0)))) + +F_A0 = :(1 * ifs(rand_scalar() > 0.9991, Color(0.23 + 100*t, 0.35 - 200*t, 0.24+500*t), Color(x - 0.1 + t*200, y - 0.2 + t*300, abs(x - y) + t*300))) +F_dA = :(neighbor_min(A; Δx = 2 + Int(round(t*200)), Δy = 2 + Int(round(t*200))) + exp(laplacian(B))) + +F_B0 = :(-0.24 * x * Color(x, y, abs(x - y))) +F_dB = :(4.99 * laplacian(A * 10 * t)* 10 * t) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a,b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + +animate_system_2(ds, 720, 720, 2.0, 0.0005; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation7.jl b/examples/animation7.jl new file mode 100644 index 0000000..cc9d388 --- /dev/null +++ b/examples/animation7.jl @@ -0,0 +1,18 @@ +# Example usage: +using GeneticTextures + +# F_A0= :(Color(Complex(x, y), Complex(y, x), Complex(abs(x - y), abs(x + y)))) +# F_dA= :(min(A, Complex(1.0, 0.2)) * A + exp(laplacian(A * Complex(-1.2, 1.0)))) + +F_A0 = :(Complex(x, y)) +F_dA = :(Complex(0, 1) * (min(A, 1.0) * A) - 0.7 * exp(neighbor_max(A) * Complex(0.2, -0.12))) + +A = VariableDynamics(:A, F_A0, F_dA) + + +ds = DynamicalSystem([A]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) + +animate_system_2(ds, 64, 64, 2.0, 0.02; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation8.jl b/examples/animation8.jl new file mode 100644 index 0000000..b1a38da --- /dev/null +++ b/examples/animation8.jl @@ -0,0 +1,21 @@ +using GeneticTextures + + +# F_A0= :(Color(Complex(x, y), Complex(y, x), Complex(abs(x - y), abs(x + y)))) +# F_dA= :(min(A, Complex(1.0, 0.2)) * A + exp(laplacian(A * Complex(-1.2, 1.0)))) + +F_A0 = :(1 * ifs(rand_scalar() > 0.9994, 0, 1)) +F_dA = :(neighbor_min(A; Δx = 2 + Int(round(t*200)), Δy = 2 + Int(round(t*200))) + exp(laplacian(B))) + +F_B0 = :(-0.24 * x) +F_dB = :(4.99 * laplacian(A * 10 * t)* 10 * t - x_grad(A * 10 * t)^2) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a,b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + +animate_system_2(ds, 720, 720, 2.0, 0.0005; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation9.jl b/examples/animation9.jl new file mode 100644 index 0000000..4e98fca --- /dev/null +++ b/examples/animation9.jl @@ -0,0 +1,14 @@ +using GeneticTextures +F_A0 = :(perlin_color(84.2126, grad_mag(mod, 0.4672, 0.8845 - 0.9128), perlin_2d(66.4615, dissolve(grad_dir(mod, or(x, x) + and(x, y), exp(y)), sinh(atan(x, y)), and(exp(y), x)), sqrt(sinh(dissolve(dissolve(y, 0.0344, x) - and(x, 0.9832), cosh(y) - abs(y), sin(y))))), Color(0.28, 0.4202, 0.86))) +F_dA = :(-1.0 * laplacian(A) * B + 0.4 * neighbor_min(A; Δx = 2, Δy = 2) * perlin_color(24.2126, 2.4, 3.2-t*90, 2.4+t*100)) +F_B0 = :(ifs(and(x ^ 2.0 + y ^ 2.0 < 0.1, x ^ 2.0 + y ^ 2.0 > 0.09), 0.0, 1.0) * Color(0.7, 1.4, 0.9)* perlin_color(84.2126, 2.4, 3.2-t*10, 2.4+t*100)) +F_dB = :(-1.0 * laplacian(B) * A * Color(0.7, 1.4, 0.9) * perlin_color(24.2126, 2.4, 3.2-t*80, 2.4+t*90)- 0.4 * neighbor_min(B)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A,B]) +complex_expr = :((c) -> abs(c)) +# color_expr = :((a) -> RGB(a.r, a.g, a.b)) +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + +animate_system_2(ds, 128, 128, 4.0, 0.01; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/animation_test.jl b/examples/animation_test.jl new file mode 100644 index 0000000..b04c47d --- /dev/null +++ b/examples/animation_test.jl @@ -0,0 +1,18 @@ +using GeneticTextures + +F_A0 = :(sqrt(perlin_color(67.9588, x, x, Color(0.26, 0.2, 0.13)))) +F_dA = :(-1.0 * laplacian(A) * B + 0.4 * neighbor_min(A; Δx = 2, Δy = 2) * perlin_color(24.2126, 2.4, 3.2-t*90, 2.4+t*100)) +F_B0 = :(ifs(and(x ^ 2.0 + y ^ 2.0 < 0.1, x ^ 2.0 + y ^ 2.0 > 0.09), 0.0, 1.0) * Color(0.7, 1.4, 0.9)* perlin_color(84.2126, 2.4, 3.2-t*10, 2.4+t*100)) +F_dB = :(-1.0 * laplacian(B) * A * Color(0.7, 1.4, 0.9) * perlin_color(24.2126, 2.4, 3.2-t*80, 2.4+t*90)- 0.4 * neighbor_min(B)) + + + +color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A,B]) + +w = h = 64 +animate_system_2(ds, w, h, 200.0, 0.1; color_expr, complex_expr) diff --git a/examples/benchmark_append.jl b/examples/benchmark_append.jl new file mode 100644 index 0000000..c6c8627 --- /dev/null +++ b/examples/benchmark_append.jl @@ -0,0 +1,265 @@ +using BenchmarkTools, CSV, DataFrames, Plots + +function evolve_normal!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function evolve_threaded!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function vectorize_color_decision!(results, δvals, complex_func, i) + # Determine the type of each element in results + is_color = [r isa Color for r in results] + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] + + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] + + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_vectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Loop through each dynamical system + for (i, ds) in enumerate(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Helper function to create meshgrid equivalent to MATLAB's +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end + +function evolve_threadandvectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + n_blocks = Threads.nthreads() * 4 + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] + + + # Vectorized computation within each block + for (i, ds) in enumerate(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) + end + + # Update the global δvals with the block's results + for (i, ds) in enumerate(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +T = 1.0 # Total time +dt = 0.2 # Time step + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +color_func = eval(color_expr) +complex_func = eval(complex_expr) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +dynamics = DynamicalSystem3([A, B]) + +# Define the new sizes you want to benchmark +additional_sizes = [16, 32, 64] + +# Total time and time step settings +T = 1.0 +dt = 0.2 + +# Load existing results if they exist +results_file = "benchmark_results_new.csv" +df_existing = if isfile(results_file) + CSV.read(results_file, DataFrame) +else + DataFrame( + Size = Int[], + Normal_Time = Float64[], + Threaded_Time = Float64[], + Vectorized_Time = Float64[], + ThreadVector_Time = Float64[], + Normal_MinTime = Float64[], + Threaded_MinTime = Float64[], + Vectorized_MinTime = Float64[], + ThreadVector_MinTime = Float64[], + Normal_MaxTime = Float64[], + Threaded_MaxTime = Float64[], + Vectorized_MaxTime = Float64[], + ThreadVector_MaxTime = Float64[], + Normal_Mem = String[], + Threaded_Mem = String[], + Vectorized_Mem = String[], + ThreadVector_Mem = String[] + ) +end + +# Initialize a DataFrame to store new benchmark results +df_new = similar(df_existing) + +# Perform benchmarking only for additional sizes +for size in additional_sizes + global width = global height = size + vals = [Color.(rand(height, width), rand(height, width), rand(height, width)) for _ in 1:length(dynamics)] + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + normal_bench = @benchmark evolve_normal!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + threaded_bench = @benchmark evolve_threaded!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + vectorized_bench = @benchmark evolve_vectorized!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + threadvector_bench = @benchmark evolve_threadandvectorized!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + + # Store detailed benchmark results + push!(df_new, ( + Size = size, + Normal_Time = mean(normal_bench.times) / 1e6, + Threaded_Time = mean(threaded_bench.times) / 1e6, + Vectorized_Time = mean(vectorized_bench.times) / 1e6, + ThreadVector_Time = mean(threadvector_bench.times) / 1e6, + Normal_MinTime = minimum(normal_bench.times) / 1e6, + Threaded_MinTime = minimum(threaded_bench.times) / 1e6, + Vectorized_MinTime = minimum(vectorized_bench.times) / 1e6, + ThreadVector_MinTime = minimum(threadvector_bench.times) / 1e6, + Normal_MaxTime = maximum(normal_bench.times) / 1e6, + Threaded_MaxTime = maximum(threaded_bench.times) / 1e6, + Vectorized_MaxTime = maximum(vectorized_bench.times) / 1e6, + ThreadVector_MaxTime = maximum(threadvector_bench.times) / 1e6, + Normal_Mem = Base.format_bytes(memory(normal_bench)), + Threaded_Mem = Base.format_bytes(memory(threaded_bench)), + Vectorized_Mem = Base.format_bytes(memory(vectorized_bench)), + ThreadVector_Mem = Base.format_bytes(memory(threadvector_bench)) + )) +end + +# Append new results to the existing DataFrame and save +append!(df_existing, df_new) +CSV.write(results_file, df_existing) diff --git a/examples/benchmark_evolve.jl b/examples/benchmark_evolve.jl new file mode 100644 index 0000000..44ec862 --- /dev/null +++ b/examples/benchmark_evolve.jl @@ -0,0 +1,254 @@ +using BenchmarkTools, CSV, DataFrames, Plots + +function evolve_normal!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function evolve_threaded!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function vectorize_color_decision!(results, δvals, complex_func, i) + # Determine the type of each element in results + is_color = [r isa Color for r in results] + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] + + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] + + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_vectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Loop through each dynamical system + for (i, ds) in enumerate(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Helper function to create meshgrid equivalent to MATLAB's +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end + +function evolve_threadandvectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + n_blocks = Threads.nthreads() * 4 + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] + + + # Vectorized computation within each block + for (i, ds) in enumerate(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) + end + + # Update the global δvals with the block's results + for (i, ds) in enumerate(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Example sizes and parameters +sizes = [128, 256, 512, 1024] # Example: 128x128, 256x256, 512x512, 1024x1024 pixels +T = 1.0 # Total time +dt = 0.2 # Time step + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +color_func = eval(color_expr) +complex_func = eval(complex_expr) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +dynamics = DynamicalSystem3([A, B]) + +# DataFrame to hold the results +df = DataFrame( + Size = Int[], + Normal_Time = Float64[], + Threaded_Time = Float64[], + Vectorized_Time = Float64[], + ThreadVector_Time = Float64[], + Normal_MinTime = Float64[], + Threaded_MinTime = Float64[], + Vectorized_MinTime = Float64[], + ThreadVector_MinTime = Float64[], + Normal_MaxTime = Float64[], + Threaded_MaxTime = Float64[], + Vectorized_MaxTime = Float64[], + ThreadVector_MaxTime = Float64[], + Normal_Mem = String[], + Threaded_Mem = String[], + Vectorized_Mem = String[], + ThreadVector_Mem = String[] +) + +for size in sizes + global width = global height = size + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + vals = [Color.(rand(height, width), rand(height, width), rand(height, width)) for _ in 1:length(dynamics)] + + normal_bench = @benchmark evolve_normal!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + @info "Normal benchmark for size $size completed" + threaded_bench = @benchmark evolve_threaded!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + @info "Threaded benchmark for size $size completed" + vectorized_bench = @benchmark evolve_vectorized!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + @info "Vectorized benchmark for size $size completed" + threadvector_bench = @benchmark evolve_threadandvectorized!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func) + @info "Threaded and vectorized benchmark for size $size completed" + + # Store detailed benchmark results + push!(df, ( + Size = size, + Normal_Time = mean(normal_bench.times) / 1e6, # milliseconds + Threaded_Time = mean(threaded_bench.times) / 1e6, + Vectorized_Time = mean(vectorized_bench.times) / 1e6, + ThreadVector_Time = mean(threadvector_bench.times) / 1e6, + Normal_MinTime = minimum(normal_bench.times) / 1e6, + Threaded_MinTime = minimum(threaded_bench.times) / 1e6, + Vectorized_MinTime = minimum(vectorized_bench.times) / 1e6, + ThreadVector_MinTime = minimum(threadvector_bench.times) / 1e6, + Normal_MaxTime = maximum(normal_bench.times) / 1e6, + Threaded_MaxTime = maximum(threaded_bench.times) / 1e6, + Vectorized_MaxTime = maximum(vectorized_bench.times) / 1e6, + ThreadVector_MaxTime = maximum(threadvector_bench.times) / 1e6, + Normal_Mem = Base.format_bytes(memory(normal_bench)), + Threaded_Mem = Base.format_bytes(memory(threaded_bench)), + Vectorized_Mem = Base.format_bytes(memory(vectorized_bench)), + ThreadVector_Mem = Base.format_bytes(memory(threadvector_bench)), + )) +end + +# Save the DataFrame to a CSV file +CSV.write("benchmark_results.csv", df) \ No newline at end of file diff --git a/examples/benchmark_evolve_threadvector.jl b/examples/benchmark_evolve_threadvector.jl new file mode 100644 index 0000000..df51f9c --- /dev/null +++ b/examples/benchmark_evolve_threadvector.jl @@ -0,0 +1,232 @@ +using BenchmarkTools, CSV, DataFrames, Plots + +function evolve_normal!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function evolve_threaded!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function vectorize_color_decision!(results, δvals, complex_func, i) + # Determine the type of each element in results + is_color = [r isa Color for r in results] + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] + + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] + + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_vectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Loop through each dynamical system + for (i, ds) in enumerate(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Helper function to create meshgrid equivalent to MATLAB's +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end + +function evolve_threadandvectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function; n_blocks=Threads.nthreads()*4) + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] + + + # Vectorized computation within each block + for (i, ds) in enumerate(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) + end + + # Update the global δvals with the block's results + for (i, ds) in enumerate(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Example sizes and parameters +sizes = [64, 128, 256, 512, 1024] +n_blocks = Threads.nthreads() .* [1, 2, 4, 8, 16] +T = 1.0 # Total time +dt = 0.2 # Time step + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +color_func = eval(color_expr) +complex_func = eval(complex_expr) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +dynamics = DynamicalSystem3([A, B]) + +# Define the DataFrame to capture benchmark results +df = DataFrame( + Size = Int[], + Blocks = Int[], + Time = Float64[], + MinTime = Float64[], + MaxTime = Float64[], + Memory = String[], + nthreads = Int[] +) + +# Loop through each size and block configuration +for size in sizes + global width = global height = size + vals = [Color.(rand(height, width), rand(height, width), rand(height, width)) for _ in 1:length(dynamics)] + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + for blocks in n_blocks + bench_result = @benchmark evolve_threadandvectorized!($vals, $dynamics, $custom_exprs, $width, $height, $T, $dt, $complex_func; n_blocks=$blocks) + + # Append results to DataFrame + push!(df, ( + Size = size, + Blocks = blocks, + Time = mean(bench_result.times) / 1e6, # Convert to milliseconds + MinTime = minimum(bench_result.times) / 1e6, + MaxTime = maximum(bench_result.times) / 1e6, + Memory = Base.format_bytes(memory(bench_result)), + nthreads = Threads.nthreads() + )) + + @info "Benchmark completed for size $size with $blocks blocks" + end +end + + +# Save the DataFrame to a CSV file +CSV.write("benchmark_evolve_threadandvector_results.csv", df) \ No newline at end of file diff --git a/examples/debug20.jl b/examples/debug20.jl new file mode 100644 index 0000000..b9b2dc6 --- /dev/null +++ b/examples/debug20.jl @@ -0,0 +1,76 @@ +using GeneticTextures + +dt = 1. + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +# F_dA = :(neighbor_min(A; Δx=4, Δy=4)^2 + A^2 + Complex(0.74543,0.11301)) +F_dA = :(A^2 + Complex(0.3,-0.01) - A/$dt) + +F_dA = :(ifs(abs(A) > 2, 0, A^2 + Complex(0.3,-0.01) - A/$dt)) +# F_dA = :(A*x-1*A/$dt) + +F_B0 = :(0) # C will be used as a counter +F_dB = :(ifs(abs(A) > 2, -1/$dt, 1/$dt)) # stop iterating when A escapes + +F_C0 = :(0) +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + continuous_potential(k, z) = abs(z) > 2 ? 1 + k - log(log(abs(z))) / log(2) : k + + tester(k, z) = real(z) + + orbit_trap_modifier(k, z, trap_radius, max_iter, c) = begin + trapped = false + z_trap = z + for i in 1:k + z_trap = z_trap^2 + c + if abs(z_trap) <= trap_radius + trapped = true + break + end + end + + if trapped + return abs(z_trap) / trap_radius + else + return k / max_iter + end + end + + # (a, b, c) -> begin + # # Include orbit trap modifier + # trap_radius = 1 # Set this to whatever trap radius you want + # max_iter = 1 # Set this to your maximum number of iterations + # orbit_value = orbit_trap_modifier(c.r, a.r, trap_radius, max_iter, Complex(0.74543, 0.11301)) # c is your Julia constant + # end + + (a, b) -> begin + # Calculate the modified iteration count + # modified_k = orbit_trap_modifier(b.r, a.r, 10, 1000, Complex(0.74543, 0.11301)) + modified_k = continuous_potential(b.r, a.r) + end +end) + +complex_expr = :((c) -> (c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B]) + + +w = h = 64 +GeneticTextures.animate_system_3(ds, w, h, 200.0, dt; +normalize_img = true, +cmap = :curl, +value_expr, +complex_expr) diff --git a/examples/example.jl b/examples/example.jl index a356982..07cea5e 100644 --- a/examples/example.jl +++ b/examples/example.jl @@ -17,11 +17,62 @@ mutation_probs = Dict( :duplicate_node => 0.03, ) -original_population, original_image = generate_population(1, primitives_with_arity, max_depth, width, height) + +primitives_with_arity2 = Dict( + :+ => 2, + :- => 2, + :* => 2, + :/ => 2, + :^ => 2, + :sin => 1, + :cos => 1, + :sinh => 1, + :cosh => 1, + :abs => 1, + :sqrt => 1, + :mod => 2, + :perlin_2d => 3, + :perlin_color => 4, + :grad_mag => 1, # grad_mag takes 1 argument, but it can be a function with variable number of arguments + :grad_dir => 3, + :blur => 3, # blur takes 3 arguments, since currently the first argument has to be a function with 2 arguments + :atan => 2, + :log => 1, + :exp => 1, + :round => 1, + :Int => 1, + :or => 2, + :and => 2, + :xor => 2, + :x => 0, + :y => 0, + :rand_scalar => 0, + :rand_color => 0, + :dissolve => 3, + :laplacian => 1, + :x_grad => 1, + :y_grad => 1, + :grad_magnitude => 1, + :grad_direction => 1, + :neighbor_min => 1, + :neighbor_max => 1, + :neighbor_ave => 1, + :ifs => 3, + :max => 2, + :min => 2, + :real => 1, + :imag => 1, + # :A => 0, + # :B => 0, + # :C => 0, + # :t => 0 +) + +original_population, original_image = generate_population(1, primitives_with_arity2, max_depth, width, height) # original_population = [GeneticTextures.CustomExpr(:(cosh(mod(exp(sqrt(or(Color(0.57, 0.22, 0.0), 0.0693))), grad_dir(atan, y / x, sinh(0.4493), 0.4074)))))] # original_population = [GeneticTextures.CustomExpr(:(cos(perlin_color(84.2126, grad_mag(mod, exp(x), 0.91 - 0.9128), abs(x), dissolve(y, Color(0.28, 0.47, 0.86), Color(0.28, 0.47, 0.86))))))] original_image = [generate_image(original_population[1], width, height)] -population, images = create_variations(1, original_population, mutation_probs, primitives_with_arity, max_depth, width, height) +population, images = create_variations(1, original_population, mutation_probs, primitives_with_arity2, max_depth, width, height) display_images(original_image[1], images) @@ -43,7 +94,7 @@ let population = population, images = images break end - population, images = create_variations(best_choice, population, mutation_probs, primitives_with_arity, max_depth, width, height) + population, images = create_variations(best_choice, population, mutation_probs, primitives_with_arity2, max_depth, width, height) display_images(chosen_image, images) push!(images, chosen_image) diff --git a/examples/plot_benchmark.jl b/examples/plot_benchmark.jl new file mode 100644 index 0000000..3b2d0ab --- /dev/null +++ b/examples/plot_benchmark.jl @@ -0,0 +1,76 @@ +using CSV, DataFrames, Plots + +function normalize_memory_(mem_str) + if occursin("GiB", mem_str) + val = parse(Float64, replace(mem_str, " GiB" => "")) + return val * 1024 # Convert GiB to MiB if you want everything in MiB + elseif occursin("MiB", mem_str) + return parse(Float64, replace(mem_str, " MiB" => "")) + else + error("Unknown memory unit in the string: $mem_str") + end +end + +line_styles = [:solid, :dash, :dot, :dashdot] +markers = [:circle, :square, :diamond, :cross] +labels = ["Normal", "Threaded", "Vectorized", "ThreadVector"] + +# Load the data +df = CSV.read("benchmark_results_new.csv", DataFrame) + +# Correcting the ribbon data calculation +ribbon_lower = [(df.Normal_Time .- df.Normal_MinTime), + (df.Threaded_Time .- df.Threaded_MinTime), + (df.Vectorized_Time .- df.Vectorized_MinTime), + (df.ThreadVector_Time .- df.ThreadVector_MinTime)] +ribbon_upper = [(df.Normal_MaxTime .- df.Normal_Time), + (df.Threaded_MaxTime .- df.Threaded_Time), + (df.Vectorized_MaxTime .- df.Vectorized_Time), + (df.ThreadVector_MaxTime .- df.ThreadVector_Time)] + +# You should use ribbon as a vector of tuples, each tuple corresponds to lower and upper for each series +ribbons = [(lower, upper) for (lower, upper) in zip(ribbon_lower, ribbon_upper)] + +# Initialize the plot +p1 = plot(title = "Runtime Performance", + xlabel = "Image Size (pixels)", ylabel = "Average Time (ms)", + legend = :right, size = (800, 600)) + +# Add each series individually +labels = ["Normal", "Threaded", "Vectorized", "ThreadVector"] +times = [df.Normal_Time, df.Threaded_Time, df.Vectorized_Time, df.ThreadVector_Time] + +min_times = zeros(length(df.Size)) # Get the minimum value at each size between df.Normal_Time, df.Threaded_Time, df.Vectorized_Time, df.ThreadVector_Time +min_memory = zeros(length(df.Size)) # Get the minimum value at each size between df.Normal_Mem, df.Threaded_Mem, df.Vectorized_Mem, df.ThreadVector_Mem +for (i, s) in enumerate(df.Size) + min_times[i] = minimum([df.Normal_Time[i], df.Threaded_Time[i], df.Vectorized_Time[i], df.ThreadVector_Time[i]]) - 1 + min_memory[i] = minimum([normalize_memory_(df.Normal_Mem[i]), normalize_memory_(df.Threaded_Mem[i]), normalize_memory_(df.Vectorized_Mem[i]), normalize_memory_(df.ThreadVector_Mem[i])]) - 1 +end + + +for i in eachindex(labels) + plot!(df.Size, times[i], + label = labels[i], + lw = 2, line = line_styles[i], marker = markers[i], markersize = 4) +end + +# Adjust memory normalization and plot memory usage +mem_usage = [normalize_memory_.(df.Normal_Mem), normalize_memory_.(df.Threaded_Mem), normalize_memory_.(df.Vectorized_Mem), normalize_memory_.(df.ThreadVector_Mem)] + +# Initialize the plot +p2 = plot(title = "Memory Usage", + xlabel = "Image Size (pixels)", ylabel = "Memory Usage (MiB)", + legend = :right, size = (800, 600)) + +# Add each series individually +for i in eachindex(labels) + plot!(df.Size, mem_usage[i], + label = labels[i], + lw = 2, line = line_styles[i], marker = markers[i], markersize = 4) +end +# Create a combined plot layout +combined_plot = plot(p1, p2, layout = (2, 1), size = (800, 800)) +display(combined_plot) + +# Optionally save the plot to a file +savefig(combined_plot, "combined_performance_analysis.png") \ No newline at end of file diff --git a/examples/plot_evolve_threadandvec_benchmark.jl b/examples/plot_evolve_threadandvec_benchmark.jl new file mode 100644 index 0000000..9050f23 --- /dev/null +++ b/examples/plot_evolve_threadandvec_benchmark.jl @@ -0,0 +1,60 @@ +using CSV, DataFrames, Plots + +function normalize_memory_(mem_str) + if occursin("GiB", mem_str) + val = parse(Float64, replace(mem_str, " GiB" => "")) + return val * 1024 # Convert GiB to MiB if you want everything in MiB + elseif occursin("MiB", mem_str) + return parse(Float64, replace(mem_str, " MiB" => "")) + else + error("Unknown memory unit in the string: $mem_str") + end +end + +line_styles = [:solid, :dash, :dot, :dashdot, :dashdash] +markers = [:circle, :square, :diamond, :cross, :utriangle] +labels = ["Normal", "Threaded", "Vectorized", "ThreadVector"] +sizes = [64, 128, 256, 512, 1024] + +# Load the data +df = CSV.read("benchmark_evolve_threadandvector_results.csv", DataFrame) + +# Normalize memory usage +df.Memory_MiB = [normalize_memory_(mem) for mem in df.Memory] + +# Group by Size and Blocks to aggregate data if necessary or prepare for plotting directly +grouped = groupby(df, [:Blocks]) + +# Initialize the plot for time and memory usage +p1 = plot(title = "Runtime Difference compared with best", + xlabel = "Image Size (pixels)", ylabel = "Average Time (ms)", legend = :outertopright, xscale = :log2, yscale = :log) + +p2 = plot(title = "Memory Usage Difference compared with best", + xlabel = "Image Size (pixels)", ylabel = "Memory Usage (MiB)", legend = :outertopright, xscale = :log2, yscale = :log) + +min_times = zeros(length(sizes)) # Get the minimum value at each size between df.Normal_Time, df.Threaded_Time, df.Vectorized_Time, df.ThreadVector_Time +min_memory = zeros(length(sizes)) # Get the minimum value at each size between df.Normal_Mem, df.Threaded_Mem, df.Vectorized_Mem, df.ThreadVector_Mem +for (i, (key, group)) in enumerate(pairs(grouped)) + min_times[i] = minimum([group.Time[i] for (key, group) in pairs(grouped)]) - 1 + min_memory[i] = minimum([group.Memory_MiB[i] for (key, group) in pairs(grouped)]) - 1 +end + +# Iterate through each group and plot +for (i, (key, group)) in enumerate(pairs(grouped)) + blocks = key.Blocks[1] #blocks is a GroupKey + println("blocks: $blocks") + + + plot!(p1, sizes, group.Time .- min_times, label = "Blocks=$blocks", + line = line_styles[i], marker = markers[i], markersize = 4, lw = 2) + + plot!(p2, sizes, group.Memory_MiB .- min_memory, label = "Blocks=$blocks", + line = line_styles[i], marker = markers[i], markersize = 4, lw = 2) +end + +# Create a combined plot layout +combined_plot = plot(p1, p2, layout = (2, 1), size = (800, 800)) +display(combined_plot) + +# Optionally save the plot to a file +savefig(combined_plot, "evolve_theadandvec_performance_by_blocks.png") diff --git a/examples/refactor_code_5.jl b/examples/refactor_code_5.jl new file mode 100644 index 0000000..a17d831 --- /dev/null +++ b/examples/refactor_code_5.jl @@ -0,0 +1,596 @@ +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + + caller_func = positional_args[1] + + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + + + +# minus one means that this is a matrix? +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + # Evaluate function at x + Δx + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + + # Compute the finite difference + grad_x = (x_plus_val - center_val) / Δx_scaled + return grad_x +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Evaluate function at y + Δy + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + + # Compute the finite difference + grad_y = (y_plus_val - center_val) / Δy_scaled + return grad_y +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + min_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int + idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + + max_val = expr(merge(vars, Dict([k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars]))) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int + idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + + sum_val = func(vars) + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + # temp_vars = merge(temp_vars, Dict(k => (isa(v, Matrix) ? v[idx_x + dx, idx_y + dy] : v) for (k, v) in temp_vars)) + # println("values in : $([(k, v) for (k, v) in temp_vars])") + # println("typeof values: $([typeof(v) for (k, v) in temp_vars])") + # println("values in : $([typeof(v) for (k, v) in merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in temp_vars))])") + + sum_val += func(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +using Images, Colors + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + cval = rand(width, height) + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5, :D => cval) + # vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +using GeneticTextures: Color + +# TODO: please make that these can have any name and just pass them as arguments... +width = height = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +expr = :(sin(100*x) + neighbor_ave(x)) +# expr = :(rand_color()) +samplers = Dict() +compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) +custom_expr = CustomExpr(compiled) + +# Generate the image +# @time image = generate_image_refactored(custom_expr, w, h) + +@time image = generate_image_refactored(custom_expr, width, height; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + + +# struct VariableDynamics3 +# name::Symbol +# F_0::Union{Expr, Symbol, Number, Color} +# δF::Union{Expr, Symbol, Number, Color} + +# function VariableDynamics3(name, F_0, δF) +# return new(name, F_0, δF) +# end +# end + +# name(var::VariableDynamics3) = var.name +# F_0(var::VariableDynamics3) = var.F_0 +# δF(var::VariableDynamics3) = var.δF + +# struct DynamicalSystem3 +# dynamics::Vector{VariableDynamics3} +# end + +# function evolve_system_step_2!(vals, dynamics::DynamicalSystem3, width, height, t, dt, complex_func::Function) +# δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + +# custom_expr = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + +# vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + +# for x_pixel in 1:width +# for y_pixel in 1:height +# x = (x_pixel - 1) / (width - 1) - 0.5 +# y = (y_pixel - 1) / (height - 1) - 0.5 + +# vars[:x] = x +# vars[:y] = y + +# for (i, ds) in enumerate(dynamics) + +# val = dt .* invokelatest(custom_expr[i].func, vars) + +# if val isa Color +# δvals[i][y_pixel, x_pixel] = val +# elseif isreal(val) +# δvals[i][y_pixel, x_pixel] = Color(val, val, val) +# else +# δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) +# end +# end +# end +# end + +# # Update vals +# for (i, ds) in enumerate(dynamics) +# vals[i] += δvals[i] +# end + +# return vals +# end + +# function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) +# color_func = eval(color_expr) +# complex_func = eval(complex_expr) + + +# custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + +# # Initialize each vars' grid using their F_0 expression +# vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] +# t = 0 +# for x_pixel in 1:width +# for y_pixel in 1:height +# x = (x_pixel - 1) / (width - 1) - 0.5 +# y = (y_pixel - 1) / (height - 1) - 0.5 + +# for (i, ds) in enumerate(dynamics) +# vars = Dict(:x => x, :y => y, :t => t) +# val = invokelatest(custom_exprs[i].func, vars) + +# if val isa Color +# vals[i][y_pixel, x_pixel] = val +# else +# vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) +# end +# end +# end +# end + +# # Generate a unique filename +# base_dir = "saves" +# if !isdir(base_dir) +# mkdir(base_dir) +# end + +# animation_id = length(readdir(base_dir)) + 1 +# animation_dir = base_dir * "/animation_$animation_id" + +# # If the directory already exists, increment the id until we find one that doesn't +# while isdir(animation_dir) +# animation_id += 1 +# animation_dir = base_dir * "/animation_$animation_id" +# end + +# mkdir(animation_dir) + +# # Save the system's expressions to a file +# expr_file = animation_dir * "/expressions.txt" +# open(expr_file, "w") do f +# write(f, "Animated using 'animate_system_2' function\n") +# for ds in dynamics +# write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") +# write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") +# end +# write(f, "color_func= $(capture_function(color_expr))\n") +# write(f, "complex_func= $(capture_function(complex_expr))\n") +# write(f, "T= $T\n") +# write(f, "dt= $dt\n") +# write(f, "width= $width\n") +# write(f, "height= $height\n") +# end + +# image_files = [] # Store the names of the image files to use for creating the gif + +# for (i, t) in enumerate(range(0, T, step=dt)) +# vals = evolve_system_step_2!(vals, dynamics, width, height, t, dt, complex_func) # Evolve the system + +# # Create an image from current state +# img = Array{RGB{Float64}, 2}(undef, height, width) +# for x_pixel in 1:width +# for y_pixel in 1:height +# values = [var[y_pixel, x_pixel] for var in vals] + +# img[y_pixel, x_pixel] = +# invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) +# end +# end + +# if normalize_img +# img = clean!(img) +# end + +# frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" +# save(frame_file, map(clamp01nan, img)) + +# # Append the image file to the list +# push!(image_files, frame_file) +# end +# # Create the gif +# gif_file = animation_dir * "/animation_$animation_id.gif" +# run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps +# # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) +# # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + +# println("Animation saved to: $gif_file") +# println("Frames saved to: $animation_dir") +# println("Expressions saved to: $expr_file") +# end + +# Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +# Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25; Δx=4, Δy=4))) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# w = h = 128 +# animate_system_2threaded(ds, w, h, 10.0, 0.1; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/refactor_test.jl b/examples/refactor_test.jl new file mode 100644 index 0000000..0eb7a36 --- /dev/null +++ b/examples/refactor_test.jl @@ -0,0 +1,797 @@ +using CoherentNoise: sample, perlin_2d + +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + + caller_func = positional_args[1] + + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +# minus one means that this is a matrix? +GeneticTextures.primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + if idx_x == width + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled))) # Evaluate function at x - Δx + return (center_val - x_minus_val) / Δx_scaled + else + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) # Evaluate function at x + Δx + return (x_plus_val - center_val) / Δx_scaled + end +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Compute the finite difference + if idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) # Evaluate function at y - Δy + return (center_val - y_minus) / Δy_scaled + else + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) # Evaluate function at y + Δy + return (y_plus_val - center_val) / Δy_scaled + end +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + if Δx == 0 + ∇x = 0 + else + if idx_x > 1 && idx_x < width + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + elseif idx_x == 1 + x_plus = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + ∇x = (x_plus - center_val) / Δx_scaled^2 + else # idx_x == width + x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled))) + ∇x = (center_val - x_minus) / Δx_scaled^2 + end + end + + if Δy == 0 + ∇y = 0 + else + if idx_y > 1 && idx_y < height + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + elseif idx_y == 1 + y_plus = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + ∇y = (y_plus - center_val) / Δy_scaled^2 + else # idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) + ∇y = (center_val - y_minus) / Δy_scaled^2 + end + end + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + min_val = func(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = func(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + max_val = func(vars) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + val = func(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + sum_val = func(vars) + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + sum_val += func(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +using Images, Colors + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +using GeneticTextures: Color + +# TODO: please make that these can have any name and just pass them as arguments... +width = height = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +# expr = :(sin(100*x) + neighbor_ave(x)) +# expr = :( + neighbor_max(sin(100*x)/sin(10*y))) +# expr = :(perlin_2d(123, Color(0.4*x, 0.5*x, 0.2*y), Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y) + D)) +# expr = :(Color(0.4*x, 0.5*x, 0.2*y)* Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y)) + +# # expr = :(rand_color()) +# samplers = Dict() +# compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) +# custom_expr = CustomExpr(compiled) + +# # Generate the image +# # @time image = generate_image_refactored(custom_expr, w, h) + +# @time image = generate_image_refactored(custom_expr, width, height; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + +using GeneticTextures: capture_function +struct VariableDynamics3 + name::Symbol + F_0::Union{Expr, Symbol, Number, Color} + δF::Union{Expr, Symbol, Number, Color} + + function VariableDynamics3(name, F_0, δF) + return new(name, F_0, δF) + end +end + +name(var::VariableDynamics3) = var.name +F_0(var::VariableDynamics3) = var.F_0 +δF(var::VariableDynamics3) = var.δF + +struct DynamicalSystem3 + dynamics::Vector{VariableDynamics3} +end + +function evolve_system_step_threaded!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function vectorize_color_decision!(results, δvals, complex_func, i) + # Determine the type of each element in results + is_color = [r isa Color for r in results] + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] + + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] + + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_system_step_vectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Loop through each dynamical system + for (i, ds) in enumerate(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Helper function to create meshgrid equivalent to MATLAB's +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end + +function evolve_system_step_threadandvectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + n_blocks = Threads.nthreads() * 4 + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] + + # Vectorized computation within each block + for (i, ds) in enumerate(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) + end + + # Update the global δvals with the block's results + for (i, ds) in enumerate(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + + +using ProgressMeter + +function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) + color_func = eval(color_expr) + complex_func = eval(complex_expr) + + custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] # Initialize each vars' grid using their F_0 expression + t = 0 + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + for (i, ds) in enumerate(dynamics) + vars = Dict(:x => x, :y => y, :t => t) + val = invokelatest(custom_exprs[i].func, vars) + + if val isa Color + vals[i][y_pixel, x_pixel] = val + else + vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Generate a unique filename + base_dir = "saves" + if !isdir(base_dir) + mkdir(base_dir) + end + + animation_id = length(readdir(base_dir)) + 1 + animation_dir = base_dir * "/animation_$animation_id" + + # If the directory already exists, increment the id until we find one that doesn't + while isdir(animation_dir) + animation_id += 1 + animation_dir = base_dir * "/animation_$animation_id" + end + + mkdir(animation_dir) + + # Save the system's expressions to a file + expr_file = animation_dir * "/expressions.txt" + open(expr_file, "w") do f + write(f, "Animated using 'animate_system_2' function\n") + for ds in dynamics + write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") + write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") + end + write(f, "color_func= $(capture_function(color_expr))\n") + write(f, "complex_func= $(capture_function(complex_expr))\n") + write(f, "T= $T\n") + write(f, "dt= $dt\n") + write(f, "width= $width\n") + write(f, "height= $height\n") + end + + image_files = [] # Store the names of the image files to use for creating the gif + + # We only need to compile once each expression + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + total_frames = ceil(Int, T / dt) + progress = Progress(total_frames, desc="Initializing everything...", barlen=80) + + # Evolve the system over time + start_time = time() + for (i, t) in enumerate(range(0, T, step=dt)) + vals = evolve_system_step_threaded!(vals, dynamics, custom_exprs, width, height, t, dt, complex_func) # Evolve the system + + # Create an image from current state + img = Array{RGB{Float64}, 2}(undef, height, width) + for x_pixel in 1:width + for y_pixel in 1:height + values = [var[y_pixel, x_pixel] for var in vals] + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) + end + end + + normalize_img && GeneticTextures.clean!(img) # Clean the image if requested + + frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" + save(frame_file, map(clamp01nan, img)) + push!(image_files, frame_file) # Append the image file to the list + + elapsed_time = time() - start_time + avg_time_per_frame = elapsed_time / i + remaining_time = avg_time_per_frame * (total_frames - i) + + ProgressMeter.update!(progress, i, desc="Processing Frame $i: Avg time per frame $(round(avg_time_per_frame, digits=2))s, Remaining $(round(remaining_time, digits=2))s") + end + + # Create the gif + println("Creating GIF...") + gif_file = animation_dir * "/animation_$animation_id.gif" + run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps + # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) + # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + + println("Animation saved to: $gif_file") + println("Frames saved to: $animation_dir") + println("Expressions saved to: $expr_file") +end + +Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25); Δx=4, Δy=4)) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# width = height = 128 +# animate_system_2threaded(ds, width, height, 10.0, 0.1; color_expr, complex_expr) + + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +ds = DynamicalSystem3([A, B]) +width = height = 512 +animate_system_2threaded(ds, width, height, 50.0, 0.2; color_expr, complex_expr, normalize_img=true) + + + +# F_A0 = :(0*x) +# F_dA = :(laplacian(B)) + +# F_B0 = :(rand_scalar()*0.3+0.1) +# F_dB = :(A+0) + +# color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) + +# ds = DynamicalSystem3([A, B]) +# width = height = 512 +# animate_system_2threaded(ds, width, height, 50.0, 0.1; color_expr, complex_expr, normalize_img=true) diff --git a/examples/refactor_test_1.jl b/examples/refactor_test_1.jl new file mode 100644 index 0000000..fcd86c3 --- /dev/null +++ b/examples/refactor_test_1.jl @@ -0,0 +1,104 @@ +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +function perlin_color(x, y, vars) + # Assume perlin_2d is correctly set up in `vars` + vars[:perlin](x, y) +end + +# More custom functions here... +custom_operations = Dict( + :safe_divide => safe_divide, + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity), expr.args[2:end]) + if haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + else + return expr + end + else + return expr + end +end + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + + +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0 +) + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => true, + :grad_direction => true +) + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +compiled = compile_expr(expr, custom_operations, primitives_with_arity) +custom_expr = CustomExpr(compiled) + +w = h = 512 +# Generate the image +# @time image = generate_image_refactored(custom_expr, w, h) + +@time image = generate_image_refactored(custom_expr, w, h; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) diff --git a/examples/refactor_test_2.jl b/examples/refactor_test_2.jl new file mode 100644 index 0000000..a210f6b --- /dev/null +++ b/examples/refactor_test_2.jl @@ -0,0 +1,190 @@ +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +function perlin_color(x, y, vars) + # Assume perlin_2d is correctly set up in `vars` + vars[:perlin](x, y) +end + +# More custom functions here... +custom_operations = Dict( + :safe_divide => safe_divide, + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + # Convert each argument, now passing width and height where needed + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height), expr.args[2:end]) + + if haskey(gradient_functions, func) + # Handle special gradient functions + println("Handling gradient function: $func") + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + else + return expr + end + else + return expr + end +end + + +# function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) +# args = expr.args[2:end] # extract arguments from the expression + +# # Convert the expression argument into a function if not already +# expr_func = compile_expr(args[1], custom_operations, primitives_with_arity, gradient_functions, width, height) + +# # Create a new expression for x_grad with the compiled function +# grad_expr = quote +# x_grad($expr_func, vars, width, height) +# end + +# return grad_expr +# end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + println("positional_args: $positional_args, kwargs: $kwargs") + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(positional_args[1], custom_operations, primitives_with_arity, gradient_functions, width, height) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to x_grad, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + + +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => 0 +) + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + # :grad_magnitude => grad_magnitude, # grad_magnitude function reference + # :grad_direction => grad_direction, # grad_direction function reference + :x_grad => x_grad # x_grad function reference +) + + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + # Evaluate function at x + Δx + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + + # Compute the finite difference + grad_x = (x_plus_val - center_val) / Δx_scaled + return grad_x +end + +w = h = 512 + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +expr = :(x_grad(x^2)) +# expr = :(2*x) +compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, w, h) +custom_expr = CustomExpr(compiled) + +# Generate the image +# @time image = generate_image_refactored(custom_expr, w, h) + +@time image = generate_image_refactored(custom_expr, w, h; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) diff --git a/examples/refactor_test_3.jl b/examples/refactor_test_3.jl new file mode 100644 index 0000000..ca5b5cf --- /dev/null +++ b/examples/refactor_test_3.jl @@ -0,0 +1,325 @@ +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +# More custom functions here... +custom_operations = Dict( + :safe_divide => safe_divide, + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + # Convert each argument, now passing width and height where needed + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height), expr.args[2:end]) + + if haskey(gradient_functions, func) + # Handle special gradient functions + println("Handling gradient function: $func") + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + else + return expr + end + else + return expr + end +end + + +# function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) +# args = expr.args[2:end] # extract arguments from the expression + +# # Convert the expression argument into a function if not already +# expr_func = compile_expr(args[1], custom_operations, primitives_with_arity, gradient_functions, width, height) + +# # Create a new expression for x_grad with the compiled function +# grad_expr = quote +# x_grad($expr_func, vars, width, height) +# end + +# return grad_expr +# end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + println("positional_args: $positional_args, kwargs: $kwargs") + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(positional_args[1], custom_operations, primitives_with_arity, gradient_functions, width, height) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to x_grad, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + + +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + # Evaluate function at x + Δx + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + + # Compute the finite difference + grad_x = (x_plus_val - center_val) / Δx_scaled + return grad_x +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Evaluate function at y + Δy + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + + # Compute the finite difference + grad_y = (y_plus_val - center_val) / Δy_scaled + return grad_y +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + min_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + max_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + sum_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + sum_val += expr(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +w = h = 512 + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(sin(100*x)) +compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, w, h) +custom_expr = CustomExpr(compiled) + +# Generate the image +# @time image = generate_image_refactored(custom_expr, w, h) + +@time image = generate_image_refactored(custom_expr, w, h; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) diff --git a/examples/refactor_test_4.jl b/examples/refactor_test_4.jl new file mode 100644 index 0000000..444131d --- /dev/null +++ b/examples/refactor_test_4.jl @@ -0,0 +1,575 @@ +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + # TODO: Should we broadcast everything? Or only certain functions? (for now, broadcast everything) + # return Expr(:call, broadcast(func, args...)) + + # convert func to a broadcasted function + + return Expr(:call, func, args...) + # broadcast_expr = Expr(:call, func, args...) + # return Expr(:., broadcast_expr) + end + elseif isa(expr, Symbol) + println("expr: $expr") + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + println("positional_args: $positional_args, kwargs: $kwargs") + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(positional_args[1], custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + + + +# minus one means that this is a matrix? +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + # Evaluate function at x + Δx + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + + # Compute the finite difference + grad_x = (x_plus_val - center_val) / Δx_scaled + return grad_x +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Evaluate function at y + Δy + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + + # Compute the finite difference + grad_y = (y_plus_val - center_val) / Δy_scaled + return grad_y +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + min_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int + idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + + println("merge: $(merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)))") + max_val = expr(vars) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = expr(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(expr, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + sum_val = expr(vars) # Directly use vars, no need to merge if x, y are already set + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # Evaluate neighborhood + for dx in -Δx:Δx, dy in -Δy:Δy + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + sum_val += expr(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + cval = rand(w, h) + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5, :D => cval) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +w = h = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +expr = :(sin(100*x) + neighbor_ave(D)) +# expr = :(rand_color()) +samplers = Dict() +compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, w, h, samplers) +custom_expr = CustomExpr(compiled) + +# Generate the image +# @time image = generate_image_refactored(custom_expr, w, h) + +@time image = generate_image_refactored(custom_expr, w, h; clean=true) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + + +# struct VariableDynamics3 +# name::Symbol +# F_0::Union{Expr, Symbol, Number, Color} +# δF::Union{Expr, Symbol, Number, Color} + +# function VariableDynamics3(name, F_0, δF) +# return new(name, F_0, δF) +# end +# end + +# name(var::VariableDynamics3) = var.name +# F_0(var::VariableDynamics3) = var.F_0 +# δF(var::VariableDynamics3) = var.δF + +# struct DynamicalSystem3 +# dynamics::Vector{VariableDynamics3} +# end + +# function evolve_system_step_2!(vals, dynamics::DynamicalSystem3, width, height, t, dt, complex_func::Function) +# δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + +# custom_expr = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + +# vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + +# for x_pixel in 1:width +# for y_pixel in 1:height +# x = (x_pixel - 1) / (width - 1) - 0.5 +# y = (y_pixel - 1) / (height - 1) - 0.5 + +# vars[:x] = x +# vars[:y] = y + +# for (i, ds) in enumerate(dynamics) + +# val = dt .* invokelatest(custom_expr[i].func, vars) + +# if val isa Color +# δvals[i][y_pixel, x_pixel] = val +# elseif isreal(val) +# δvals[i][y_pixel, x_pixel] = Color(val, val, val) +# else +# δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) +# end +# end +# end +# end + +# # Update vals +# for (i, ds) in enumerate(dynamics) +# vals[i] += δvals[i] +# end + +# return vals +# end + +# function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) +# color_func = eval(color_expr) +# complex_func = eval(complex_expr) + + +# custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + +# # Initialize each vars' grid using their F_0 expression +# vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] +# t = 0 +# for x_pixel in 1:width +# for y_pixel in 1:height +# x = (x_pixel - 1) / (width - 1) - 0.5 +# y = (y_pixel - 1) / (height - 1) - 0.5 + +# for (i, ds) in enumerate(dynamics) +# vars = Dict(:x => x, :y => y, :t => t) +# val = invokelatest(custom_exprs[i].func, vars) + +# if val isa Color +# vals[i][y_pixel, x_pixel] = val +# else +# vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) +# end +# end +# end +# end + +# # Generate a unique filename +# base_dir = "saves" +# if !isdir(base_dir) +# mkdir(base_dir) +# end + +# animation_id = length(readdir(base_dir)) + 1 +# animation_dir = base_dir * "/animation_$animation_id" + +# # If the directory already exists, increment the id until we find one that doesn't +# while isdir(animation_dir) +# animation_id += 1 +# animation_dir = base_dir * "/animation_$animation_id" +# end + +# mkdir(animation_dir) + +# # Save the system's expressions to a file +# expr_file = animation_dir * "/expressions.txt" +# open(expr_file, "w") do f +# write(f, "Animated using 'animate_system_2' function\n") +# for ds in dynamics +# write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") +# write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") +# end +# write(f, "color_func= $(capture_function(color_expr))\n") +# write(f, "complex_func= $(capture_function(complex_expr))\n") +# write(f, "T= $T\n") +# write(f, "dt= $dt\n") +# write(f, "width= $width\n") +# write(f, "height= $height\n") +# end + +# image_files = [] # Store the names of the image files to use for creating the gif + +# for (i, t) in enumerate(range(0, T, step=dt)) +# vals = evolve_system_step_2!(vals, dynamics, width, height, t, dt, complex_func) # Evolve the system + +# # Create an image from current state +# img = Array{RGB{Float64}, 2}(undef, height, width) +# for x_pixel in 1:width +# for y_pixel in 1:height +# values = [var[y_pixel, x_pixel] for var in vals] + +# img[y_pixel, x_pixel] = +# invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) +# end +# end + +# if normalize_img +# img = clean!(img) +# end + +# frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" +# save(frame_file, map(clamp01nan, img)) + +# # Append the image file to the list +# push!(image_files, frame_file) +# end +# # Create the gif +# gif_file = animation_dir * "/animation_$animation_id.gif" +# run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps +# # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) +# # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + +# println("Animation saved to: $gif_file") +# println("Frames saved to: $animation_dir") +# println("Expressions saved to: $expr_file") +# end + +# Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +# Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25; Δx=4, Δy=4))) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# w = h = 128 +# animate_system_2threaded(ds, w, h, 10.0, 0.1; color_expr, complex_expr) \ No newline at end of file diff --git a/examples/refactor_test_5.jl b/examples/refactor_test_5.jl new file mode 100644 index 0000000..bd9e106 --- /dev/null +++ b/examples/refactor_test_5.jl @@ -0,0 +1,668 @@ +using CoherentNoise: sample, perlin_2d + +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + + caller_func = positional_args[1] + + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +# minus one means that this is a matrix? +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + if idx_x == width + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled))) # Evaluate function at x - Δx + return (center_val - x_minus_val) / Δx + else + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) # Evaluate function at x + Δx + return (x_plus_val - center_val) / Δx_scaled + end +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Compute the finite difference + if idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) # Evaluate function at y - Δy + return (center_val - y_minus) / Δy + else + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) # Evaluate function at y + Δy + return (y_plus_val - center_val) / Δy_scaled + end +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + if Δx == 0 + ∇x = 0 + else + if idx_x > 1 && idx_x < width + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + elseif idx_x == 1 + x_plus = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + ∇x = (x_plus - center_val) / Δx^2 + else # idx_x == width + x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled))) + ∇x = (center_val - x_minus) / Δx^2 + end + end + + if Δy == 0 + ∇y = 0 + else + if idx_y > 1 && idx_y < height + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + elseif idx_y == 1 + y_plus = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + ∇y = (y_plus - center_val) / Δy^2 + else # idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) + ∇y = (center_val - y_minus) / Δy^2 + end + end + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + min_val = func(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = func(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + max_val = func(vars) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + val = func(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + sum_val = func(vars) + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + sum_val += func(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +using Images, Colors + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +using GeneticTextures: Color + +# TODO: please make that these can have any name and just pass them as arguments... +width = height = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +# expr = :(sin(100*x) + neighbor_ave(x)) +# expr = :( + neighbor_max(sin(100*x)/sin(10*y))) +# expr = :(perlin_2d(123, Color(0.4*x, 0.5*x, 0.2*y), Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y) + D)) +# expr = :(Color(0.4*x, 0.5*x, 0.2*y)* Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y)) + +# # expr = :(rand_color()) +# samplers = Dict() +# compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) +# custom_expr = CustomExpr(compiled) + +# # Generate the image +# # @time image = generate_image_refactored(custom_expr, w, h) + +# @time image = generate_image_refactored(custom_expr, width, height; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + +using GeneticTextures: capture_function +struct VariableDynamics3 + name::Symbol + F_0::Union{Expr, Symbol, Number, Color} + δF::Union{Expr, Symbol, Number, Color} + + function VariableDynamics3(name, F_0, δF) + return new(name, F_0, δF) + end +end + +name(var::VariableDynamics3) = var.name +F_0(var::VariableDynamics3) = var.F_0 +δF(var::VariableDynamics3) = var.δF + +struct DynamicalSystem3 + dynamics::Vector{VariableDynamics3} +end + +function evolve_system_step_2!(vals, dynamics::DynamicalSystem3, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + custom_expr = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_expr[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) + color_func = eval(color_expr) + complex_func = eval(complex_expr) + + custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + # Initialize each vars' grid using their F_0 expression + vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + t = 0 + for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + for (i, ds) in enumerate(dynamics) + vars = Dict(:x => x, :y => y, :t => t) + val = invokelatest(custom_exprs[i].func, vars) + + if val isa Color + vals[i][y_pixel, x_pixel] = val + else + vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Generate a unique filename + base_dir = "saves" + if !isdir(base_dir) + mkdir(base_dir) + end + + animation_id = length(readdir(base_dir)) + 1 + animation_dir = base_dir * "/animation_$animation_id" + + # If the directory already exists, increment the id until we find one that doesn't + while isdir(animation_dir) + animation_id += 1 + animation_dir = base_dir * "/animation_$animation_id" + end + + mkdir(animation_dir) + + # Save the system's expressions to a file + expr_file = animation_dir * "/expressions.txt" + open(expr_file, "w") do f + write(f, "Animated using 'animate_system_2' function\n") + for ds in dynamics + write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") + write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") + end + write(f, "color_func= $(capture_function(color_expr))\n") + write(f, "complex_func= $(capture_function(complex_expr))\n") + write(f, "T= $T\n") + write(f, "dt= $dt\n") + write(f, "width= $width\n") + write(f, "height= $height\n") + end + + image_files = [] # Store the names of the image files to use for creating the gif + + for (i, t) in enumerate(range(0, T, step=dt)) + vals = evolve_system_step_2!(vals, dynamics, width, height, t, dt, complex_func) # Evolve the system + + # Create an image from current state + img = Array{RGB{Float64}, 2}(undef, height, width) + for x_pixel in 1:width + for y_pixel in 1:height + values = [var[y_pixel, x_pixel] for var in vals] + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) + end + end + + if normalize_img + img = GeneticTextures.clean!(img) + end + + frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" + save(frame_file, map(clamp01nan, img)) + + # Append the image file to the list + push!(image_files, frame_file) + end + # Create the gif + gif_file = animation_dir * "/animation_$animation_id.gif" + run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps + # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) + # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + + println("Animation saved to: $gif_file") + println("Frames saved to: $animation_dir") + println("Expressions saved to: $expr_file") +end + +Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25); Δx=4, Δy=4)) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# width = height = 128 +# animate_system_2threaded(ds, width, height, 10.0, 0.1; color_expr, complex_expr) + + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +ds = DynamicalSystem3([A, B]) +width = height = 1024 +animate_system_2threaded(ds, width, height, 50.0, 0.2; color_expr, complex_expr, normalize_img=true) \ No newline at end of file diff --git a/examples/refactor_test_6.jl b/examples/refactor_test_6.jl new file mode 100644 index 0000000..b431752 --- /dev/null +++ b/examples/refactor_test_6.jl @@ -0,0 +1,680 @@ +using CoherentNoise: sample, perlin_2d + +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + + caller_func = positional_args[1] + + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +# minus one means that this is a matrix? +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + if idx_x == width + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled))) # Evaluate function at x - Δx + return (center_val - x_minus_val) / Δx + else + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) # Evaluate function at x + Δx + return (x_plus_val - center_val) / Δx_scaled + end +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Compute the finite difference + if idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) # Evaluate function at y - Δy + return (center_val - y_minus) / Δy + else + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) # Evaluate function at y + Δy + return (y_plus_val - center_val) / Δy_scaled + end +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + if Δx == 0 + ∇x = 0 + else + if idx_x > 1 && idx_x < width + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + elseif idx_x == 1 + x_plus = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + ∇x = (x_plus - center_val) / Δx^2 + else # idx_x == width + x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled))) + ∇x = (center_val - x_minus) / Δx^2 + end + end + + if Δy == 0 + ∇y = 0 + else + if idx_y > 1 && idx_y < height + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + elseif idx_y == 1 + y_plus = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + ∇y = (y_plus - center_val) / Δy^2 + else # idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) + ∇y = (center_val - y_minus) / Δy^2 + end + end + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + min_val = func(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = func(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + max_val = func(vars) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + val = func(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + sum_val = func(vars) + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + sum_val += func(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +using Images, Colors + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +using GeneticTextures: Color + +# TODO: please make that these can have any name and just pass them as arguments... +width = height = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +# expr = :(sin(100*x) + neighbor_ave(x)) +# expr = :( + neighbor_max(sin(100*x)/sin(10*y))) +# expr = :(perlin_2d(123, Color(0.4*x, 0.5*x, 0.2*y), Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y) + D)) +# expr = :(Color(0.4*x, 0.5*x, 0.2*y)* Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y)) + +# # expr = :(rand_color()) +# samplers = Dict() +# compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) +# custom_expr = CustomExpr(compiled) + +# # Generate the image +# # @time image = generate_image_refactored(custom_expr, w, h) + +# @time image = generate_image_refactored(custom_expr, width, height; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + +using GeneticTextures: capture_function +struct VariableDynamics3 + name::Symbol + F_0::Union{Expr, Symbol, Number, Color} + δF::Union{Expr, Symbol, Number, Color} + + function VariableDynamics3(name, F_0, δF) + return new(name, F_0, δF) + end +end + +name(var::VariableDynamics3) = var.name +F_0(var::VariableDynamics3) = var.F_0 +δF(var::VariableDynamics3) = var.δF + +struct DynamicalSystem3 + dynamics::Vector{VariableDynamics3} +end + +function evolve_system_step_2!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + + +using ProgressMeter + +function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) + color_func = eval(color_expr) + complex_func = eval(complex_expr) + + custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] # Initialize each vars' grid using their F_0 expression + t = 0 + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + for (i, ds) in enumerate(dynamics) + vars = Dict(:x => x, :y => y, :t => t) + val = invokelatest(custom_exprs[i].func, vars) + + if val isa Color + vals[i][y_pixel, x_pixel] = val + else + vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Generate a unique filename + base_dir = "saves" + if !isdir(base_dir) + mkdir(base_dir) + end + + animation_id = length(readdir(base_dir)) + 1 + animation_dir = base_dir * "/animation_$animation_id" + + # If the directory already exists, increment the id until we find one that doesn't + while isdir(animation_dir) + animation_id += 1 + animation_dir = base_dir * "/animation_$animation_id" + end + + mkdir(animation_dir) + + # Save the system's expressions to a file + expr_file = animation_dir * "/expressions.txt" + open(expr_file, "w") do f + write(f, "Animated using 'animate_system_2' function\n") + for ds in dynamics + write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") + write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") + end + write(f, "color_func= $(capture_function(color_expr))\n") + write(f, "complex_func= $(capture_function(complex_expr))\n") + write(f, "T= $T\n") + write(f, "dt= $dt\n") + write(f, "width= $width\n") + write(f, "height= $height\n") + end + + image_files = [] # Store the names of the image files to use for creating the gif + + # We only need to compile once each expression + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + total_frames = ceil(Int, T / dt) + progress = Progress(total_frames, desc="Initializing everything...", barlen=80) + + # Evolve the system over time + start_time = time() + for (i, t) in enumerate(range(0, T, step=dt)) + vals = evolve_system_step_2!(vals, dynamics, custom_exprs, width, height, t, dt, complex_func) # Evolve the system + + # Create an image from current state + img = Array{RGB{Float64}, 2}(undef, height, width) + for x_pixel in 1:width + for y_pixel in 1:height + values = [var[y_pixel, x_pixel] for var in vals] + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) + end + end + + normalize_img && GeneticTextures.clean!(img) # Clean the image if requested + + frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" + save(frame_file, map(clamp01nan, img)) + push!(image_files, frame_file) # Append the image file to the list + + elapsed_time = time() - start_time + avg_time_per_frame = elapsed_time / i + remaining_time = avg_time_per_frame * (total_frames - i) + + ProgressMeter.update!(progress, i, desc="Processing Frame $i: Avg time per frame $(round(avg_time_per_frame, digits=2))s, Remaining $(round(remaining_time, digits=2))s") + end + + # Create the gif + println("Creating GIF...") + gif_file = animation_dir * "/animation_$animation_id.gif" + run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps + # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) + # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + + println("Animation saved to: $gif_file") + println("Frames saved to: $animation_dir") + println("Expressions saved to: $expr_file") +end + +Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25); Δx=4, Δy=4)) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# width = height = 128 +# animate_system_2threaded(ds, width, height, 10.0, 0.1; color_expr, complex_expr) + + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +ds = DynamicalSystem3([A, B]) +width = height = 512 +animate_system_2threaded(ds, width, height, 50.0, 0.2; color_expr, complex_expr, normalize_img=true) \ No newline at end of file diff --git a/examples/refactor_test_7.jl b/examples/refactor_test_7.jl new file mode 100644 index 0000000..5eb4ec3 --- /dev/null +++ b/examples/refactor_test_7.jl @@ -0,0 +1,779 @@ +using CoherentNoise: sample, perlin_2d + +struct CustomExpr + func::Function +end + +# Define custom operations +function safe_divide(a, b) + isapprox(b, 0) ? 0 : a / b +end + +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + +using Random: seed! + +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end +end + +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) + end +end + +# More custom functions here... +# Is this Dict necessary? I don't think so +custom_operations = Dict( + :safe_divide => safe_divide, + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# Helper to handle conversion from Expr to Julia functions +# Updated function to handle Symbols and other literals correctly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) + + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator + end + sampler = samplers[seed] + + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) + else + return Expr(:call, func, args...) + end + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end + else + return expr + end +end + +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end + +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end + +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later + end + end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) + end + end + + caller_func = positional_args[1] + + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end + + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end + + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end + + return grad_expr +end + +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int]) + else + return expr + end +end + + +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars) -> $expr )) +end + +# minus one means that this is a matrix? +primitives_with_arity = Dict( + :sin => 1, + :cos => 1, + :tan => 1, + :perlin_color => 2, + :safe_divide => 2, + :x => 0, + :y => 0, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => 0 +) + +function x_grad(func, vars, width, height; Δx = 1) + x_val = vars[:x] + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + + # Evaluate function at x + center_val = func(merge(vars, Dict(:x => x_val))) + + if idx_x == width + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled))) # Evaluate function at x - Δx + return (center_val - x_minus_val) / Δx + else + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled))) # Evaluate function at x + Δx + return (x_plus_val - center_val) / Δx_scaled + end +end + +function y_grad(func, vars, width, height; Δy = 1) + y_val = vars[:y] + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Evaluate function at y + center_val = func(merge(vars, Dict(:y => y_val))) + + # Compute the finite difference + if idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) # Evaluate function at y - Δy + return (center_val - y_minus) / Δy + else + y_plus_val = func(merge(vars, Dict(:y => y_val + Δy_scaled))) # Evaluate function at y + Δy + return (y_plus_val - center_val) / Δy_scaled + end +end + +function grad_magnitude(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return sqrt.(∂f_∂x .^ 2 + ∂f_∂y .^ 2) +end + +function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) + ∂f_∂x = x_grad(expr, vars, width, height; Δx = Δx) + ∂f_∂y = y_grad(expr, vars, width, height; Δy = Δy) + return atan.(∂f_∂y, ∂f_∂x) +end + +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + x_val = vars[:x] + y_val = vars[:y] + + Δx_scaled = Δx / (width - 1) # scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # scale Δy to be proportional to the image height + + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + + if Δx == 0 + ∇x = 0 + else + if idx_x > 1 && idx_x < width + x_plus_val = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + elseif idx_x == 1 + x_plus = func(merge(vars, Dict(:x => x_val + Δx_scaled))) + ∇x = (x_plus - center_val) / Δx^2 + else # idx_x == width + x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled))) + ∇x = (center_val - x_minus) / Δx^2 + end + end + + if Δy == 0 + ∇y = 0 + else + if idx_y > 1 && idx_y < height + y_plus_val = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + y_minus_val = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + elseif idx_y == 1 + y_plus = func(merge(vars, Dict(:y => y_val + Δy_scaled))) + ∇y = (y_plus - center_val) / Δy^2 + else # idx_y == height + y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) + ∇y = (center_val - y_minus) / Δy^2 + end + end + + return ∇x + ∇y +end + +# Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + min_val = func(vars) # Directly use vars, no need to merge if x, y are already set + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) + + val = func(temp_vars) + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + max_val = func(vars) + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + val = func(temp_vars) + if val > max_val + max_val = val + end + end + + return max_val +end + +# Return the average value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + # Initialize the center values + x_val = vars[:x] + y_val = vars[:y] + + sum_val = func(vars) + count = 1 + + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) + + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy + + if any([isa(v, Matrix) for v in values(vars)]) + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + + # Filter the iterations that are not in the matrix + range_x = filter(x -> 1 <= idx_x + x <= width, range_x) + range_y = filter(y -> 1 <= idx_y + y <= height, range_y) + end + + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + end + + temp_vars[:x] = (idx_x + dx - 1) / (width - 1) - 0.5 + temp_vars[:y] = (idx_y + dy - 1) / (height - 1) - 0.5 + + sum_val += func(temp_vars) + count += 1 + end + + return sum_val / count +end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_max => neighbor_max, + :neighbor_ave => neighbor_ave +) + +perlin_functions = Dict( + :perlin_2d => 3, + :perlin_color => 4, +) + +using Images, Colors + +function generate_image_refactored(custom_expr::CustomExpr, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + for y in 1:height + for x in 1:width + vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) + # Add more variables if needed, e.g., :t => time + rgb = custom_expr.func(vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from custom_eval: $(typeof(rgb))") + end + end + end + + clean && GeneticTextures.clean!(img) + return img +end + +using GeneticTextures: Color + +# TODO: please make that these can have any name and just pass them as arguments... +width = height = 512 + + + +# Example of using this system +# Assuming custom_operations and primitives_with_arity are already defined as shown +# expr = :(safe_divide(sin(100*x), sin(y))) # Example expression +# expr = :(laplacian(y^3)) +# expr = :(neighbor_max(sin(100*x*y); Δx=4, Δy=1)) +# expr = :(perlin_color(321, sin(y * Color(0.2, 0.4, 0.8))*10, x*8, x)) +# expr = :(sin(100*x) + neighbor_ave(x)) +# expr = :( + neighbor_max(sin(100*x)/sin(10*y))) +# expr = :(perlin_2d(123, Color(0.4*x, 0.5*x, 0.2*y), Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y) + D)) +# expr = :(Color(0.4*x, 0.5*x, 0.2*y)* Color(0.4*x, 0.5*x, 0.2*y)*sin(100*x)/sin(10*y)) + +# # expr = :(rand_color()) +# samplers = Dict() +# compiled = compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) +# custom_expr = CustomExpr(compiled) + +# # Generate the image +# # @time image = generate_image_refactored(custom_expr, w, h) + +# @time image = generate_image_refactored(custom_expr, width, height; clean=false) + +# @time image = generate_image(GeneticTextures.CustomExpr(expr), w, h) + +using GeneticTextures: capture_function +struct VariableDynamics3 + name::Symbol + F_0::Union{Expr, Symbol, Number, Color} + δF::Union{Expr, Symbol, Number, Color} + + function VariableDynamics3(name, F_0, δF) + return new(name, F_0, δF) + end +end + +name(var::VariableDynamics3) = var.name +F_0(var::VariableDynamics3) = var.F_0 +δF(var::VariableDynamics3) = var.δF + +struct DynamicalSystem3 + dynamics::Vector{VariableDynamics3} +end + +function evolve_system_step_threaded!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + vars[:x] = x + vars[:y] = y + + for (i, ds) in enumerate(dynamics) + + val = dt .* invokelatest(custom_exprs[i].func, vars) + + if val isa Color + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) + else + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Update vals + for (i, ds) in enumerate(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +function vectorize_color_decision!(results, δvals, complex_func, i) + # Determine the type of each element in results + is_color = [r isa Color for r in results] + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] + + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] + + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_system_step_vectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # Loop through each dynamical system + for (i, ds) in enumerate(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + +# Helper function to create meshgrid equivalent to MATLAB's +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end + +function evolve_system_step_threadandvectorized!(vals, dynamics::DynamicalSystem3, custom_exprs, width, height, t, dt, complex_func::Function) + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + n_blocks = Threads.nthreads() * 4 + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] + + + # Vectorized computation within each block + for (i, ds) in enumerate(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(custom_exprs[i].func, merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) + end + + # Update the global δvals with the block's results + for (i, ds) in enumerate(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end + + # Update vals by adding δvals + for (i, ds) in enumerate(dynamics) + vals[i] .+= δvals[i] + end + + return vals +end + + +using ProgressMeter + +function animate_system_2threaded(dynamics::DynamicalSystem3, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) + color_func = eval(color_expr) + complex_func = eval(complex_expr) + + custom_exprs = [CustomExpr(compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] # Initialize each vars' grid using their F_0 expression + t = 0 + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + for (i, ds) in enumerate(dynamics) + vars = Dict(:x => x, :y => y, :t => t) + val = invokelatest(custom_exprs[i].func, vars) + + if val isa Color + vals[i][y_pixel, x_pixel] = val + else + vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + end + end + end + end + + # Generate a unique filename + base_dir = "saves" + if !isdir(base_dir) + mkdir(base_dir) + end + + animation_id = length(readdir(base_dir)) + 1 + animation_dir = base_dir * "/animation_$animation_id" + + # If the directory already exists, increment the id until we find one that doesn't + while isdir(animation_dir) + animation_id += 1 + animation_dir = base_dir * "/animation_$animation_id" + end + + mkdir(animation_dir) + + # Save the system's expressions to a file + expr_file = animation_dir * "/expressions.txt" + open(expr_file, "w") do f + write(f, "Animated using 'animate_system_2' function\n") + for ds in dynamics + write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") + write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") + end + write(f, "color_func= $(capture_function(color_expr))\n") + write(f, "complex_func= $(capture_function(complex_expr))\n") + write(f, "T= $T\n") + write(f, "dt= $dt\n") + write(f, "width= $width\n") + write(f, "height= $height\n") + end + + image_files = [] # Store the names of the image files to use for creating the gif + + # We only need to compile once each expression + custom_exprs = [CustomExpr(compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height, Dict())) for ds in dynamics] + + total_frames = ceil(Int, T / dt) + progress = Progress(total_frames, desc="Initializing everything...", barlen=80) + + # Evolve the system over time + start_time = time() + for (i, t) in enumerate(range(0, T, step=dt)) + vals = evolve_system_step_threaded!(vals, dynamics, custom_exprs, width, height, t, dt, complex_func) # Evolve the system + + # Create an image from current state + img = Array{RGB{Float64}, 2}(undef, height, width) + for x_pixel in 1:width + for y_pixel in 1:height + values = [var[y_pixel, x_pixel] for var in vals] + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) + end + end + + normalize_img && GeneticTextures.clean!(img) # Clean the image if requested + + frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" + save(frame_file, map(clamp01nan, img)) + push!(image_files, frame_file) # Append the image file to the list + + elapsed_time = time() - start_time + avg_time_per_frame = elapsed_time / i + remaining_time = avg_time_per_frame * (total_frames - i) + + ProgressMeter.update!(progress, i, desc="Processing Frame $i: Avg time per frame $(round(avg_time_per_frame, digits=2))s, Remaining $(round(remaining_time, digits=2))s") + end + + # Create the gif + println("Creating GIF...") + gif_file = animation_dir * "/animation_$animation_id.gif" + run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps + # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) + # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal + + println("Animation saved to: $gif_file") + println("Frames saved to: $animation_dir") + println("Expressions saved to: $expr_file") +end + +Base.length(ds::DynamicalSystem3) = length(ds.dynamics) +Base.iterate(ds::DynamicalSystem3, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) + + +# F_A0 = :(y + 0) +# F_dA = :(neighbor_max(neighbor_max(C; Δx=4, Δy=4); Δx=4, Δy=4)) + +# F_B0 = :(1.0+0*x) +# F_dB = :(x_grad(C)) + +# F_C = :((1 - rand_scalar()*1.68+0.12) + y) +# F_dC = :(neighbor_ave(grad_direction(B * 0.25); Δx=4, Δy=4)) + + +# color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +# complex_expr = :((c) -> real(c)) + +# A = VariableDynamics3(:A, F_A0, F_dA) +# B = VariableDynamics3(:B, F_B0, F_dB) +# C = VariableDynamics3(:C, F_C, F_dC) + +# ds = DynamicalSystem3([A, B, C]) +# width = height = 128 +# animate_system_2threaded(ds, width, height, 10.0, 0.1; color_expr, complex_expr) + + +F_A0 = :(-1*rand_scalar()*1.27-0.06) +F_dA = :(neighbor_min(A; Δx=2, Δy=2)) + +F_B0 = :(-0.032+0) +F_dB = :(laplacian(A*4.99)) + +color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics3(:A, F_A0, F_dA) +B = VariableDynamics3(:B, F_B0, F_dB) + +ds = DynamicalSystem3([A, B]) +width = height = 512 +animate_system_2threaded(ds, width, height, 50.0, 0.2; color_expr, complex_expr, normalize_img=true) \ No newline at end of file diff --git a/examples/testing22.jl b/examples/testing22.jl new file mode 100644 index 0000000..0d8cf8a --- /dev/null +++ b/examples/testing22.jl @@ -0,0 +1,76 @@ +using GeneticTextures + +dt = 1. + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +# F_dA = :(neighbor_min(A; Δx=4, Δy=4)^2 + A^2 + Complex(0.74543,0.11301)) +F_dA = :(A^2 + Complex(0.3,-0.01) - A/$dt) + +F_dA = :(ifs(abs(A) > 3, A^2 + Complex(0.3,-0.01) - A/$dt, A^3 - A/$dt)) +# F_dA = :(A*x-1*A/$dt) + +F_B0 = :(0) # C will be used as a counter +F_dB = :(ifs(abs(A) > 2, -1/$dt, 1/$dt)) # stop iterating when A escapes + +F_C0 = :(Complex(4*x, 4*y)) +F_dC = :(ifs(abs(C) > 2, 0, C^2 + Complex(0.4,0.02) - C/$dt)) +F_dA = :(ifs(abs(A) > 2, 0, A^2 + C + Complex(0.3,-0.01) - A/$dt)) + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + continuous_potential(k, z) = abs(z) > 2 ? 1 + k - log(log(abs(z))) / log(2) : k + + tester(k, z) = real(z) + + orbit_trap_modifier(k, z, trap_radius, max_iter, c) = begin + trapped = false + z_trap = z + for i in 1:k + z_trap = z_trap^2 + c + if abs(z_trap) <= trap_radius + trapped = true + break + end + end + + if trapped + return abs(z_trap) / trap_radius + else + return k / max_iter + end + end + + # (a, b, c) -> begin + # # Include orbit trap modifier + # trap_radius = 1 # Set this to whatever trap radius you want + # max_iter = 1 # Set this to your maximum number of iterations + # orbit_value = orbit_trap_modifier(c.r, a.r, trap_radius, max_iter, Complex(0.74543, 0.11301)) # c is your Julia constant + # end + + (a, b,c) -> begin + # Calculate the modified iteration count + # modified_k = orbit_trap_modifier(b.r, a.r, 10, 1000, Complex(0.74543, 0.11301)) + modified_k = continuous_potential(b.r, c.r) + end +end) + +complex_expr = :((c) -> (c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B,C]) + + +w = h = 32 +GeneticTextures.animate_system_3(ds, w, h, 200.0, dt; +normalize_img = true, +cmap = :curl, +value_expr, +complex_expr) diff --git a/examples/testluxor20.jl b/examples/testluxor20.jl new file mode 100644 index 0000000..e4e8fe2 --- /dev/null +++ b/examples/testluxor20.jl @@ -0,0 +1,76 @@ +using GeneticTextures + +dt = 1. + +F_A0 = :(Complex(4*x, 4*y)) +# F_dA = :(A^2 + Complex(0.355, 0.355)) +# F_dA = :(A^2 + Complex(0.74543, 0.11301)) +# F_dA = :(neighbor_min(A; Δx=4, Δy=4)^2 + A^2 + Complex(0.74543,0.11301)) +F_dA = :(A^2 + Complex(0.3,-0.01) - A/$dt) + +F_dA = :(ifs(abs(A) > 2, 0, A^2 + Complex(0.3,-0.01) - A/$dt)) +# F_dA = :(A*x-1*A/$dt) + +F_B0 = :(0) # C will be used as a counter +F_dB = :(ifs(abs(A) > 2, 0/$dt, 1/$dt)) # stop iterating when A escapes + +F_C0 = :(0) +F_dC = :(C + 1) + + +# Define the value expression +value_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + continuous_potential(k, z) = abs(z) > 2 ? 1 + k - log(log(abs(z))) / log(2) : k + + tester(k, z) = real(z) + + orbit_trap_modifier(k, z, trap_radius, max_iter, c) = begin + trapped = false + z_trap = z + for i in 1:k + z_trap = z_trap^2 + c + if abs(z_trap) <= trap_radius + trapped = true + break + end + end + + if trapped + return abs(z_trap) / trap_radius + else + return k / max_iter + end + end + + # (a, b, c) -> begin + # # Include orbit trap modifier + # trap_radius = 1 # Set this to whatever trap radius you want + # max_iter = 1 # Set this to your maximum number of iterations + # orbit_value = orbit_trap_modifier(c.r, a.r, trap_radius, max_iter, Complex(0.74543, 0.11301)) # c is your Julia constant + # end + + (a, b) -> begin + # Calculate the modified iteration count + # modified_k = orbit_trap_modifier(b.r, a.r, 10, 1000, Complex(0.74543, 0.11301)) + modified_k = angle_modifier(b.r, a.r) + end +end) + +complex_expr = :((c) -> (c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C0, F_dC) +ds = DynamicalSystem([A,B]) + + +w = h = 128 +GeneticTextures.animate_system_4(ds, w, h, 200.0, dt; +normalize_img = true, +cmap = :curl, +value_expr, +complex_expr) diff --git a/examples/v2_animation_1.jl b/examples/v2_animation_1.jl new file mode 100644 index 0000000..0430d51 --- /dev/null +++ b/examples/v2_animation_1.jl @@ -0,0 +1,193 @@ +begin + using GeneticTextures + delta = 2 + F_A0 = :(real(x)+imag(y)) + F_dA = :((min(A, 1.0) / A) -0.7 * 3.5^(max(A, (0.2 -0.12*im)))) + #color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) + #color_expr = :((a, b) -> RGB((x -> (x > 0.) ? x*0.001 : -x*0.001).([b.r, b.g,b.b])...)) + color_expr = :((b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + #color_expr = :((a, b, c) -> RGB((x -> (x > 0.) ? log(x) : -log(-x)).([c.r, c.g,c.b])...)) + #color_expr = :((a, b) -> RGB(clamp(0.002*b.r, 0, 1), clamp(0.002*b.g, 0, 1), clamp(0.002*b.b, 0, 1))) + complex_expr = :((c) -> imag(c)) + + A = VariableDynamics(:A, F_A0, F_dA) + + ds = DynamicalSystem([A]) + width = height = 32 + animate_system(ds, width, height, 50.0, 0.12; color_expr, complex_expr, normalize_img=true, renderer=:basic) + end + +begin + using GeneticTextures + delta = 2 + F_A0 = :(Complex(4*x, 4*y)) + F_dA = :(A^2 + Complex(0.355, 0.355)) + + F_B0 = :(0.0+0.0im) + F_dB = :(ifs(abs(A) > 2, B + 0.1, B)) + + color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + complex_expr = :((c) -> real(c)) + + A = VariableDynamics(:A, F_A0, F_dA) + B = VariableDynamics(:B, F_B0, F_dB) + ds = DynamicalSystem([A,B]) + + #color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) + #color_expr = :((a, b) -> RGB((x -> (x > 0.) ? x*0.001 : -x*0.001).([b.r, b.g,b.b])...)) + color_expr = :((a, b) -> RGB(abs(b.r), abs(b.g), abs(b.b))) + #color_expr = :((a, b, c) -> RGB((x -> (x > 0.) ? log(x) : -log(-x)).([c.r, c.g,c.b])...)) + #color_expr = :((a, b) -> RGB(clamp(0.002*b.r, 0, 1), clamp(0.002*b.g, 0, 1), clamp(0.002*b.b, 0, 1))) + complex_expr = :((c) -> imag(c)) + + A = VariableDynamics(:A, F_A0, F_dA) + B = VariableDynamics(:B, F_B0, F_dB) + ds = DynamicalSystem([A, B]) + width = height = 32 + animate_system(ds, width, height, 50.0, 0.12; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end + +begin + F_A0 = :(Complex(4*x, y)) + F_dA = :(A^2 + Complex(0.74543, 0.11301)) + + F_B0 = :(0.0+0.0) + F_dB = :(ifs(abs(A) > 2, B + 1, B)) + + F_C0 = :(0.0+0.0) # C will be used as a counter + F_dC = :(C + 1) + + + # Define the value expression + color_expr = :(begin + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + (a, b, c) -> begin + # Calculate the modified iteration count + modified_k = angle_modifier(c.r, a.r) + end + end) + + complex_expr = :((c) -> real(c)) + + A = VariableDynamics(:A, F_A0, F_dA) + B = VariableDynamics(:B, F_B0, F_dB) + C = VariableDynamics(:C, F_C0, F_dC) + ds = DynamicalSystem([A,B,C]) + + width = height = 32 + animate_system(ds, width, height, 50.0, 0.01; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end + +begin + using GeneticTextures +F_A0 = :(y+0.0) +F_dA = :(neighbor_max(neighbor_max(C; Δx=2, Δy=2); Δx=2, Δy=2)) + +F_B0 = :(1.0+0.0) +F_dB = :(x_grad(C)) + +F_C = :((1 - rand_scalar()*1.68+0.12) + y) +F_dC = :(neighbor_ave(grad_direction(B * 0.25; Δx=2, Δy=2))) + + +color_expr = :((a, b, c) -> RGB(abs(c.r), abs(c.g), abs(c.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +width = height = 64 +animate_system(ds, width, height, 10.0, 0.1; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end + +begin + dt = 0.1 + A0 = :(Complex(x, y)+0.) + dA = :(ifs(abs(A) > 2, A, A^2 + Complex(0.2, 0.4) - A/$dt)) + + # B will count the iterations + B0 = :(0.0+0.0) + dB = :(ifs(abs(A) > 2, B - 1, B + 1) - B/$dt) + + A = VariableDynamics(:A, A0, dA) + B = VariableDynamics(:B, B0, dB) + ds = DynamicalSystem([A, B]) + + # color_expr = :((a) -> RGB(a.r, a.g, a.b)) + + color_expr = :(begin + + smooth_modifier(k, z) = abs(z) > 2 ? k - log2(log2(abs(z))) : k + + angle_modifier(k, z) = abs(z) > 2 ? angle(z)/2 * pi : k + + f_ = smooth_modifier + complex_f = abs + (a, b) -> begin + # Calculate the modified iteration count + modified_k = RGB(complex_f(f_(b.r, a.r)), complex_f(f_(b.g, a.g)), complex_f(f_(b.b, a.b))) + end +end) + complex_expr = :((c) -> abs(c)) + + width = height = 256 + animate_system(ds, width, height, 50.0, dt; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end + +using GeneticTextures +begin +dt = 1.0 +maxiter = 100 +# Julia set constant parameter +c = Complex(0.8, -0.156) + +# Initial conditions and evolution rules +A0 = :(Complex((x) * 4, (y) * 4)) +dA = :(ifs(abs(A) > 2.0, A, A^2 + $c)/$dt - A / $dt) + +B0 = :(0.0+0.0) +dB = :(ifs(abs(A) > 2.0, B - 1, B + 1)/$dt - B / $dt) + +A = VariableDynamics(:A, A0, dA) +B = VariableDynamics(:B, B0, dB) +ds = DynamicalSystem([A, B]) + +# Color expression to visualize the fractal +color_expr = :(begin + function smooth_modifier(k, z) + return abs(z) > 2 ? k - log2(log2(abs(z))) : k + end + + function angle_modifier(k, z) + if abs(z) > 2 + return angle(z) / (2 * pi) + else + return k + end + end + + function continuous_potential_modifier(k, z) + if abs(z) > 2 + return 1 + k - log(log(abs(z))) / log(2) + else + return k + end + end + + (a, b) -> begin + k = continuous_potential_modifier(a.r, b.r) + return k + end +end) + +complex_expr = :((c) -> abs(c)) + +# Set up and run the animation +width = height = 256 +animate_system(ds, width, height, 50.0, dt; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end diff --git a/examples/v2_animation_2.jl b/examples/v2_animation_2.jl new file mode 100644 index 0000000..a3252b1 --- /dev/null +++ b/examples/v2_animation_2.jl @@ -0,0 +1,52 @@ +using GeneticTextures +begin +dt = 1.0 +maxiter = 100 +# Julia set constant parameter +c = Complex(0.3, -0.01) + +# Initial conditions and evolution rules +A0 = :(Complex((x) * 4, (y) * 4)) +dA = :(ifs(abs(A) > 2.0, A, A^2 + $c)/$dt - A / $dt) + +B0 = :(0.0+0.0) +dB = :(ifs(abs(A) > 2.0, - 1, $maxiter)/$dt - B / $dt) + +A = VariableDynamics(:A, A0, dA) +B = VariableDynamics(:B, B0, dB) +ds = DynamicalSystem([A, B]) + +# Color expression to visualize the fractal +color_expr = :(begin + function smooth_modifier(k, z) + return abs(z) > 2 ? k - log2(log2(abs(z))) : k + end + + function angle_modifier(k, z) + if abs(z) > 2 + return angle(z) / (2 * pi) + else + return k + end + end + + function continuous_potential_modifier(k, z) + if abs(z) > 2 + return 1 + k - log(log(abs(z))) / log(2) + else + return k + end + end + + (a, b) -> begin + k = smooth_modifier(b.r, a.r) + return k + end +end) + +complex_expr = :((c) -> abs(c)) + +# Set up and run the animation +width = height = 256 +animate_system(ds, width, height, 50.0, dt; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end \ No newline at end of file diff --git a/examples/v2_animation_3.jl b/examples/v2_animation_3.jl new file mode 100644 index 0000000..25099c5 --- /dev/null +++ b/examples/v2_animation_3.jl @@ -0,0 +1,20 @@ +using GeneticTextures +begin +dt = 1.0 + +F_A0 = :(1.0+perlin_2d(123, 10*x, 10*y)) +F_dA = :(neighbor_min_radius(A; Δr=2) - A) +F_B0 = :(0.1 * cos(2π * x)) +F_dB = :(laplacian(B*3.5 - A*2.0)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A, B]) + +color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> abs(c)) + +# Set up and run the animation +width = height = 256 +animate_system(ds, width, height, 50.0, dt; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end \ No newline at end of file diff --git a/examples/v2_animation_4.jl b/examples/v2_animation_4.jl new file mode 100644 index 0000000..9f6f9e3 --- /dev/null +++ b/examples/v2_animation_4.jl @@ -0,0 +1,23 @@ +using GeneticTextures +delta = 2 +F_A0 = :((y+0.5)*1+0.0) +F_dA = :(neighbor_max_radius(neighbor_max_radius(C; Δr=$delta); Δr=$delta)) + +F_B0 = :(0.0+0.0) +F_dB = :(x_grad(C)+0.) + +F_C = :((-rand_scalar()*1.68-0.12) + y) +F_dC = :(neighbor_ave_radius(grad_direction(B*0.25); Δr=$delta)) + + +color_expr = :((a, b, c) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +#color_expr = :((a, b, c) -> RGB(clamp(c.r, 0., 1.0), clamp(c.g, 0.0, 1.0), clamp(c.b,0.0,1.0))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +width = height = 256 +animate_system(ds, width, height, 200.0, 4.; color_expr, complex_expr, normalize_img=true, renderer=:basic, adjust_brighness=false) diff --git a/examples/v2_animation_5.jl b/examples/v2_animation_5.jl new file mode 100644 index 0000000..17ecaaf --- /dev/null +++ b/examples/v2_animation_5.jl @@ -0,0 +1,18 @@ +using GeneticTextures + +# Let's create a reaction-diffusion system +F_A0 = :(rand_scalar() + 0.0) +F_dA = :(laplacian(B) + 0.1) + +F_B0 = :(rand_scalar() + 0.0) +F_dB = :(laplacian(A) + 0.1) + +color_expr = :((a, b) -> RGB(abs(a.r), abs(b.g), abs(a.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A, B]) +width = height = 256 +animate_system(ds, width, height, 80.0, 0.001; color_expr, complex_expr, normalize_img=true, renderer=:basic, adjust_brighness=false) diff --git a/examples/v2_animation_6.jl b/examples/v2_animation_6.jl new file mode 100644 index 0000000..327dc46 --- /dev/null +++ b/examples/v2_animation_6.jl @@ -0,0 +1,15 @@ +using GeneticTextures + +# Let's create a spiral wave +F_A0 = :(Complex(4*x, 4*y) + 0.0) +F_dA = :(min(abs(A), 1.0)/A - 0.7*(max(A, Complex(0.2, -0.12))^3.5)) + +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +# B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A]) +width = height = 128 +animate_system(ds, width, height, 80.0, 1.0; color_expr, complex_expr, normalize_img=true, renderer=:basic, adjust_brighness=false) diff --git a/examples/v2_animation_7.jl b/examples/v2_animation_7.jl new file mode 100644 index 0000000..bb3f790 --- /dev/null +++ b/examples/v2_animation_7.jl @@ -0,0 +1,23 @@ +using GeneticTextures +delta = 2 +F_A0 = :((y+0.0)*1+0.0) +F_dA = :(neighbor_max_radius(neighbor_max_radius(C; Δr=$delta); Δr=$delta)) + +F_B0 = :(0.0+0.0) +F_dB = :(x_grad(C)+0.) + +F_C = :((-rand_scalar()*1.68-0.12) + y) +F_dC = :(neighbor_ave_radius(grad_direction(B*0.25); Δr=$delta)) + + +color_expr = :((a, b, c) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +#color_expr = :((a, b, c) -> RGB(clamp(c.r, 0., 1.0), clamp(c.g, 0.0, 1.0), clamp(c.b,0.0,1.0))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +C = VariableDynamics(:C, F_C, F_dC) + +ds = DynamicalSystem([A, B, C]) +width = height = 128 +animate_system(ds, width, height, 200.0, 4.; color_expr, complex_expr, normalize_img=false, renderer=:basic, adjust_brighness=false) diff --git a/examples/v2_animation_8.jl b/examples/v2_animation_8.jl new file mode 100644 index 0000000..1a05371 --- /dev/null +++ b/examples/v2_animation_8.jl @@ -0,0 +1,15 @@ +using GeneticTextures + +# Let's create a spiral wave +F_A0 = :(Complex(rand_scalar(), rand_scalar()) + 0.0) +F_dA = :(laplacian(A) + grad_direction(abs(A))) + +color_expr = :((a) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> real(c)) + +A = VariableDynamics(:A, F_A0, F_dA) +# B = VariableDynamics(:B, F_B0, F_dB) + +ds = DynamicalSystem([A]) +width = height = 128 +animate_system(ds, width, height, 80.0, 0.1; color_expr, complex_expr, normalize_img=true, renderer=:basic, adjust_brighness=false) diff --git a/examples/v2_animation_simple.jl b/examples/v2_animation_simple.jl new file mode 100644 index 0000000..28e8053 --- /dev/null +++ b/examples/v2_animation_simple.jl @@ -0,0 +1,20 @@ +using GeneticTextures +begin +dt = 1.0 + +F_A0 = :(1.0+perlin_2d(123, 10*x, 10*y)) +F_dA = :(-1*A) +F_B0 = :(0.1 * cos(2π * x)) +F_dB = :(laplacian(B*3.5 - A*2.0)) + +A = VariableDynamics(:A, F_A0, F_dA) +B = VariableDynamics(:B, F_B0, F_dB) +ds = DynamicalSystem([A, B]) + +color_expr = :((a, b) -> RGB(abs(a.r), abs(a.g), abs(a.b))) +complex_expr = :((c) -> abs(c)) + +# Set up and run the animation +width = height = 256 +animate_system(ds, width, height, 50.0, dt; color_expr, complex_expr, normalize_img=true, renderer=:basic) +end \ No newline at end of file diff --git a/src/Color.jl b/src/Color.jl index bd89635..b0de650 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -1,5 +1,7 @@ +import Base: sin, cos, tan, +, -, *, /, ^, sqrt, exp, log, abs, atan, asin, acos, sinh, cosh, tanh, sech, csch, coth, asec, acsc, acot, sec, csc, cot, mod, rem, fld, cld, ceil, floor, round, max, min using Base using Base.Broadcast: BroadcastStyle, DefaultArrayStyle, broadcasted +using Colors # TODO: Consider changing the name of Color, since it can conflict with Images.jl and Colors.jl mutable struct Color{T<:Number} <: AbstractArray{T, 1} @@ -13,6 +15,8 @@ function Color(v::AbstractVector{T}) where {T<:Number} return Color(v[1], v[2], v[3]) end +Color(n::Number) = Color(n, n, n) + red(c::Color) = c.r green(c::Color) = c.g blue(c::Color) = c.b @@ -93,7 +97,37 @@ function Base.show(io::IO, c::Color) print(io, "Color(", round(c.r, digits=2), ", ", round(c.g, digits=2), ", ", round(c.b, digits=2), ")") end -using Colors -Colors.RGB(c::GeneticTextures.Color) = RGB(c.r, c.g, c.b) +Colors.RGB(c::Color) = RGB(c.r, c.g, c.b) + +Color(val::Colors.RGB) = Color(val.r, val.g, val.b) +convert(::Type{Color}, val::Float64) = Color(val, val, val) +# Color(val::Colors.RGB{N0f8}) = Color(Float64(val.r), Float64(val.g), Float64(val.b)) + +unary_functions = [sin, cos, tan, sqrt, exp, log, asin, acos, atan, sinh, cosh, tanh, sech, csch, coth, asec, acsc, acot, sec, csc, cot, mod, rem, fld, cld, ceil, floor, round] +binary_functions = [+, -, *, /, ^, atan, mod, rem, fld, cld, ceil, floor, round, max, min] + +# Automatically define methods +for func in unary_functions + func = Symbol(func) + @eval begin + ($func)(c::Color) = Base.broadcast($func, c) + end +end + +for func in binary_functions + func = Symbol(func) # Get the function name symbol + + @eval begin + ($func)(c1::Color, c2::Color) = Base.broadcast($func, c1, c2) + ($func)(c::Color, x::Number) = Base.broadcast($func, c, x) + ($func)(x::Number, c::Color) = Base.broadcast($func, x, c) + end +end + +Base.isless(x::Number, y::Color) = isless(x, sum([y.r, y.g, y.b])/3.) +Base.isless(x::Color, y::Number) = isless(sum([x.r, x.g, x.b])/3., y) +Base.isless(x::Color, y::Color) = isless(sum([x.r, x.g, x.b])/3., sum([y.r, y.g, y.b])/3.) -Color(val::Colors.RGB) = Color(val.r, val.g, val.b) \ No newline at end of file +Base.isequal(x::Color, y::Color) = isequal(x.r, y.r) && isequal(x.g, y.g) && isequal(x.b, y.b) +Base.isequal(x::Color, y::Number) = isequal(x.r, y) && isequal(x.g, y) && isequal(x.b, y) +Base.isequal(x::Number, y::Color) = isequal(x, y.r) && isequal(x, y.g) && isequal(x, y.b) \ No newline at end of file diff --git a/src/DynamicalSystems.jl b/src/DynamicalSystems.jl index 58f0646..b5e40fd 100644 --- a/src/DynamicalSystems.jl +++ b/src/DynamicalSystems.jl @@ -1,15 +1,20 @@ using FileIO using Images using Colors -using Base: invokelatest +using ProgressMeter struct VariableDynamics name::Symbol - F_0::Union{CustomExpr, Symbol, Number, Color} - δF::Union{CustomExpr, Symbol, Number, Color} + F_0::Union{GeneticExpr, Symbol, Number, Matrix{RGB{N0f8}}, Color} + δF::Union{GeneticExpr, Symbol, Number, Color} + image::Union{Matrix{RGB{N0f8}}, Nothing} function VariableDynamics(name, F_0, δF) - return new(name, CustomExpr(F_0), CustomExpr(δF)) + if F_0 isa Matrix{RGB{N0f8}} + return new(name, GeneticExpr(:(0.0+0.0)), GeneticExpr(δF), F_0) + else + return new(name, GeneticExpr(F_0), GeneticExpr(δF), nothing) + end end end @@ -24,148 +29,172 @@ end Base.length(ds::DynamicalSystem) = length(ds.dynamics) Base.iterate(ds::DynamicalSystem, state = 1) = state > length(ds.dynamics) ? nothing : (ds.dynamics[state], state + 1) -function evolve_system(ds::DynamicalSystem, width, height, T, dt) - img = Array{RGB{Float64}, 2}(undef, height, width) - - A = zeros(height, width) - B = zeros(height, width) - - # Initialize A and B using F_A and F_B - for x_pixel in 1:width - for y_pixel in 1:height - x = (x_pixel - 1) / (width - 1) - 0.5 - y = (y_pixel - 1) / (height - 1) - 0.5 - - A[y_pixel, x_pixel] = custom_eval(ds.F_A0, Dict(:x => x, :y => y), width, height) - B[y_pixel, x_pixel] = custom_eval(ds.F_B0, Dict(:x => x, :y => y), width, height) - end +function evolve_system!(vals, dynamics::DynamicalSystem, genetic_funcs, possible_types, w, h, t, dt, complex_func::Function; renderer = :threaded, kwargs...) + # TODO: Find a better way to pass these arguments to the function + global width = w + global height = h + + if renderer == :basic + return evolve_system_basic!(vals, dynamics, genetic_funcs, possible_types, width, height, t, dt, complex_func) + elseif renderer == :threaded + return evolve_system_threaded!(vals, dynamics, genetic_funcs, width, height, t, dt, complex_func) + elseif renderer == :vectorized + return evolve_system_vectorized!(vals, dynamics, genetic_funcs, width, height, t, dt, complex_func) + elseif renderer == :threadandvectorized + return evolve_system_vectorized_threaded!(vals, dynamics, genetic_funcs, width, height, t, dt, complex_func; kwargs...) + else + error("Invalid renderer: $renderer") end +end - # Time evolution - for t in range(0, T, step=dt) - dA = zeros(height, width) - dB = zeros(height, width) - for x_pixel in 1:width - for y_pixel in 1:height - x = (x_pixel - 1) / (width - 1) - 0.5 - y = (y_pixel - 1) / (height - 1) - 0.5 - - dA[y_pixel, x_pixel] = dt * custom_eval(ds.F_dA, Dict(:x => x, :y => y, :A => A, :B => B), width, height) - dB[y_pixel, x_pixel] = dt * custom_eval(ds.F_dB, Dict(:x => x, :y => y, :A => A, :B => B), width, height) - end - end +contains_color_code(ge::GeneticExpr) = contains_color_code(ge.expr) - # Update A and B - A += dA - B += dB - end +# Check method's source code for color-specific logic (this is just a placeholder) +contains_color_code(e::Expr) = occursin("Color(", string(e)) - # Create final image - for x_pixel in 1:width - for y_pixel in 1:height - r = clamp(A[y_pixel, x_pixel], 0.0, 1.0) - g = clamp(B[y_pixel, x_pixel], 0.0, 1.0) - img[y_pixel, x_pixel] = RGB(r, g, 0.0) # we set blue = 0 for simplicity - end - end +contains_complex_code(ge::GeneticExpr) = contains_complex_code(ge.expr) - img -end +# Check if the expression involves Complex number logic +# TODO: This will have problems for functions that f(Real) -> Complex ... RETHINK this problem +# maybe in this case we can force the functions to behave like f(Real) -> complex_func(Complex) +contains_complex_code(e::Expr) = occursin("Complex(", string(e)) -function evolve_system_step!(vars, dynamics::DynamicalSystem, width, height, t, dt) - δvars = [zeros(height, width) for _ in 1:length(dynamics)] +using ExprTools - variable_dict = merge(Dict(:t => t), Dict(name(ds) => vars[i] for (i, ds) in enumerate(dynamics))) +# TODO: Fix this if F0 isa Matrix{RGB{N0f8}} +function determine_type(variable::VariableDynamics, dynamics::DynamicalSystem, checked=Set{Symbol}()) + # Initialize flags for detected types + is_color = false + is_complex = false - for x_pixel in 1:width - for y_pixel in 1:height - x = (x_pixel - 1) / (width - 1) - 0.5 - y = (y_pixel - 1) / (height - 1) - 0.5 + # Recursive helper function to analyze expressions + function recurse_expr(expr) + if expr isa Symbol + # Prevent infinite recursion for cycles in dynamics + if expr in checked + return (false, false) + end + push!(checked, expr) + + # Find and analyze the dynamic expressions associated with the symbol + for dyn in dynamics + if dyn.name == expr + color_f0, complex_f0 = recurse_expr(F_0(dyn).expr) + color_δf, complex_δf = recurse_expr(δF(dyn).expr) + return (color_f0 || color_δf, complex_f0 || complex_δf) + end + end - variable_dict[:x] = x - variable_dict[:y] = y + return (false, false) # Default if the symbol doesn't match any dynamic + elseif expr isa Expr + # Handle potential type-defining function calls or constructors + if occursin("Color(", string(expr)) || occursin("rand_color(", string(expr)) || occursin("RGB(", string(expr)) + is_color = true + end + if occursin("Complex(", string(expr)) || occursin("imag(", string(expr)) + is_complex = true + end - for (i, ds) in enumerate(dynamics) - δvars[i][y_pixel, x_pixel] = dt * custom_eval(δF(ds), variable_dict, width, height) + # Analyze each component of the expression recursively + for arg in expr.args + color, complex = recurse_expr(arg) + is_color |= color + is_complex |= complex end end + + return (is_color, is_complex) end - # Update vars - for (i, ds) in enumerate(dynamics) - vars[i] += δvars[i] + # Check both F_0 and δF expressions of the given variable + if variable.image !== nothing + is_color_F0, is_complex_F0 = (true, false) + else + is_color_F0, is_complex_F0 = recurse_expr(F_0(variable).expr) end + is_color_δF, is_complex_δF = recurse_expr(δF(variable).expr) - return vars + # Combine results from both initial and dynamic function checks + return (is_color_F0 || is_color_δF, is_complex_F0 || is_complex_δF) end -function evolve_system_step_2!(vars, dynamics::DynamicalSystem, width, height, t, dt, complex_func::Function) - δvars = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] - - variable_dict = merge(Dict(:t => t), Dict(name(ds) => vars[i] for (i, ds) in enumerate(dynamics))) - - for x_pixel in 1:width - for y_pixel in 1:height - x = (x_pixel - 1) / (width - 1) - 0.5 - y = (y_pixel - 1) / (height - 1) - 0.5 +function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normalize_img = false, adjust_brighness = true, plot = true, renderer = :threaded, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) + color_func = eval(color_expr) + complex_func = eval(complex_expr) - variable_dict[:x] = x - variable_dict[:y] = y + init_funcs = [compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height) for ds in dynamics] + + # IMPORTANT TODO: Initialize `vals` as Matrix{Float64} instead, and only convert to Color at the end + # we could also inspect the expr of F_0 and δF to determine if the result is a color or not + # vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] # Initialize each vars' grid using their F_0 expression + + # vals = [] + # for i in 1:length(dynamics) + # if contains_color_code(F_0(dynamics.dynamics[i])) # Assuming dynamics[i].F_0 is the function or its expression + # push!(vals, Matrix{Color}(undef, height, width)) + # elseif contains_complex_code(F_0(dynamics.dynamics[i])) + # push!(vals, Matrix{Complex{Float64}}(undef, height, width)) + # else + # push!(vals, Matrix{Float64}(undef, height, width)) + # end + # end + + # Initialize vals as a vector of matrices with the appropriate type + return_types = [determine_type(ds, dynamics) for ds in dynamics] + vals = [] + possible_types = [] + for (is_color, is_complex) in return_types + if is_color + push!(vals, Matrix{Color}(undef, height, width)) + + if Matrix{Color} ∉ possible_types + push!(possible_types, Matrix{Color}) + end + elseif is_complex + push!(vals, Matrix{Complex{Float64}}(undef, height, width)) - for (i, ds) in enumerate(dynamics) - val = dt .* custom_eval(δF(ds), variable_dict, width, height) + if Matrix{Complex{Float64}} ∉ possible_types + push!(possible_types, Matrix{Complex{Float64}}) + end + else + push!(vals, Matrix{Float64}(undef, height, width)) - if val isa Color - δvars[i][y_pixel, x_pixel] = val - elseif isreal(val) - δvars[i][y_pixel, x_pixel] = Color(val, val, val) - else - δvars[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) - end + if Matrix{Float64} ∉ possible_types + push!(possible_types, Matrix{Float64}) end end end + t = 0. - # Update vars - for (i, ds) in enumerate(dynamics) - vars[i] += δvars[i] - end - - return vars -end - -""" - animate_system(ds::DynamicalSystem, width, height, T, dt, color_func::Function) + # TODO: The type of vars Dict should also be determined by the expressions + # vars = Dict{Symbol, Union{Float64, Matrix{Float64}}}() + # The Matrix{Union{Float64, Color, ComplexF64}} is needed for the cache_computed_values function... This is sad, RETHINK. But maybe this is not a big overhead + vars = Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}() + images = [ds.image === nothing ? ds.image : imresize(ds.image, (height, width)) for ds in dynamics] -Animate a dynamical system by evolving it over time and saving the frames to a folder. - -# Arguments -- `dynamics::DynamicalSystem`: The dynamical system to animate. -- `width::Int`: The width of the image in pixels. -- `height::Int`: The height of the image in pixels. -- `T::Number`: The total time to evolve the system. -- `dt::Number`: The time step to use when evolving the system. - -# Optional Arguments -- `color_expr::Expr`: An expr that contains a function that tells how to combine the values of A and B to create a color. e.g., `:((A, B) -> RGB(A, B, 0))` -- `complex_expr::Expr`: An expr that contains a function that tells how convert a complex number to a real number. e.g., `:((c) -> real(c))` -""" -function animate_system(dynamics::DynamicalSystem, width, height, T, dt; color_expr::Expr = :((vals...) -> RGB(sum(vals)/length(vals), sum(vals)/length(vals), sum(vals)/length(vals))), complex_expr::Expr = :((c) -> real(c))) - color_func = eval(color_expr) - complex_func = eval(complex_expr) - - # Initialize each vars' grid using their F_0 expression - vars = [zeros(height, width) for _ in 1:length(dynamics)] - t = 0 for x_pixel in 1:width for y_pixel in 1:height x = (x_pixel - 1) / (width - 1) - 0.5 y = (y_pixel - 1) / (height - 1) - 0.5 - for (i, ds) in enumerate(dynamics) - val = custom_eval(F_0(ds), Dict(:x => x, :y => y, :t => t), width, height) - vars[i][y_pixel, x_pixel] = isreal(val) ? val : invokelatest(complex_func, val) + for i in 1:length(dynamics) + if (images[i] !== nothing) + vals[i][y_pixel, x_pixel] = Color(images[i][y_pixel, x_pixel]) + else + vars[:x] = x + vars[:y] = y + vars[:t] = t + val = invokelatest(init_funcs[i], vars) + + vals[i][y_pixel, x_pixel] = val + end + + # if val isa Color + # vals[i][y_pixel, x_pixel] = val + # else + # vals[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + # end end end end @@ -190,7 +219,7 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; color_e # Save the system's expressions to a file expr_file = animation_dir * "/expressions.txt" open(expr_file, "w") do f - write(f, "Animated using 'animate_system' function\n") + write(f, "Animated using 'animate_system_2' function\n") for ds in dynamics write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") @@ -205,125 +234,250 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; color_e image_files = [] # Store the names of the image files to use for creating the gif + # We only need to compile once each expression + genetic_funcs = [compile_expr(δF(ds), custom_operations, primitives_with_arity, gradient_functions, width, height) for ds in dynamics] + + total_frames = ceil(Int, T / dt) + progress = Progress(total_frames, desc="Initializing everything...", barlen=80) + + # Save the initial state + img = Array{RGB{Float64}, 2}(undef, height, width) + for x_pixel in 1:width + for y_pixel in 1:height + values = [var[y_pixel, x_pixel] for var in vals] + + # convert values to color + values = Color.(values) + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) + end + end + normalize_img && clean!(img) # Clean the image if requested + adjust_brighness && adjust_brightness!(img) # Adjust the brightness if requested + + plot && display(img) + + frame_file = animation_dir * "/frame_$(lpad(0, 5, '0')).png" + save(frame_file, map(clamp01nan, img)) + push!(image_files, frame_file) # Append the image file to the list + + # Evolve the system over time + start_time = time() for (i, t) in enumerate(range(0, T, step=dt)) - # Evolve the system - vars = evolve_system_step!(vars, dynamics, width, height, t, dt) + vals = evolve_system!(vals, dynamics, genetic_funcs, possible_types, width, height, t, dt, complex_func; renderer = renderer) # Evolve the system # Create an image from current state img = Array{RGB{Float64}, 2}(undef, height, width) for x_pixel in 1:width for y_pixel in 1:height - values = [isreal(var[y_pixel, x_pixel]) ? var[y_pixel, x_pixel] : invokelatest(complex_func, var[y_pixel, x_pixel]) for var in vars] - img[y_pixel, x_pixel] = invokelatest(color_func, values...) + values = [var[y_pixel, x_pixel] for var in vals] + + # convert values to color + values = Color.(values) + + img[y_pixel, x_pixel] = + invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) end end + normalize_img && clean!(img) # Clean the image if requested + adjust_brighness && adjust_brightness!(img) # Adjust the brightness if requested + + plot && display(img) + frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" save(frame_file, map(clamp01nan, img)) + push!(image_files, frame_file) # Append the image file to the list - # Append the image file to the list - push!(image_files, frame_file) + elapsed_time = time() - start_time + avg_time_per_frame = elapsed_time / i + remaining_time = avg_time_per_frame * (total_frames - i) + + ProgressMeter.update!(progress, i, desc="Processing Frame $i: Avg time per frame $(round(avg_time_per_frame, digits=2))s, Remaining $(round(remaining_time, digits=2))s") end + # Create the gif - gif_file = animation_dir * "/animation.gif" + println("Creating GIF...") + gif_file = animation_dir * "/animation_$animation_id.gif" run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps - # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) println("Animation saved to: $gif_file") println("Frames saved to: $animation_dir") println("Expressions saved to: $expr_file") end -function animate_system_2(dynamics::DynamicalSystem, width, height, T, dt; normalize_img = false, color_expr::Expr = :((vals...) -> RGB(sum(red.(vals))/length(vals), sum(green.(vals))/length(vals), sum(blue.(vals))/length(vals))), complex_expr::Expr = :((c) -> real(c))) - color_func = eval(color_expr) - complex_func = eval(complex_expr) +function evolve_system_basic!(vals, dynamics::DynamicalSystem, genetic_funcs, possible_types, width, height, t, dt, complex_func::Function) + # δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + # PROBLEM HERE! This will error for coupled variables that one have Color or Complex and the others + # don't. We should rethink this or add a smarter way to check if there will be Color or Complex + # δvals = [] + # for i in 1:length(dynamics) + # if contains_color_code(δF(dynamics.dynamics[i])) # Assuming dynamics[i].F_0 is the function or its expression + # push!(δvals, Matrix{Color}(undef, height, width)) + # elseif contains_complex_code(δF(dynamics.dynamics[i])) + # push!(δvals, Matrix{Complex{Float64}}(undef, height, width)) + # else + # push!(δvals, Matrix{Float64}(undef, height, width)) + # end + # end + # δvals = [Matrix{Float64}(undef, height, width) for _ in 1:length(dynamics)] + + vars = Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}(name(ds) => vals[i] for (i, ds) in enumerate(dynamics)) + vars[:t] = t + # vars[:possible_types] = possible_types - # Initialize each vars' grid using their F_0 expression - vars = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] - t = 0 for x_pixel in 1:width for y_pixel in 1:height x = (x_pixel - 1) / (width - 1) - 0.5 y = (y_pixel - 1) / (height - 1) - 0.5 - for (i, ds) in enumerate(dynamics) - val = custom_eval(F_0(ds), Dict(:x => x, :y => y, :t => t), width, height) + for i in 1:length(dynamics) + vars[:x] = x + vars[:y] = y + val = dt .* invokelatest(genetic_funcs[i], vars) + + vals[i][y_pixel, x_pixel] += val + + # δvals[i][y_pixel, x_pixel] = val + + # if val isa Color + # δvals[i][y_pixel, x_pixel] = val + # elseif isreal(val) + # δvals[i][y_pixel, x_pixel] = Color(val, val, val) + # else + # δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + # end + end + end + end + + # # Update vals + # for i in 1:length(dynamics) + # vals[i] += δvals[i] + # end + + return vals +end + +# TODO: Check if using threads may lead to unexpected results when there are random number generators involved +function evolve_system_threaded!(vals, dynamics, genetic_funcs, width, height, t, dt, complex_func) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + vars = Dict{Symbol, Union{Float64, Matrix{Float64}}}(name(ds) => vals[i] for (i, ds) in enumerate(dynamics)) + vars[:t] = t + + Threads.@threads for x_pixel in 1:width + for y_pixel in 1:height + x = (x_pixel - 1) / (width - 1) - 0.5 + y = (y_pixel - 1) / (height - 1) - 0.5 + + for i in 1:length(dynamics) + val = dt .* invokelatest(genetic_funcs[i], merge(vars, Dict(:x => x, :y => y))) if val isa Color - vars[i][y_pixel, x_pixel] = val + δvals[i][y_pixel, x_pixel] = val + elseif isreal(val) + δvals[i][y_pixel, x_pixel] = Color(val, val, val) else - vars[i][y_pixel, x_pixel] = isreal(val) ? Color(val, val, val) : Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + δvals[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) end end end end - # Generate a unique filename - base_dir = "saves" - if !isdir(base_dir) - mkdir(base_dir) + # Update vals + for i in 1:length(dynamics) + vals[i] += δvals[i] end - animation_id = length(readdir(base_dir)) + 1 - animation_dir = base_dir * "/animation_$animation_id" + return vals +end - # If the directory already exists, increment the id until we find one that doesn't - while isdir(animation_dir) - animation_id += 1 - animation_dir = base_dir * "/animation_$animation_id" - end +function vectorize_color_decision!(results, δvals, complex_func, i) + is_color = [r isa Color for r in results] # Determine the type of each element in results + is_real_and_not_color = [isreal(r) && !(r isa Color) for r in results] - mkdir(animation_dir) + # Assign directly where result elements are Color + δvals[i][is_color] = results[is_color] - # Save the system's expressions to a file - expr_file = animation_dir * "/expressions.txt" - open(expr_file, "w") do f - write(f, "Animated using 'animate_system_2' function\n") - for ds in dynamics - write(f, "$(name(ds))_0 = CustomExpr($(string(F_0(ds))))\n") - write(f, "δ$(name(ds))/δt = CustomExpr($(string(δF(ds))))\n") - end - write(f, "color_func= $(capture_function(color_expr))\n") - write(f, "complex_func= $(capture_function(complex_expr))\n") - write(f, "T= $T\n") - write(f, "dt= $dt\n") - write(f, "width= $width\n") - write(f, "height= $height\n") + # Where the results are real numbers, create Color objects + real_vals = results[is_real_and_not_color] + δvals[i][is_real_and_not_color] = Color.(real_vals, real_vals, real_vals) + + # For remaining cases, apply the complex function and create Color objects + needs_complex = .!(is_color .| is_real_and_not_color) + complex_results = results[needs_complex] + processed_complex = complex_func.(complex_results) + δvals[i][needs_complex] = Color.(processed_complex, processed_complex, processed_complex) +end + + +function evolve_system_vectorized!(vals, dynamics::DynamicalSystem, genetic_funcs, width, height, t, dt, complex_func::Function) + # Precompute coordinate grids + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + # Prepare δvals to accumulate changes + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] + + vars = Dict{Symbol, Union{Float64, Matrix{Float64}}}(name(ds) => vals[i] for (i, ds) in enumerate(dynamics)) + vars[:t] = t + + # Loop through each dynamical system + for i in 1:length(dynamics) + # Evaluate the function for all pixels in a vectorized manner + result = broadcast((x, y) -> dt .* invokelatest(genetic_funcs[i], merge(vars, Dict(:x => x, :y => y))), X, Y) + + # After obtaining results for each dynamic system + vectorize_color_decision!(result, δvals, complex_func, i) end - image_files = [] # Store the names of the image files to use for creating the gif + # Update vals + for i in 1:length(dynamics) + vals[i] += δvals[i] + end - for (i, t) in enumerate(range(0, T, step=dt)) - vars = evolve_system_step_2!(vars, dynamics, width, height, t, dt, complex_func) # Evolve the system + return vals +end - # Create an image from current state - img = Array{RGB{Float64}, 2}(undef, height, width) - for x_pixel in 1:width - for y_pixel in 1:height - values = [var[y_pixel, x_pixel] for var in vars] +function evolve_system_vectorized_threaded!(vals, dynamics::DynamicalSystem, genetic_funcs, width, height, t, dt, complex_func::Function; n_blocks = Threads.nthreads() * 4) + δvals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] - img[y_pixel, x_pixel] = - invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) - end - end + # Compute coordinate grids once + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + vars = Dict{Symbol, Union{Float64, Matrix{Float64}}}(name(ds) => vals[i] for (i, ds) in enumerate(dynamics)) + vars[:t] = t + + # Multithreading across either columns or blocks of columns + Threads.@threads for block in 1:n_blocks + x_start = 1 + (block - 1) * Int(width / n_blocks) + x_end = block * Int(width / n_blocks) + X_block = X[:, x_start:x_end] + Y_block = Y[:, x_start:x_end] + δvals_block = [Matrix{Color}(undef, height, x_end - x_start + 1) for _ in 1:length(dynamics)] - if normalize_img - img = clean!(img) + # Vectorized computation within each block + for i in 1:length(dynamics) + result_block = broadcast((x, y) -> dt .* invokelatest(genetic_funcs[i], merge(vars, Dict(:x => x, :y => y))), X_block, Y_block) + + # Use a vectorized color decision + vectorize_color_decision!(result_block, δvals_block, complex_func, i) end - frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" - save(frame_file, map(clamp01nan, img)) + # Update the global δvals with the block's results + for i in 1:length(dynamics) + δvals[i][:, x_start:x_end] .= δvals_block[i] + end + end - # Append the image file to the list - push!(image_files, frame_file) + # Update vals + for i in 1:length(dynamics) + vals[i] += δvals[i] end - # Create the gif - gif_file = animation_dir * "/animation_$animation_id.gif" - run(`convert -delay 4.16 -loop 0 $(image_files) $gif_file`) # Use ImageMagick to create a GIF animation at 24 fps - # run(`ffmpeg -framerate 24 -pattern_type glob -i '$(animation_dir)/*.png' -r 15 -vf scale=512:-1 $gif_file`) - # run convert -delay 4.16 -loop 0 $(ls frame_*.png | sort -V) animation_356.gif to do it manually in the terminal - println("Animation saved to: $gif_file") - println("Frames saved to: $animation_dir") - println("Expressions saved to: $expr_file") + return vals end \ No newline at end of file diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index 5bf0874..9e36166 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -1,260 +1,163 @@ using CoherentNoise: sample, perlin_2d using Random: seed! -custom_eval(ce::CustomExpr, vars, width, height; samplers = Dict(), primitives_with_arity = primitives_with_arity) = - custom_eval(ce.expr, vars, width, height; samplers, primitives_with_arity) +# minus one means that this is a matrix? +# TODO: Check this, maybe this is not necessary at all... Also we have another primitives_with_arity in the ExprGenerators.jl file +# primitives_with_arity = Dict( +# :sin => 1, +# :cos => 1, +# :tan => 1, +# :perlin_color => 2, +# :safe_divide => 2, +# :x => 0, +# :y => 0, +# :A => -1, +# :B => -1, +# :C => -1, +# :D => -1, +# :t => 0 +# ) + +custom_operations = Dict( + :ifs => ternary, + :rand_scalar => rand_scalar, + :rand_color => rand_color + # add more custom operations as needed +) + +# TODO: Format this function properly +function convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + if isa(expr, Expr) && expr.head == :call + func = expr.args[1] + args = map(a -> convert_expr(a, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers), expr.args[2:end]) -ternary(cond, x, y) = cond ? x : y -ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean + if func == :perlin_2d || func == :perlin_color + seed = expr.args[2] -function custom_eval(expr, vars, width, height; samplers = Dict(), primitives_with_arity = primitives_with_arity) - if expr isa Symbol - if primitives_with_arity[expr] == 0 - if vars[expr] isa Number || vars[expr] isa Color - return vars[expr] - elseif vars[expr] isa Matrix - idx_x = (vars[:x]+0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y]+0.5) * (height-1) + 1 |> trunc |> Int - return vars[expr][idx_x, idx_y] - else - throw(ArgumentError("Invalid type for variable $expr: $(typeof(vars[expr]))")) + if !haskey(samplers, seed) + samplers[seed] = perlin_2d(seed=hash(seed)) # Initialize the Perlin noise generator end + sampler = samplers[seed] + if func == :perlin_2d + return Expr(:call, :sample_perlin_2d, sampler, args[2:end]...) # Return an expression that will perform sampling at runtime + elseif func == :perlin_color + return Expr(:call, :sample_perlin_color, sampler, args[2:end]...) + end + elseif haskey(gradient_functions, func) + return handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + elseif haskey(custom_operations, func) + return Expr(:call, custom_operations[func], args...) else - return safe_getfield(expr) # Return the function associated with the symbol + return Expr(:call, func, args...) end - elseif expr isa Number || expr isa Color - return expr - else - if expr.head == :kw - # Handle individual keyword arguments. - return custom_eval(expr.args[2], vars, width, height; samplers=samplers, primitives_with_arity=primitives_with_arity) - elseif expr.head == :parameters - # Handle grouped keyword arguments. - kw_args = [custom_eval(kw, vars, width, height; samplers=samplers, primitives_with_arity=primitives_with_arity) for kw in expr.args] - return Dict(expr.args[i].args[1] => kw_args[i] for i in 1:length(expr.args)) + elseif isa(expr, Symbol) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:y] + 0.5) * (height-1) + 1 |> round |> Int, (vars[:x] + 0.5) * (width-1) + 1 |> round |> Int]) + else + return expr end - # Assume expr is an Expr with head :call - func = expr.args[1] - args = expr.args[2:end] - evaluated_args = custom_eval.(args, Ref(vars), width, height; samplers, primitives_with_arity) - - # Check for infinite values in the arguments - for i in eachindex(evaluated_args) - arg = evaluated_args[i] - - if arg isa Number || arg isa Color + else + return expr + end +end - if arg isa Complex || (arg isa Color && (arg.r isa Complex || arg.g isa Complex || arg.b isa Complex)) - mask_inf = isinf.(real.(arg)) .| isnan.(real.(arg)) .| isinf.(imag.(arg)) .| isnan.(imag.(arg)) - mask_large = (real.(arg) .> 1e6) .| (imag.(arg) .> 1e6) - mask_small = (real.(arg) .< -1e6) .| (imag.(arg) .< -1e6) - else - mask_inf = isinf.(arg) .| isnan.(arg) - mask_large = arg .> 1e6 - mask_small = arg .< -1e6 - end +function sample_perlin_2d(sampler, args...) + return sample.(sampler, args...) +end - new_arg = map((m, a) -> ifelse(m, 0.0, a), mask_inf, arg) - new_arg = map((m, a) -> ifelse(m, 1.0, a), mask_large, new_arg) - new_arg = map((m, a) -> ifelse(m, -1.0, a), mask_small, new_arg) +function sample_perlin_color(sampler, args...) + offset = args[3] + return sample.(sampler, args[1] .+ offset, args[2] .+ offset) +end - if new_arg isa Number - evaluated_args[i] = new_arg - else - if arg isa Color && (arg.r isa Complex || arg.g isa Complex || arg.b isa Complex) - evaluated_args[i] = Color(Complex.(new_arg...)) - else - evaluated_args[i] = Color(new_arg...) - end +function handle_gradient_function(func, expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + args = expr.args[2:end] # extract arguments from the expression + kwargs = Dict() + positional_args = [] + + for arg in args + if isa(arg, Expr) && arg.head == :parameters + # Handle keyword arguments nested within :parameters + for kw in arg.args + if isa(kw, Expr) && kw.head == :kw + key = kw.args[1] + value = kw.args[2] + kwargs[Symbol(key)] = value # Store kwargs to pass later end end + elseif isa(arg, Expr) && arg.head == :kw + # Handle keyword arguments directly + key = arg.args[1] + value = arg.args[2] + kwargs[Symbol(key)] = value + else + # It's a positional argument, add to positional_args + push!(positional_args, arg) end + end - if func == :+ - return evaluated_args[1] .+ evaluated_args[2] - elseif func == :- - return evaluated_args[1] .- evaluated_args[2] - elseif func == :* - return evaluated_args[1] .* evaluated_args[2] - elseif func == :/ - return evaluated_args[1] ./ evaluated_args[2] - elseif func == :^ - return evaluated_args[1] .^ evaluated_args[2] - elseif func == :sin - return sin.(evaluated_args[1]) - elseif func == :cos - return cos.(evaluated_args[1]) - elseif func == :round - return round.(evaluated_args[1]) - elseif func == :Int - return Int(evaluated_args[1]) - elseif func == :sinh - return sinh.(evaluated_args[1]) - elseif func == :cosh - return cosh.(evaluated_args[1]) - elseif func == :abs - return abs.(evaluated_args[1]) - elseif func == :sqrt - return sqrt.(abs.(evaluated_args[1])) - elseif func == :mod - return mod.(evaluated_args[1], evaluated_args[2]) - elseif func == :log - return log.(abs.(evaluated_args[1])) - elseif func == :exp - return exp.(evaluated_args[1]) - elseif func == :or - return apply_elementwise((x, y) -> convert(Float64, x | y), threshold.(evaluated_args[1]), threshold.(evaluated_args[2])) - elseif func == :and - return apply_elementwise((x, y) -> convert(Float64, x & y), threshold.(evaluated_args[1]), threshold.(evaluated_args[2])) - elseif func == :xor - return apply_elementwise((x, y) -> convert(Float64, xor(x, y)), threshold.(evaluated_args[1]), threshold.(evaluated_args[2])) - elseif func == :perlin_2d || func == :perlin_color - seed = expr.args[2] - if !haskey(samplers, seed) - samplers[seed] = perlin_2d(seed=hash(seed)) - end - sampler = samplers[seed] - noise_args = evaluated_args[2:end] - - if func == :perlin_2d - return sample.(sampler, noise_args[1], noise_args[2]) - else - offset = noise_args[3] - return sample.(sampler, noise_args[1] .+ offset, noise_args[2] .+ offset) - end - elseif func == :grad_dir - return grad_dir.(safe_getfield(args[1]), evaluated_args[2], evaluated_args[3]) - elseif func == :grad_mag - return grad_mag.(safe_getfield(args[1]), evaluated_args[2:end]...) - elseif func == :blur - return blur.(safe_getfield(args[1]), evaluated_args[2], evaluated_args[3]) - elseif func == :atan - return atan.(evaluated_args[1], evaluated_args[2]) - elseif func == :dissolve - return dissolve.(evaluated_args[1], evaluated_args[2], evaluated_args[3]) - elseif func == :Color - return Color(evaluated_args...) - elseif func == :Complex - return Complex.(evaluated_args...) - elseif func == :real - return real.(evaluated_args...) - elseif func == :imag - return imag.(evaluated_args...) - elseif func == :rand_scalar - if length(evaluated_args) == 0 - return rand(1) |> first - else - seed!(trunc(Int, evaluated_args[1] * 1000)) - return rand(1) |> first - end - elseif func == :ifs - # TODO: maybe check the case with Color in the conditional - return ternary.(evaluated_args[1], evaluated_args[2], evaluated_args[3]) - elseif func == :max - if isreal(evaluated_args[1]) && isreal(evaluated_args[2]) - return max.(evaluated_args[1], evaluated_args[2]) - else - return max.(real.(evaluated_args[1]), real.(evaluated_args[2])) + max.(imag.(evaluated_args[1]), imag.(evaluated_args[2])) * im - end - elseif func == :min - if evaluated_args[1] isa Complex || evaluated_args[2] isa Complex - return min.(real.(evaluated_args[1]), real.(evaluated_args[2])) + min.(imag.(evaluated_args[1]), imag.(evaluated_args[2])) * im - else - return min.(real.(evaluated_args[1]), real.(evaluated_args[2])) - end - elseif func == :< - return evaluated_args[1] .< evaluated_args[2] - elseif func == :> - return evaluated_args[1] .> evaluated_args[2] - elseif func == :<= - return evaluated_args[1] .<= evaluated_args[2] - elseif func == :>= - return evaluated_args[1] .>= evaluated_args[2] - # elseif func == :laplacian - # return laplacian(args[1], vars, width, height) - elseif func == :laplacian - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments + caller_func = positional_args[1] - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return laplacian(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :x_grad - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments + # if caller_func isa Symbol, then convert it to a function + 0 + # TODO: Fix this embarrassing hack... + if caller_func isa Symbol + caller_func = Expr(:call, +, caller_func, 0) + end - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return x_grad(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :y_grad - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments + # Convert the primary expression argument into a function if not already + if !isempty(positional_args) + expr_func = compile_expr(caller_func, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) + else + throw(ErrorException("No positional arguments provided for the gradient function")) + end - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return y_grad(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :grad_magnitude - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments + # Construct a call to the proper func, incorporating kwargs correctly + grad_expr = Expr(:call, Symbol(func), expr_func, :(vars), :(width), :(height)) + for (k, v) in kwargs + push!(grad_expr.args, Expr(:kw, k, v)) + end - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return grad_magnitude(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :grad_direction - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments + return grad_expr +end - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return grad_direction(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :neighbor_min - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments +# TODO: Do we need this function? +function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + if get(primitives_with_arity, expr, 1) == 0 + return :(vars[$(QuoteNode(expr))]) + elseif get(primitives_with_arity, expr, 1) == -1 + return :(vars[$(QuoteNode(expr))][(vars[:y] + 0.5) * (height-1) + 1 |> round |> Int, (vars[:x] + 0.5) * (width-1) + 1 |> round |> Int]) + else + return expr + end +end - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return neighbor_min(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :neighbor_max - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments +compile_expr(expr::Union{Number, Color}, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) = + return expr - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return neighbor_max(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - elseif func == :neighbor_ave - positional_args = filter(a -> !(a isa Expr && (a.head == :(=) || a.head == :parameters)), args) # Extract positional arguments +function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height, samplers) + # First transform the expression to properly reference `vars` + expr = convert_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height, samplers) - if any(a -> a isa Expr && a.head == :parameters, args) # Extract keyword arguments - kwargs_expr = first(filter(a -> a isa Expr && a.head == :parameters, args)) - kw_dict = custom_eval(kwargs_expr, vars, width, height; samplers, primitives_with_arity) - else - kw_dict = Dict() - end - return neighbor_ave(positional_args[1], vars, width, height; kw_dict...) # Call the function with positional and keyword arguments - else - error("Unknown function: $func") - end - end + # Now compile the transformed expression into a Julia function + # This function explicitly requires `vars` to be passed as an argument + return eval(:( (vars::Dict) -> $expr )) end +f(expr::GeneticExpr, width, height) = + compile_expr(expr, custom_operations, primitives_with_arity, gradient_functions, width, height) + +compile_expr(expr::GeneticExpr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height) = + compile_expr(expr.expr, custom_operations, primitives_with_arity, gradient_functions, width, height, Dict()) + +Base.isless(x::Complex, y::Number) = x.re < y +Base.isless(x::Number, y::Complex) = x < y.re +Base.isless(x::Complex, y::Complex) = x.re < y.re || (x.re == y.re && x.im < y.im) + +Color(x::Complex, y::Complex, z::Float64) = Color(x, y, Complex(z)) +Color(x::Complex, y::Float64, z::Float64) = Color(x, Complex(y), Complex(z)) +Color(x::Float64, y::Complex, z::Float64) = Color(Complex(x), y, Complex(z)) +Color(x::Float64, y::Float64, z::Complex) = Color(Complex(x), Complex(y), z) +Color(x::Complex, y::Float64, z::Complex) = Color(x, Complex(y), z) +Color(x::Float64, y::Complex, z::Complex) = Color(Complex(x), y, z) \ No newline at end of file diff --git a/src/ExprGenerators.jl b/src/ExprGenerators.jl index 303736a..03890a4 100644 --- a/src/ExprGenerators.jl +++ b/src/ExprGenerators.jl @@ -40,16 +40,17 @@ const primitives_with_arity = Dict( :ifs => 3, :max => 2, :min => 2, - :A => 0, - :B => 0, - :C => 0, - :t => 0 + :real => 1, + :imag => 1, + :A => -1, + :B => -1, + :C => -1, + :D => -1, + :t => -1 ) # special_funcs take not only numbers as arguments const special_funcs = ( - :grad_mag, - :grad_dir, :blur, :perlin_2d, :perlin_color @@ -63,6 +64,15 @@ const boolean_funcs = ( :ifs, ) +# variables +const variables = ( + :A, + :B, + :C, + :D, + :t +) + # color_funcs are functions that can return a color const color_funcs = ( :perlin_color, @@ -71,7 +81,7 @@ const color_funcs = ( ) function random_expr(primitives_with_arity, max_depth; kwargs...) - return CustomExpr(random_function(primitives_with_arity, max_depth; kwargs...)) + return GeneticExpr(random_function(primitives_with_arity, max_depth; kwargs...)) end function random_function(primitives_with_arity, max_depth; boolean_functions_depth_threshold = 1) @@ -93,11 +103,10 @@ function random_function(primitives_with_arity, max_depth; boolean_functions_dep end else if max_depth > boolean_functions_depth_threshold # only allow boolean functions deep in the function graph - available_funcs = [k for k in keys(primitives_with_arity) if k ∉ boolean_funcs] + available_funcs = [k for k in keys(primitives_with_arity) if k ∉ boolean_funcs && k ∉ variables] else - available_funcs = keys(primitives_with_arity) + available_funcs = filter(x -> x ∉ variables, keys(primitives_with_arity)) end - # Select a random primitive function f = rand(available_funcs) n_args = primitives_with_arity[f] @@ -124,18 +133,6 @@ function random_function(primitives_with_arity, max_depth; boolean_functions_dep return rand() elseif f == :rand_color return Color(rand(3)) - elseif f == :grad_dir # ??remove the Color maker functions from primitives_with_arity - op = rand((x -> x[1]).(filter(x -> x.second ∈ [2] && x.first ∉ special_funcs ∪ boolean_funcs, collect(primitives_with_arity)))) # TODO: Maybe enable boolean functions here? - n_args = primitives_with_arity[op] - args = [op, [random_function(primitives_with_arity, max_depth - 1) for _ in 1:n_args]...] - - return Expr(:call, f, args...) - elseif f == :grad_mag - op = rand((x -> x[1]).(filter(x -> x.second != 0 && x.first ∉ special_funcs ∪ boolean_funcs, collect(primitives_with_arity)))) # TODO: Maybe enable boolean functions here? - n_args = primitives_with_arity[op] - args = [op, [random_function(primitives_with_arity, max_depth - 1) for _ in 1:n_args]...] - - return Expr(:call, f, args...) elseif f == :blur op = rand((x -> x[1]).(filter(x -> x.second ∈ [2] && x.first ∉ [:or, :and, :xor, :perlin_2d, :perlin_color, :grad_dir, :grad_mag], collect(primitives_with_arity)))) #maybe disable boolean functions here? n_args = primitives_with_arity[op] diff --git a/src/Genetic.jl b/src/Genetic.jl index a11627c..391951a 100644 --- a/src/Genetic.jl +++ b/src/Genetic.jl @@ -145,16 +145,16 @@ function recursive_mutation!(expr, mutation_probs, primitives_dict, parent, idx, return expr end -function mutate!(ce::CustomExpr, mutation_probs, primitives_dict=primitives_with_arity, max_mutations::Int=5) - mutated_expr = recursive_mutation!(ce.expr, mutation_probs, deepcopy(primitives_dict), nothing, 0, max_mutations) - return CustomExpr(mutated_expr) +function mutate!(geneticexpr::GeneticExpr, mutation_probs, primitives_dict=primitives_with_arity, max_mutations::Int=5) + mutated_expr = recursive_mutation!(geneticexpr.expr, mutation_probs, deepcopy(primitives_dict), nothing, 0, max_mutations) + return GeneticExpr(mutated_expr) end function mutate(e, mutation_probs, primitives_dict=primitives_with_arity, max_mutations::Int=5) - if e isa CustomExpr + if e isa GeneticExpr mutated_expr = recursive_mutation!(deepcopy(e.expr), mutation_probs, deepcopy(primitives_dict), nothing, 0, max_mutations) else mutated_expr = recursive_mutation!(deepcopy(e), mutation_probs, deepcopy(primitives_dict), nothing, 0, max_mutations) end - return CustomExpr(mutated_expr) + return GeneticExpr(mutated_expr) end \ No newline at end of file diff --git a/src/CustomExpr.jl b/src/GeneticExpr.jl similarity index 83% rename from src/CustomExpr.jl rename to src/GeneticExpr.jl index 81c89f0..56ca476 100644 --- a/src/CustomExpr.jl +++ b/src/GeneticExpr.jl @@ -1,14 +1,14 @@ using Base: show -struct CustomExpr +struct GeneticExpr expr::Expr end -function CustomExpr(x::Union{Number, Symbol, Color}) +function GeneticExpr(x::Union{Number, Symbol, Color}) return x end -function Base.show(io::IO, c_expr::CustomExpr) +function Base.show(io::IO, c_expr::GeneticExpr) function short_expr(expr) if expr.head == :call && length(expr.args) > 0 new_args = Any[] diff --git a/src/GeneticTextures.jl b/src/GeneticTextures.jl index 11f1241..117ebe9 100644 --- a/src/GeneticTextures.jl +++ b/src/GeneticTextures.jl @@ -6,14 +6,17 @@ include("Color.jl") export Color, red, blue, green, ColorStyle include("PerlinBroadcasting.jl") -include("CustomExpr.jl") +include("GeneticExpr.jl") +export GeneticExpr + include("ExprGenerators.jl") export primitives_with_arity, random_expr include("MathOperations.jl") +export gradient_functions include("ExprEvaluation.jl") -export custom_eval +export compile_expr include("Renderer.jl") export generate_image, save_image_and_expr @@ -25,6 +28,6 @@ include("UI.jl") export generate_population, display_images, get_user_choice, create_variations include("DynamicalSystems.jl") -export DynamicalSystem, VariableDynamics, animate_system, animate_system_2 +export DynamicalSystem, VariableDynamics, animate_system end \ No newline at end of file diff --git a/src/MathOperations.jl b/src/MathOperations.jl index 48a958e..3143650 100644 --- a/src/MathOperations.jl +++ b/src/MathOperations.jl @@ -1,37 +1,43 @@ -using ForwardDiff: gradient, derivative +using Random -function threshold(x, t = 0.5) - return x >= t ? 1 : 0 -end +ternary(cond, x, y) = cond ? x : y +ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert the float to a boolean -function apply_elementwise(op, args...) - is_color = any(x -> x isa Color, args) - result = op.(args...) - return is_color ? Color(result) : result -end +threshold(x, t = 0.5) = x >= t ? 1 : 0 -function grad_dir(f, x, y) - """ - Compute the gradient of f and return the direction of the gradient (in radians). - """ +and(x::Number, y::Number) = convert(Float64, threshold(x) & threshold(y)) +or(x::Number, y::Number) = convert(Float64, threshold(x) | threshold(y)) +xor(x::Number, y::Number) = convert(Float64, threshold(x) ⊻ threshold(y)) +ifs(cond::Number, x::Number, y::Number) = ternary(cond, x, y) +and(x::Color, y::Color) = and.(x, y) +or(x::Color, y::Color) = or.(x, y) +xor(x::Color, y::Color) = xor.(x, y) +ifs(cond::Color, x::Color, y::Color) = ternary.(cond, x, y) - g = gradient(z -> f(z[1], z[2]), [x, y]) - return atan(g[2], g[1]) +function rand_scalar(args...) + if length(args) == 0 + return rand(1) |> first + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return rand(1) |> first + end end -function grad_mag(f, coords::Vararg{Number}) - """ - Compute the gradient of f and return the magnitude of the gradient. - """ - - if f == log - f = x -> log(abs(x)) - elseif f == sqrt - f = x -> sqrt(abs(x)) +function rand_color(args...) + if length(args) == 0 + return Color(rand(3)...) + else + # TODO: Fix the seed fix, right now it will create a homogeneous image + seed!(trunc(Int, args[1] * 1000)) + return Color(rand(3)...) end +end - g = gradient(c -> f(c...), collect(coords)) - return sqrt(sum(x^2 for x in g)) +function apply_elementwise(op, args...) + is_color = any(x -> x isa Color, args) + result = op.(args...) + return is_color ? Color(result) : result end function dissolve(f1, f2, weight) @@ -80,105 +86,51 @@ function gaussian_kernel(size, sigma) return kernel end -function laplacian(expr, vars, width, height; Δx = 1, Δy = 1) - # Compute the Laplacian of an expression at a point (x, y) - # by comparing the value of expr at (x, y) with its values at (x±Δ, y) and (x, y±Δ). - - idx_x = (vars[:x]+0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y]+0.5) * (height-1) + 1 |> trunc |> Int +# TODO: Remove kwargs? Right now only works for Δx = 1 +function x_grad(func, vars, width, height; Δx = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + Δx_scaled = Δx / (width - 1) - if Δx == 0 - ∇x = 0 - else - if idx_x > 1 && idx_x < width - vars_plus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x + Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x + Δx)) - vars_minus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x - Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x - Δx)) - x_plus = custom_eval(expr, vars_plus_Δx, width, height) - x_minus = custom_eval(expr, vars_minus_Δx, width, height) - ∇x = (x_plus + x_minus - 2 * center) / Δx^2 - elseif idx_x == 1 - vars_plus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x + Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x + Δx)) - x_plus = custom_eval(expr, vars_plus_Δx, width, height) - ∇x = (x_plus.- center) / Δx^2 - else # idx_x == width - vars_minus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x - Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x - Δx)) - x_minus = custom_eval(expr, vars_minus_Δx, width, height) - ∇x = (x_minus - center) / Δx^2 - end - end + center_val = computed_values[idx_y, idx_x] - if Δy == 0 - ∇y = 0 + if idx_x == width + x_minus_val = computed_values[idx_y, idx_x - Δx] + return (center_val - x_minus_val) / Δx_scaled else - if idx_y > 1 && idx_y < height - vars_plus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y + Δy] : v) for (k, v) in vars), Dict(:y => idx_y + Δy)) - vars_minus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y - Δy] : v) for (k, v) in vars), Dict(:y => idx_y - Δy)) - y_plus = custom_eval(expr, vars_plus_Δy, width, height) - y_minus = custom_eval(expr, vars_minus_Δy, width, height) - ∇y = (y_plus + y_minus - 2 * center) / Δy^2 - elseif idx_y == 1 - vars_plus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y + Δy] : v) for (k, v) in vars), Dict(:y => idx_y + Δy)) - y_plus = custom_eval(expr, vars_plus_Δy, width, height) - ∇y = (y_plus - center) / Δy^2 - else # idx_y == height - vars_minus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y - Δy] : v) for (k, v) in vars), Dict(:y => idx_y - Δy)) - y_minus = custom_eval(expr, vars_minus_Δy, width, height) - ∇y = (y_minus - center) / Δy^2 - end + x_plus_val = computed_values[idx_y, idx_x + Δx] + return (x_plus_val - center_val) / Δx_scaled end - - return ∇x + ∇y end -function x_grad(expr, vars, width, height; Δx = 1) - idx_x = (vars[:x] + 0.5) * (width - 1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height - 1) + 1 |> trunc |> Int - - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) - - if Δx == 0 - return 0 - else - if idx_x > 1 && idx_x <= width - Δx - vars_plus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x + Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x + Δx)) - x_plus = custom_eval(expr, vars_plus_Δx, width, height) - return (x_plus - center) / Δx - elseif idx_x == 1 - vars_plus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x + Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x + Δx)) - x_plus = custom_eval(expr, vars_plus_Δx, width, height) - return (x_plus - center) / Δx - else # idx_x == width - vars_minus_Δx = merge(Dict(k => (isa(v, Matrix) ? v[idx_x - Δx, idx_y] : v) for (k, v) in vars), Dict(:x => idx_x - Δx)) - x_minus = custom_eval(expr, vars_minus_Δx, width, height) - return (center - x_minus) / Δx - end +# TODO: Remove kwargs? Right now only works for Δy = 1 +function y_grad(func, vars, width, height; Δy = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached end -end -function y_grad(expr, vars, width, height; Δy = 1) - idx_x = (vars[:x] + 0.5) * (width - 1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height - 1) + 1 |> trunc |> Int + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + Δy_scaled = Δy / (height - 1) - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) + center_val = computed_values[idx_y, idx_x] - if Δy == 0 - return 0 + if idx_y == height + y_minus_val = computed_values[idx_y - Δy, idx_x] + return (center_val - y_minus_val) / Δy_scaled else - if idx_y > 1 && idx_y <= height - Δy - vars_plus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y + Δy] : v) for (k, v) in vars), Dict(:y => idx_y + Δy)) - y_plus = custom_eval(expr, vars_plus_Δy, width, height) - return (y_plus - center) / Δy - elseif idx_y == 1 - vars_plus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y + Δy] : v) for (k, v) in vars), Dict(:y => idx_y + Δy)) - y_plus = custom_eval(expr, vars_plus_Δy, width, height) - return (y_plus - center) / Δy - else # idx_y == height - vars_minus_Δy = merge(Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y - Δy] : v) for (k, v) in vars), Dict(:y => idx_y - Δy)) - y_minus = custom_eval(expr, vars_minus_Δy, width, height) - return (center - y_minus) / Δy - end + y_plus_val = computed_values[idx_y + Δy, idx_x] + return (y_plus_val - center_val) / Δy_scaled end end @@ -194,22 +146,103 @@ function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) return atan.(∂f_∂y, ∂f_∂x) end -function neighbor_min(expr, vars, width, height; Δx = 1, Δy = 1) - # Return the smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) - # around the point (x, y) +# TODO: Remove kwargs? Right now only works for Δx = Δy = 1 +function laplacian(func, vars, width, height; Δx = 1, Δy = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + + Δx_scaled = Δx / (width - 1) # Scale Δx to be proportional to the image width + Δy_scaled = Δy / (height - 1) # Scale Δy to be proportional to the image height + + # Handle boundary conditions for Laplacian calculation + # Ensuring not to go out of bounds with @inbounds when accessing the array + center_val = computed_values[idx_y, idx_x] + ∇x, ∇y = 0.0, 0.0 + + if Δx > 0 && idx_x > 1 && idx_x < width + x_plus_val = computed_values[idx_y, idx_x + Δx] + x_minus_val = computed_values[idx_y, idx_x - Δx] + ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 + end + + if Δy > 0 && idx_y > 1 && idx_y < height + y_plus_val = computed_values[idx_y + Δy, idx_x] + y_minus_val = computed_values[idx_y - Δy, idx_x] + ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 + end + + return ∇x + ∇y +end + +# Return the smallest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + + # Initialize min_val using the value at the central point + min_val = computed_values[idx_y, idx_x] + + # Define the ranges, ensuring they stay within bounds + min_x = max(1, idx_x - Δx) + max_x = min(width, idx_x + Δx) + min_y = max(1, idx_y - Δy) + max_y = min(height, idx_y + Δy) + + # Loop through the neighborhood + @inbounds for dy in min_y:max_y, dx in min_x:max_x + # Avoid considering the center again + if dx == idx_x && dy == idx_y + continue + end + val = computed_values[dy, dx] + if val < min_val + min_val = val + end + end + + return min_val +end + +# Return the minimum value from a neighborhood of radius Δr around the point (x, y) +function neighbor_min_radius(func, vars, width, height; Δr = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) - idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + # Initialize min_val using the center point value from the cached data + min_val = computed_values[idx_y, idx_x] - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) - min_val = center + # Evaluate within a circular neighborhood using cached data + for dx in -Δr:Δr, dy in -Δr:Δr + if dx^2 + dy^2 <= Δr^2 # Check if the point (dx, dy) is within the circular radius + new_x = idx_x + dx + new_y = idx_y + dy - for i in filter(x -> x > 0 && x != idx_x && x <= width, idx_x-Δx:idx_x+Δx) - for j in filter(y -> y > 0 && y != idx_y && y <= height, idx_y-Δy:idx_y+Δy) - new_vars = merge(Dict(k => (isa(v, Matrix) ? v[i, j] : v) for (k, v) in vars), Dict(:x => i, :y => j)) - val = custom_eval(expr, new_vars, width, height) - if val < min_val - min_val = val + if 1 <= new_x <= width && 1 <= new_y <= height # Check if the indices are within image boundaries + val = computed_values[new_y, new_x] + if val < min_val + min_val = val + end end end end @@ -217,29 +250,68 @@ function neighbor_min(expr, vars, width, height; Δx = 1, Δy = 1) return min_val end -function neighbor_max(expr, vars, width, height; Δx = 1, Δy = 1) - # Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) - # around the point (x, y) +# Return the largest value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + + # Initialize max_val using the value at the central point + max_val = computed_values[idx_y, idx_x] + + # Define the ranges, ensuring they stay within bounds + min_x = max(1, idx_x - Δx) + max_x = min(width, idx_x + Δx) + min_y = max(1, idx_y - Δy) + max_y = min(height, idx_y + Δy) + + # Loop through the neighborhood + @inbounds for dy in min_y:max_y, dx in min_x:max_x + # Avoid considering the center again + if dx == idx_x && dy == idx_y + continue + end + val = computed_values[dy, dx] + if val > max_val + max_val = val + end + end + + return max_val +end - idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int +# Return the maximum value from a neighborhood of radius Δr around the point (x, y) +function neighbor_max_radius(func, vars, width, height; Δr = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) - max_val = center + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) - for i in filter(x -> x > 0 && x != idx_x && x <= width, idx_x-Δx:idx_x+Δx) - for j in filter(y -> y > 0 && y != idx_y && y <= height, idx_y-Δy:idx_y+Δy) - new_vars = merge(Dict(k => (isa(v, Matrix) ? v[i, j] : v) for (k, v) in vars), Dict(:x => i, :y => j)) - val = custom_eval(expr, new_vars, width, height) + # Initialize max_val using the center point value from the cached data + max_val = computed_values[idx_y, idx_x] - if isreal(val) && isreal(max_val) + # Evaluate within a circular neighborhood using cached data + for dx in -Δr:Δr, dy in -Δr:Δr + if dx^2 + dy^2 <= Δr^2 # Check if the point (dx, dy) is within the circular radius + new_x = idx_x + dx + new_y = idx_y + dy + + if 1 <= new_x <= width && 1 <= new_y <= height # Check if the indices are within image boundaries + val = computed_values[new_y, new_x] if val > max_val max_val = val end - else - if abs(val) > abs(max_val) - max_val = val - end end end end @@ -247,25 +319,93 @@ function neighbor_max(expr, vars, width, height; Δx = 1, Δy = 1) return max_val end -function neighbor_ave(expr, vars, width, height; Δx = 1, Δy = 1) - # Return the average from a neighborhood of size (2Δx + 1) x (2Δy + 1) - # around the point (x, y) +function cache_computed_values!(func, width::Int, height::Int, vars) + func_key = Symbol(string(func)) # Create a unique key based on function name + if !haskey(vars, func_key) + # computed_values = Matrix{Float64}(undef, height, width) + computed_values = Matrix{Union{Float64, Color, ComplexF64}}(undef, height, width) + for y in 1:height + for x in 1:width + vars[:x] = (x - 1) / (width - 1) - 0.5 + vars[:y] = (y - 1) / (height - 1) - 0.5 + val = func(vars) + computed_values[y, x] = val + # if val isa Color + # computed_values[y, x] = val + # else + # computed_values[y, x] = Color(val, val, val) + # end + end + end + vars[func_key] = computed_values # Store the computed values in vars + end +end + +function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + + min_x, max_x = max(1, idx_x - Δx), min(width, idx_x + Δx) + min_y, max_y = max(1, idx_y - Δy), min(height, idx_y + Δy) + sum_val = 0.0 + count = 0 + + @inbounds for dy in min_y:max_y, dx in min_x:max_x + sum_val += computed_values[dy, dx] + count += 1 + end + + return sum_val / count +end + +# Return the average value from a neighborhood of radius Δr around the point (x, y) +function neighbor_ave_radius(func, vars, width, height; Δr = 1) + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end + + computed_values = vars[func_key] + x_val, y_val = vars[:x], vars[:y] + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) - idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + sum_val = 0.0 + count = 0 - center = custom_eval(expr, merge(vars, Dict(k => (isa(v, Matrix) ? v[idx_x, idx_y] : v) for (k, v) in vars)), width, height) - sum_val = center + for dx in -Δr:Δr, dy in -Δr:Δr + if dx^2 + dy^2 <= Δr^2 # Check if the point (dx, dy) is within the circular radius + new_x = idx_x + dx + new_y = idx_y + dy - iterations = 1 - for i in filter(x -> x > 0 && x != idx_x && x <= width, idx_x-Δx:idx_x+Δx) - for j in filter(y -> y > 0 && y != idx_y && y <= height, idx_y-Δy:idx_y+Δy) - new_vars = merge(Dict(k => (isa(v, Matrix) ? v[i, j] : v) for (k, v) in vars), Dict(:x => i, :y => j)) - val = custom_eval(expr, new_vars, width, height) - sum_val += val - iterations += 1 + if 1 <= new_x <= width && 1 <= new_y <= height # Check if the indices are within image boundaries + sum_val += computed_values[new_y, new_x] + count += 1 + end end end - return sum_val / iterations + return sum_val / count end + +# This dictionary indicates which functions are gradient-related and need special handling +gradient_functions = Dict( + :grad_magnitude => grad_magnitude, + :grad_direction => grad_direction, + :x_grad => x_grad, + :y_grad => y_grad, + :laplacian => laplacian, + :neighbor_min => neighbor_min, + :neighbor_min_radius => neighbor_min_radius, + :neighbor_max => neighbor_max, + :neighbor_max_radius => neighbor_max_radius, + :neighbor_ave => neighbor_ave, + :neighbor_ave_radius => neighbor_ave_radius +) \ No newline at end of file diff --git a/src/PerlinBroadcasting.jl b/src/PerlinBroadcasting.jl index e6a4506..ac71084 100644 --- a/src/PerlinBroadcasting.jl +++ b/src/PerlinBroadcasting.jl @@ -2,6 +2,7 @@ using CoherentNoise using Base using Base.Broadcast +# Is all this necessary ? ... I think it is not struct PerlinStyle <: Broadcast.BroadcastStyle end Base.Broadcast.BroadcastStyle(::Type{<:CoherentNoise.Perlin{2}}) = PerlinStyle() diff --git a/src/Renderer.jl b/src/Renderer.jl index 40a2327..fb3b55a 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -1,33 +1,10 @@ using Images, Colors using CoherentNoise: perlin_2d - -function generate_image(expr, width, height) - img = Array{RGB{Float64}, 2}(undef, height, width) - - for y in 1:height - for x in 1:width - vars = Dict(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) - rgb = custom_eval(expr, vars, width, height) - - if rgb isa Color - img[y, x] = RGB(rgb.r, rgb.g, rgb.b) - elseif isa(rgb, Number) - img[y, x] = RGB(rgb, rgb, rgb) - else - error("Invalid type output from custom_eval: $(typeof(rgb))") - end - end - end - - clean!(img) - - return img -end - -# TODO: Maybe normalize each channel separately? +using Statistics clean!(img::Matrix{Color}) = clean!(RGB.(img)) # convert img to RGB and call clean! again +# TODO: Maybe normalize each channel separately? function clean!(img) # @show typeof(img), img # normalize img to be in [0, 1] @@ -49,13 +26,38 @@ function clean!(img) return img end +# Function to adjust the brightness of an image to a specific target value using luminosity weights +function adjust_brightness!(img::Matrix{RGB{Float64}}; target_brightness::Float64=0.71) + # Calculate current average brightness using weighted luminosity + current_brightness = mean([0.299*p.r + 0.587*p.g + 0.114*p.b for p in img]) + + # Avoid division by zero and unnecessary adjustments + if current_brightness > 0 && target_brightness > 0 + scale_factor = target_brightness / current_brightness + else + return img # Return image as is if brightness is zero or target is zero + end -function save_image_and_expr(img::Matrix{T}, custom_expr::CustomExpr; folder = "saves", prefix = "images") where {T} - # Create the folder if it doesn't exist - if !isdir(folder) - mkdir(folder) + # Adjust each pixel's RGB values based on the scale factor + for y in 1:size(img, 1) + for x in 1:size(img, 2) + r = img[y, x].r * scale_factor + g = img[y, x].g * scale_factor + b = img[y, x].b * scale_factor + + # Clamp values to the range [0, 1] and handle any NaNs that might appear + img[y, x] = RGB(clamp(r, 0, 1), clamp(g, 0, 1), clamp(b, 0, 1)) + end end + return img +end + + +function save_image_and_expr(img::Matrix{T}, genetic_expr::GeneticExpr; folder = "saves", prefix = "images") where {T} + # Create the folder if it doesn't exist + !isdir(folder) && mkdir(folder) + # Generate a unique filename filename = generate_unique_filename(folder, prefix) @@ -66,9 +68,185 @@ function save_image_and_expr(img::Matrix{T}, custom_expr::CustomExpr; folder = " # Save the expression to a file expr_file = folder * "/" * filename * ".txt" open(expr_file, "w") do f - write(f, string(custom_expr)) + write(f, string(genetic_expr)) end println("Image saved to: $image_file") println("Expression saved to: $expr_file") end + +# TODO: Fix this if F0 isa Matrix{RGB{N0f8}} +function determine_type(ge::GeneticExpr) + # Initialize flags for detected types + is_color = false + is_complex = false + + expr = ge.expr + + if expr isa Symbol + return (false, false) + + elseif expr isa Expr + # Handle potential type-defining function calls or constructors + if occursin("Color(", string(expr)) || occursin("rand_color(", string(expr)) || occursin("RGB(", string(expr)) + is_color = true + end + if occursin("Complex(", string(expr)) || occursin("imag(", string(expr)) + is_complex = true + end + end + + return (is_color, is_complex) +end + +function generate_image_basic(func, possible_types, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + vars = Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}() + + for y in 1:height + for x in 1:width + # vars = Dict{Symbol, Union{Float64, Matrix{Float64}}}(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) # Add more variables if needed, e.g., :t => time + vars[:x] = (x - 1) / (width - 1) - 0.5 + vars[:y] = (y - 1) / (height - 1) - 0.5 + + rgb = invokelatest(func, vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from evaluation: $(typeof(rgb))") + end + end + end + + clean && clean!(img) + display(img) + return img +end + +function generate_image_threaded(func, possible_types, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + vars = Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}() + + Threads.@threads for y in 1:height + for x in 1:width + # check if there are concurrent problems here, I am sure there are + vars[:x] = (x - 1) / (width - 1) - 0.5 + vars[:y] = (y - 1) / (height - 1) - 0.5 + rgb = invokelatest(func, vars) + + if rgb isa Color + img[y, x] = RGB(rgb.r, rgb.g, rgb.b) + elseif isa(rgb, Number) + img[y, x] = RGB(rgb, rgb, rgb) + else + error("Invalid type output from evaluation: $(typeof(rgb))") + end + end + end + + clean && clean!(img) + display(img) + return img +end + +function generate_image_vectorized(func, possible_types, width::Int, height::Int; clean = true) + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + img = broadcast((x, y) -> invokelatest(func, Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}(:x => x, :y => y)), X, Y) + + output = Array{RGB{Float64}, 2}(undef, height, width) + + is_color = [r isa Color for r in img] + output[is_color] = RGB.(red.(img[is_color]), green.(img[is_color]), blue.(img[is_color])) + output[.!is_color] = RGB.(img[.!is_color], img[.!is_color], img[.!is_color]) + + clean && clean!(output) + display(output) + return output +end + +function generate_image_vectorized_threaded(func, possible_types, width::Int, height::Int; clean = true, n_blocks = Threads.nthreads() * 4) + x_grid = ((1:width) .- 1) ./ (width - 1) .- 0.5 + y_grid = ((1:height) .- 1) ./ (height - 1) .- 0.5 + X, Y = meshgrid(x_grid, y_grid) + + img = Array{RGB{Float64}, 2}(undef, height, width) + + block_size = div(height, n_blocks) + Threads.@threads for block in 1:n_blocks + start_y = (block - 1) * block_size + 1 + end_y = min(block * block_size, height) + X_block = X[start_y:end_y, :] + Y_block = Y[start_y:end_y, :] + + # Vectorize within the block + img_block = broadcast((x, y) -> invokelatest(func, Dict{Symbol, Union{Float64, possible_types..., Matrix{Union{Float64, Color, ComplexF64}}}}(:x => x, :y => y)), X_block, Y_block) + + is_color = [r isa Color for r in img_block] + img_block[is_color] = RGB.(img_block[is_color]) + img_block[!is_color] = RGB.(img_block[!is_color], img_block[!is_color], img_block[!is_color]) + + img[start_y:end_y, :] = img_block # Assign the block to the full image + end + + clean && clean!(img) + display(img) + return img +end + +# Declare global variables +global width = 128 # Default width +global height = 128 # Default height + +# if geneticexpr is a number or symbol, convert it to a GeneticExpr +generate_image(geneticexpr::Union{Number, Symbol}, width::Int, height::Int; kwargs...) = generate_image(GeneticExpr(geneticexpr), width, height; kwargs...) + +# TODO: Allow for complex results, add a complex_func argument +function generate_image(geneticexpr::GeneticExpr, w::Int, h::Int; clean = true, renderer = :basic, kwargs...) + # TODO: Find a better way to pass these arguments to the function + global width = w + global height = h + + # Initialize vals as a vector of matrices with the appropriate type + vals = [] + possible_types = [] + is_color, is_complex = determine_type(geneticexpr) + if is_color + push!(vals, Matrix{Color}(undef, height, width)) + + if Matrix{Color} ∉ possible_types + push!(possible_types, Matrix{Color}) + end + elseif is_complex + push!(vals, Matrix{Complex{Float64}}(undef, height, width)) + + if Matrix{Complex{Float64}} ∉ possible_types + push!(possible_types, Matrix{Complex{Float64}}) + end + else + push!(vals, Matrix{Float64}(undef, height, width)) + + if Matrix{Float64} ∉ possible_types + push!(possible_types, Matrix{Float64}) + end + end + + func = compile_expr(geneticexpr, custom_operations, primitives_with_arity, gradient_functions, width, height) # Compile the expression + + if renderer == :basic + return generate_image_basic(func, possible_types, width, height; clean = clean) + elseif renderer == :vectorized + return generate_image_vectorized(func, possible_types, width, height; clean = clean) + elseif renderer == :threaded + return generate_image_threaded(func, possible_types, width, height; clean = clean) + elseif renderer == :vectorized_threaded + return generate_image_vectorized_threaded(func, possible_types, width, height; clean = clean, kwargs...) + else + error("Invalid renderer: $renderer") + end +end \ No newline at end of file diff --git a/src/Utils.jl b/src/Utils.jl index 071e69c..d0ee32c 100644 --- a/src/Utils.jl +++ b/src/Utils.jl @@ -40,3 +40,10 @@ function capture_function(func_expr::Expr) return strip(cleaned_str) end + +# Helper function to create meshgrid +function meshgrid(x, y) + X = repeat(reshape(x, 1, :), length(y), 1) + Y = repeat(reshape(y, :, 1), 1, length(x)) + return X, Y +end \ No newline at end of file diff --git a/test/ExprEvaluation_test.jl b/test/ExprEvaluation_test.jl index 0239432..9af9de8 100644 --- a/test/ExprEvaluation_test.jl +++ b/test/ExprEvaluation_test.jl @@ -1,98 +1,98 @@ @testset "ExprEvaluation" begin - using GeneticTextures: Color, CustomExpr, custom_eval, primitives_with_arity + using GeneticTextures: Color, GeneticExpr, custom_eval, primitives_with_arity using CoherentNoise: sample, perlin_2d vars = Dict(:x => 0.5, :y => 0.5) @testset "basic operations" begin add_expr = :(+, x, y) - ce_add = CustomExpr(add_expr) - @test custom_eval(ce_add, vars) ≈ 1.0 + ge_add = GeneticExpr(add_expr) + @test custom_eval(ge_add, vars) ≈ 1.0 sub_expr = :(-, x, y) - ce_sub = CustomExpr(sub_expr) - @test custom_eval(ce_sub, vars) ≈ 0.0 + ge_sub = GeneticExpr(sub_expr) + @test custom_eval(ge_sub, vars) ≈ 0.0 mul_expr = :(*, x, y) - ce_mul = CustomExpr(mul_expr) - @test custom_eval(ce_mul, vars) ≈ 0.25 + ge_mul = GeneticExpr(mul_expr) + @test custom_eval(ge_mul, vars) ≈ 0.25 div_expr = :(/, x, y) - ce_div = CustomExpr(div_expr) - @test custom_eval(ce_div, vars) ≈ 1.0 + ge_div = GeneticExpr(div_expr) + @test custom_eval(ge_div, vars) ≈ 1.0 end @testset "unary functions" begin sin_expr = :(sin(x)) - ce_sin = CustomExpr(sin_expr) - @test custom_eval(ce_sin, vars) ≈ sin(0.5) + ge_sin = GeneticExpr(sin_expr) + @test custom_eval(ge_sin, vars) ≈ sin(0.5) cos_expr = :(cos(x)) - ce_cos = CustomExpr(cos_expr) - @test custom_eval(ce_cos, vars) ≈ cos(0.5) + ge_cos = GeneticExpr(cos_expr) + @test custom_eval(ge_cos, vars) ≈ cos(0.5) abs_expr = :(abs(-1 * x)) - ce_abs = CustomExpr(abs_expr) - @test custom_eval(ce_abs, vars) ≈ 0.5 + ge_abs = GeneticExpr(abs_expr) + @test custom_eval(ge_abs, vars) ≈ 0.5 sqrt_expr = :(sqrt(x)) - ce_sqrt = CustomExpr(sqrt_expr) - @test custom_eval(ce_sqrt, vars) ≈ sqrt(0.5) + ge_sqrt = GeneticExpr(sqrt_expr) + @test custom_eval(ge_sqrt, vars) ≈ sqrt(0.5) exp_expr = :(exp(x)) - ce_exp = CustomExpr(exp_expr) - @test custom_eval(ce_exp, vars) ≈ exp(0.5) + ge_exp = GeneticExpr(exp_expr) + @test custom_eval(ge_exp, vars) ≈ exp(0.5) end @testset "binary functions" begin mod_expr = :(mod(3, 2)) - ce_mod = CustomExpr(mod_expr) - @test custom_eval(ce_mod, vars) ≈ 1 + ge_mod = GeneticExpr(mod_expr) + @test custom_eval(ge_mod, vars) ≈ 1 atan_expr = :(atan(x, y)) - ce_atan = CustomExpr(atan_expr) - @test custom_eval(ce_atan, vars) ≈ atan(0.5, 0.5) + ge_atan = GeneticExpr(atan_expr) + @test custom_eval(ge_atan, vars) ≈ atan(0.5, 0.5) end @testset "Color" begin color_expr = :(Color(0.5, 0.5, 0.5)) - ce_color = CustomExpr(color_expr) - @test custom_eval(ce_color, vars) ≈ Color(0.5, 0.5, 0.5) + ge_color = GeneticExpr(color_expr) + @test custom_eval(ge_color, vars) ≈ Color(0.5, 0.5, 0.5) end @testset "perlin_2d" begin perlin_expr = :(perlin_2d(1, x, y)) - ce_perlin = CustomExpr(perlin_expr) + ge_perlin = GeneticExpr(perlin_expr) sampler = perlin_2d(seed=hash(1)) - @test custom_eval(ce_perlin, vars) ≈ sample(sampler, 0.5, 0.5) + @test custom_eval(ge_perlin, vars) ≈ sample(sampler, 0.5, 0.5) end @testset "complex expressions" begin complex_expr = :(sin(cos(sqrt(/(x, y))))) - ce_complex = CustomExpr(complex_expr) - @test custom_eval(ce_complex, vars) ≈ sin(cos(sqrt(0.5 / 0.5))) + ge_complex = GeneticExpr(complex_expr) + @test custom_eval(ge_complex, vars) ≈ sin(cos(sqrt(0.5 / 0.5))) nested_expr = :(*, Color(0.5, 0.5, 0.5), +(x, y)) - ce_nested = CustomExpr(nested_expr) - @test custom_eval(ce_nested, vars) ≈ Color(0.5, 0.5, 0.5) * (0.5 + 0.5) + ge_nested = GeneticExpr(nested_expr) + @test custom_eval(ge_nested, vars) ≈ Color(0.5, 0.5, 0.5) * (0.5 + 0.5) end @testset "edge cases" begin zero_expr = :(/, x, 0) - ce_zero = CustomExpr(zero_expr) - @test custom_eval(ce_zero, vars) ≈ Inf + ge_zero = GeneticExpr(zero_expr) + @test custom_eval(ge_zero, vars) ≈ Inf large_expr = :(*, 1e7, x) - ce_large = CustomExpr(large_expr) - @test custom_eval(ce_large, vars) ≈ 0.5 + ge_large = GeneticExpr(large_expr) + @test custom_eval(ge_large, vars) ≈ 0.5 negative_large_expr = :(*, -1e7, x) - ce_negative_large = CustomExpr(negative_large_expr) - @test custom_eval(ce_negative_large, vars) ≈ -0.5 + ge_negative_large = GeneticExpr(negative_large_expr) + @test custom_eval(ge_negative_large, vars) ≈ -0.5 nan_expr = :(+, x, (0 / 0)) - ce_nan = CustomExpr(nan_expr) - @test custom_eval(ce_nan, vars) ≈ 0.5 + ge_nan = GeneticExpr(nan_expr) + @test custom_eval(ge_nan, vars) ≈ 0.5 end end \ No newline at end of file diff --git a/test/ExprGenerators_test.jl b/test/ExprGenerators_test.jl index ae03b89..d9a3ce5 100644 --- a/test/ExprGenerators_test.jl +++ b/test/ExprGenerators_test.jl @@ -1,15 +1,15 @@ @testset "ExprGenerators" begin - using GeneticTextures: CustomExpr, random_expr, random_function, grad_dir, primitives_with_arity, depth_of_expr + using GeneticTextures: GeneticExpr, random_expr, random_function, grad_dir, primitives_with_arity, depth_of_expr max_depth = 5 @testset "random_expr" begin for _ in 1:20 - c_expr = random_expr(primitives_with_arity, max_depth) + geneticexpr = random_expr(primitives_with_arity, max_depth) - if c_expr isa CustomExpr - @test c_expr.expr isa Union{Expr, Number, Color, Symbol} + if geneticexpr isa GeneticExpr + @test geneticexpr.expr isa Union{Expr, Number, Color, Symbol} else - @test c_expr isa Union{Number, Color, Symbol} + @test geneticexpr isa Union{Number, Color, Symbol} end end end