From 4ba454651bc4e244adf4516487516c90ae5cc10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:07:49 +0200 Subject: [PATCH 01/31] Update Color.jl to work with Base functions --- src/Color.jl | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Color.jl b/src/Color.jl index bd89635..4a6737b 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -1,5 +1,6 @@ using Base using Base.Broadcast: BroadcastStyle, DefaultArrayStyle, broadcasted +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 # 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} @@ -96,4 +97,25 @@ end using Colors Colors.RGB(c::GeneticTextures.Color) = RGB(c.r, c.g, c.b) -Color(val::Colors.RGB) = Color(val.r, val.g, val.b) \ No newline at end of file +Color(val::Colors.RGB) = Color(val.r, val.g, val.b) + +unary_functions = [sin, cos, tan, sqrt, exp, log, abs, 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 From d9236648e268ea17363de4efe190f34c95944e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:08:22 +0200 Subject: [PATCH 02/31] Add comment in PerlinBroadcasting.jl --- src/PerlinBroadcasting.jl | 1 + 1 file changed, 1 insertion(+) 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() From 8535ad5dde245b2025b453a35d40ebf69bc9b53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:08:59 +0200 Subject: [PATCH 03/31] Rename CustomExpr to GeneticExpr, refactor the internal --- src/{CustomExpr.jl => GeneticExpr.jl} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/{CustomExpr.jl => GeneticExpr.jl} (83%) 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[] From f071c759206549a7faf54418492bf070a9f13b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:09:36 +0200 Subject: [PATCH 04/31] Refactor custom math functions to work with new internals --- src/MathOperations.jl | 363 +++++++++++++++++++++++------------------- 1 file changed, 202 insertions(+), 161 deletions(-) diff --git a/src/MathOperations.jl b/src/MathOperations.jl index 48a958e..c670e41 100644 --- a/src/MathOperations.jl +++ b/src/MathOperations.jl @@ -1,37 +1,34 @@ -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). - """ - - 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,32 +77,80 @@ 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±Δ). +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 = (vars[:x]+0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y]+0.5) * (height-1) + 1 |> trunc |> Int + idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int + idx_y = (y_val + 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) + 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 - 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 + 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 - 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 + x_plus = func(merge(vars, Dict(:x => x_val + Δx_scaled, :y => y_val))) + ∇x = (x_plus - center_val) / Δx_scaled^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 + x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + ∇x = (center_val - x_minus) / Δx_scaled^2 end end @@ -113,159 +158,155 @@ function laplacian(expr, vars, width, height; Δx = 1, Δy = 1) ∇y = 0 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 + 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 - 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 + y_plus = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + ∇y = (y_plus - center_val) / Δy_scaled^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 + y_minus = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + ∇y = (center_val - y_minus) / Δy_scaled^2 end 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 +# 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] - 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 = func(vars) # Directly use vars, no need to merge if x, y are already set - 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 + # 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 -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 + # Evaluate neighborhood + for dx in range_x, dy in range_y + if dx == 0 && dy == 0 + continue + 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) + temp_vars[:x] = x_val + dx / (width - 1) + temp_vars[:y] = y_val + dy / (height - 1) - if Δy == 0 - return 0 - 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 + val = func(temp_vars) + if val < min_val + min_val = val end 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) + return min_val 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 +# 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] -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) + max_val = func(vars) - idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + # Temporary variables to avoid repeated dictionary updates + temp_vars = copy(vars) - 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 + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy - 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 - end - end + 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 - return min_val -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) -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) - - 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) - max_val = center - - 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 isreal(val) && isreal(max_val) - if val > max_val - max_val = val - end - else - if abs(val) > abs(max_val) - max_val = val - end - end + val = func(temp_vars) + if val > max_val + max_val = val end end 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) +# 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) - idx_x = (vars[:x] + 0.5) * (width-1) + 1 |> trunc |> Int - idx_y = (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> Int + # if there are any Matrix in vars values, then filter the iterations + range_x = -Δx:Δx + range_y = -Δy:Δy - 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 + 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 - 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 + # 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) + + sum_val += func(temp_vars) + count += 1 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_max => neighbor_max, + :neighbor_ave => neighbor_ave +) \ No newline at end of file From ae843e4c05be4045ec0175f44ad34a98c9fc58a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:10:20 +0200 Subject: [PATCH 05/31] Refactor ExprEvaluation.jl code, now much more performant --- src/ExprEvaluation.jl | 357 +++++++++++++++--------------------------- 1 file changed, 123 insertions(+), 234 deletions(-) diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index 5bf0874..12d154f 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -1,260 +1,149 @@ 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[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> 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] + caller_func = positional_args[1] - 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 - - 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[:x] + 0.5) * (width-1) + 1 |> trunc |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> trunc |> 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) -> $expr )) end +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()) \ No newline at end of file From 371aa2a69a10eb576398fc223091ced81b17cb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:10:55 +0200 Subject: [PATCH 06/31] Refactor render functions to work with new internals --- src/Renderer.jl | 149 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 30 deletions(-) diff --git a/src/Renderer.jl b/src/Renderer.jl index 40a2327..89c0be9 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -1,33 +1,9 @@ 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? - 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] @@ -50,11 +26,9 @@ function clean!(img) return img end -function save_image_and_expr(img::Matrix{T}, custom_expr::CustomExpr; folder = "saves", prefix = "images") where {T} +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 - if !isdir(folder) - mkdir(folder) - end + !isdir(folder) && mkdir(folder) # Generate a unique filename filename = generate_unique_filename(folder, prefix) @@ -66,9 +40,124 @@ 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 + +function generate_image_basic(func, 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 = invokelatest(func, vars) + + if rgb isa GeneticTextures.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) + return img +end + +function generate_image_threaded(func, width::Int, height::Int; clean = true) + img = Array{RGB{Float64}, 2}(undef, height, width) + + Threads.@threads 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 = 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) + return img +end + +function generate_image_vectorized(func, 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(:x => x, :y => y)), X, Y) + + is_color = [r isa Color for r in img] + img[is_color] = RGB.(img[is_color]) + img[!is_color] = RGB.(img[!is_color], img[!is_color], img[!is_color]) + + clean && clean!(img) + return img +end + +function generate_image_vectorized_threaded(func, 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(: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) + 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 = :threaded, kwargs...) + # TODO: Find a better way to pass these arguments to the function + global width = w + global height = h + + func = compile_expr(geneticexpr, custom_operations, primitives_with_arity, gradient_functions, width, height) # Compile the expression + + if renderer == :basic + return generate_image_basic(func, width, height; clean = clean) + elseif renderer == :vectorized + return generate_image_vectorized(func, width, height; clean = clean) + elseif renderer == :threaded + return generate_image_threaded(func, width, height; clean = clean) + elseif renderer == :vectorized_threaded + return generate_image_vectorized_threaded(func, width, height; clean = clean, kwargs...) + else + error("Invalid renderer: $renderer") + end +end \ No newline at end of file From d1992ec4fb0e3fd1df852b1d4db24a737239451e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:11:36 +0200 Subject: [PATCH 07/31] Update Genetic.jl, change CustomExpr to GeneticExpr --- src/Genetic.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From 183a9fe226742157da753d52e8b5d8f618f92e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:15:33 +0200 Subject: [PATCH 08/31] Refactor DynamicalSystems.jl with new internals, enhance code --- src/DynamicalSystems.jl | 372 ++++++++++++++++++---------------------- 1 file changed, 171 insertions(+), 201 deletions(-) diff --git a/src/DynamicalSystems.jl b/src/DynamicalSystems.jl index 58f0646..b121ea0 100644 --- a/src/DynamicalSystems.jl +++ b/src/DynamicalSystems.jl @@ -1,15 +1,15 @@ 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, Color} + δF::Union{GeneticExpr, Symbol, Number, Color} function VariableDynamics(name, F_0, δF) - return new(name, CustomExpr(F_0), CustomExpr(δF)) + return new(name, GeneticExpr(F_0), GeneticExpr(δF)) end end @@ -24,152 +24,47 @@ 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 - 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 - - # Update A and B - A += dA - B += dB +function evolve_system!(vals, dynamics::DynamicalSystem, genetic_funcs, width, height, t, dt, complex_func::Function; renderer = :threaded, kwargs...) + if renderer == :basic + return evolve_system_basic!(vals, dynamics, genetic_funcs, 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 - - # 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 - - img end +function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normalize_img = false, 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) -function evolve_system_step!(vars, dynamics::DynamicalSystem, width, height, t, dt) - δvars = [zeros(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 - - variable_dict[:x] = x - variable_dict[:y] = y - - for (i, ds) in enumerate(dynamics) - δvars[i][y_pixel, x_pixel] = dt * custom_eval(δF(ds), variable_dict, width, height) - end - end - end - - # Update vars - for (i, ds) in enumerate(dynamics) - vars[i] += δvars[i] - end - - return vars -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)] + init_funcs = [compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height) for ds in dynamics] - variable_dict = merge(Dict(:t => t), Dict(name(ds) => vars[i] for (i, ds) in enumerate(dynamics))) + vals = [Matrix{Color}(undef, height, width) for _ in 1:length(dynamics)] # Initialize each vars' grid using their F_0 expression + 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 - variable_dict[:x] = x - variable_dict[:y] = y - - for (i, ds) in enumerate(dynamics) - val = dt .* custom_eval(δF(ds), variable_dict, width, height) + for i in 1:length(dynamics) + vars = Dict(:x => x, :y => y, :t => t) + val = invokelatest(init_funcs[i], vars) if val isa Color - δvars[i][y_pixel, x_pixel] = val - elseif isreal(val) - δvars[i][y_pixel, x_pixel] = Color(val, val, val) + vals[i][y_pixel, x_pixel] = val else - δvars[i][y_pixel, x_pixel] = Color(invokelatest(complex_func, val), invokelatest(complex_func, val), invokelatest(complex_func, val)) + 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 - # 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) - -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) - end - end - end - # Generate a unique filename base_dir = "saves" if !isdir(base_dir) @@ -190,7 +85,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 +100,200 @@ 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) + + # 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, 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] + + 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 + + # if plot, display(img) + 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, 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))) - # 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) + 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 +# 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 = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) - mkdir(animation_dir) + 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 - # 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") + for i in 1:length(dynamics) + val = dt .* invokelatest(genetic_funcs[i], merge(vars, Dict(:x => x, :y => y))) + + 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 - 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 + # 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 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] - img[y_pixel, x_pixel] = - invokelatest(color_func, [isreal(val) ? val : invokelatest(complex_func, val) for val in values]...) - end - end + # 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_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 = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(dynamics))) + + # 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 - if normalize_img - img = clean!(img) + # Update vals + for i in 1:length(dynamics) + vals[i] += δvals[i] + end + + return vals +end + +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)] + + # 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 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 From 8218da146e22cc6583559dc9417abd1297bc25e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:15:48 +0200 Subject: [PATCH 09/31] Update with new exports --- src/GeneticTextures.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 From 3897ae9b438151debf2264eb13c38a958a771bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:16:26 +0200 Subject: [PATCH 10/31] Add meshgrid function to function utils --- src/Utils.jl | 7 +++++++ 1 file changed, 7 insertions(+) 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 From 8d09337db89a318923f3073c9207553634e9405a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:16:53 +0200 Subject: [PATCH 11/31] Add WIP code in ExprGenerators.jl --- src/ExprGenerators.jl | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/ExprGenerators.jl b/src/ExprGenerators.jl index 303736a..79985f8 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, + :real => 1, + :imag => 1, + :A => -1, + :B => -1, + :C => -1, + :D => -1, :t => 0 ) # special_funcs take not only numbers as arguments const special_funcs = ( - :grad_mag, - :grad_dir, :blur, :perlin_2d, :perlin_color @@ -71,7 +72,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) @@ -124,18 +125,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] From 0b9fcd6efc95e088077af11e0b5ebdb2d52d7cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 25 Apr 2024 20:17:22 +0200 Subject: [PATCH 12/31] Update tests with new internals --- test/ExprEvaluation_test.jl | 78 ++++++++++++++++++------------------- test/ExprGenerators_test.jl | 10 ++--- 2 files changed, 44 insertions(+), 44 deletions(-) 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 From de5f8f24d9dc37c09fcba5d5b8933e4b62a89777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Fri, 26 Apr 2024 17:42:02 +0200 Subject: [PATCH 13/31] Replace trunc with round to fix small precision problems --- src/ExprEvaluation.jl | 2 +- src/MathOperations.jl | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index 12d154f..ed442ff 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -54,7 +54,7 @@ function convert_expr(expr, custom_operations, primitives_with_arity, gradient_f 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]) + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> round |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> round |> Int]) else return expr end diff --git a/src/MathOperations.jl b/src/MathOperations.jl index c670e41..15fa6f3 100644 --- a/src/MathOperations.jl +++ b/src/MathOperations.jl @@ -81,7 +81,7 @@ 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 + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int # Evaluate function at x center_val = func(merge(vars, Dict(:x => x_val))) @@ -99,7 +99,7 @@ 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 + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int # Evaluate function at y center_val = func(merge(vars, Dict(:y => y_val))) @@ -133,8 +133,8 @@ function laplacian(func, vars, width, height; Δx = 1, Δy = 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 - idx_x = (x_val + 0.5) * (width - 1) + 1 |> trunc |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> trunc |> Int + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) @@ -190,8 +190,8 @@ function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) 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 + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int # Filter the iterations that are not in the matrix range_x = filter(x -> 1 <= idx_x + x <= width, range_x) @@ -232,8 +232,8 @@ function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) 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 + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int # Filter the iterations that are not in the matrix range_x = filter(x -> 1 <= idx_x + x <= width, range_x) @@ -275,8 +275,8 @@ function neighbor_ave(func, vars, width, height; Δx = 1, Δy = 1) 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 + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int # Filter the iterations that are not in the matrix range_x = filter(x -> 1 <= idx_x + x <= width, range_x) From 1a6a35b0700c0b8a4a6477f4c549d855f74ada52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:35:49 +0200 Subject: [PATCH 14/31] Add adjust_brightness function --- src/Renderer.jl | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Renderer.jl b/src/Renderer.jl index 89c0be9..264b151 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -25,6 +25,33 @@ 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 + + # 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 From 6bd833089b2b62e5c8856aa669e110a505141ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:36:01 +0200 Subject: [PATCH 15/31] Minor fixes in Renderer.jl --- src/Renderer.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Renderer.jl b/src/Renderer.jl index 264b151..a59ea35 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -1,5 +1,6 @@ using Images, Colors using CoherentNoise: perlin_2d +using Statistics clean!(img::Matrix{Color}) = clean!(RGB.(img)) # convert img to RGB and call clean! again @@ -82,7 +83,7 @@ function generate_image_basic(func, width::Int, height::Int; clean = true) 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 = invokelatest(func, vars) - if rgb isa GeneticTextures.Color + 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) @@ -93,6 +94,7 @@ function generate_image_basic(func, width::Int, height::Int; clean = true) end clean && clean!(img) + display(img) return img end @@ -115,6 +117,7 @@ function generate_image_threaded(func, width::Int, height::Int; clean = true) end clean && clean!(img) + display(img) return img end @@ -126,10 +129,11 @@ function generate_image_vectorized(func, width::Int, height::Int; clean = true) img = broadcast((x, y) -> invokelatest(func, Dict(:x => x, :y => y)), X, Y) is_color = [r isa Color for r in img] - img[is_color] = RGB.(img[is_color]) + img[is_color] = RGB.(red.(img[is_color]), green.(img[is_color]), blue.(img[is_color])) img[!is_color] = RGB.(img[!is_color], img[!is_color], img[!is_color]) clean && clean!(img) + display(img) return img end @@ -158,6 +162,7 @@ function generate_image_vectorized_threaded(func, width::Int, height::Int; clean end clean && clean!(img) + display(img) return img end From 44ba11be55322343c782cb5d3a5b0cc60295c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:36:34 +0200 Subject: [PATCH 16/31] Minor fixes in DynamicalSystems.jl --- src/DynamicalSystems.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/DynamicalSystems.jl b/src/DynamicalSystems.jl index b121ea0..39cf19b 100644 --- a/src/DynamicalSystems.jl +++ b/src/DynamicalSystems.jl @@ -24,7 +24,11 @@ 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!(vals, dynamics::DynamicalSystem, genetic_funcs, width, height, t, dt, complex_func::Function; renderer = :threaded, kwargs...) +function evolve_system!(vals, dynamics::DynamicalSystem, genetic_funcs, 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, width, height, t, dt, complex_func) elseif renderer == :threaded @@ -38,7 +42,7 @@ function evolve_system!(vals, dynamics::DynamicalSystem, genetic_funcs, width, h end end -function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normalize_img = false, 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))) +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) @@ -122,9 +126,9 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normali end end - normalize_img && GeneticTextures.clean!(img) # Clean the image if requested + normalize_img && clean!(img) # Clean the image if requested + adjust_brighness && adjust_brightness!(img) # Adjust the brightness if requested - # if plot, display(img) plot && display(img) frame_file = animation_dir * "/frame_$(lpad(i, 5, '0')).png" From b36541fdcc80c8a78fd6648b234105f9cbad8a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:37:13 +0200 Subject: [PATCH 17/31] Minor fixes in Colors.jl --- src/Color.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Color.jl b/src/Color.jl index 4a6737b..74fa762 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -1,6 +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 -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 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} @@ -94,8 +95,7 @@ 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) From d1dd2037aa7c447c2973384655d7997c7d82daea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:38:20 +0200 Subject: [PATCH 18/31] Minor fix in Colors.jl --- src/Color.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Color.jl b/src/Color.jl index 74fa762..02d21e5 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -99,7 +99,7 @@ Colors.RGB(c::Color) = RGB(c.r, c.g, c.b) Color(val::Colors.RGB) = Color(val.r, val.g, val.b) -unary_functions = [sin, cos, tan, sqrt, exp, log, abs, asin, acos, atan, sinh, cosh, tanh, sech, csch, coth, asec, acsc, acot, sec, csc, cot, mod, rem, fld, cld, ceil, floor, round] +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 From 3c70fb922daf47f94c86055370c20a30ee2d1129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 13:43:46 +0200 Subject: [PATCH 19/31] Add fixes in ExprEvaluation.jl --- src/ExprEvaluation.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index ed442ff..372fad5 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -127,7 +127,7 @@ function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_ari 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]) + return :(vars[$(QuoteNode(expr))][(vars[:x] + 0.5) * (width-1) + 1 |> round |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> round |> Int]) else return expr end @@ -142,7 +142,7 @@ function compile_expr(expr::Expr, custom_operations::Dict, 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 )) + return eval(:( (vars::Dict) -> $expr )) end compile_expr(expr::GeneticExpr, custom_operations::Dict, primitives_with_arity::Dict, gradient_functions::Dict, width, height) = From 7a429161b4aaaebc29086b7dddaf037814c9ec1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 20:30:17 +0200 Subject: [PATCH 20/31] Fix generate_image_vectorized --- src/Renderer.jl | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Renderer.jl b/src/Renderer.jl index a59ea35..a3a9ce2 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -128,13 +128,15 @@ function generate_image_vectorized(func, width::Int, height::Int; clean = true) img = broadcast((x, y) -> invokelatest(func, Dict(:x => x, :y => y)), X, Y) + output = Array{RGB{Float64}, 2}(undef, height, width) + is_color = [r isa Color for r in img] - img[is_color] = RGB.(red.(img[is_color]), green.(img[is_color]), blue.(img[is_color])) - img[!is_color] = RGB.(img[!is_color], img[!is_color], img[!is_color]) + 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!(img) - display(img) - return img + clean && clean!(output) + display(output) + return output end function generate_image_vectorized_threaded(func, width::Int, height::Int; clean = true, n_blocks = Threads.nthreads() * 4) From d728fb9920421521e0da357242c4934ef85b2e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Tue, 30 Apr 2024 20:30:49 +0200 Subject: [PATCH 21/31] Add f function --- src/ExprEvaluation.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index 372fad5..ae3a2cf 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -145,5 +145,8 @@ function compile_expr(expr::Expr, custom_operations::Dict, primitives_with_arity 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()) \ No newline at end of file From 4eea0014083cd72e3313a0bbb1c788e76506b72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 2 May 2024 08:41:30 +0200 Subject: [PATCH 22/31] Extend Base simple functions for proper Expr evaluation --- src/Color.jl | 8 ++++++++ src/ExprEvaluation.jl | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Color.jl b/src/Color.jl index 02d21e5..25fb618 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -119,3 +119,11 @@ for func in binary_functions ($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.) + +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/ExprEvaluation.jl b/src/ExprEvaluation.jl index ae3a2cf..e9e6a6f 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -149,4 +149,8 @@ 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()) \ No newline at end of file + 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) \ No newline at end of file From db901dab716e3502c2d29b6cec49e242ba39d8ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Thu, 2 May 2024 08:42:14 +0200 Subject: [PATCH 23/31] Optimize core math functions by using @inbounds and removing merge --- src/MathOperations.jl | 202 ++++++++++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 74 deletions(-) diff --git a/src/MathOperations.jl b/src/MathOperations.jl index 15fa6f3..77b383f 100644 --- a/src/MathOperations.jl +++ b/src/MathOperations.jl @@ -84,13 +84,16 @@ function x_grad(func, vars, width, height; Δx = 1) idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int # Evaluate function at x - center_val = func(merge(vars, Dict(:x => x_val))) + vars[:x] = x_val + center_val = func(vars) if idx_x == width - x_minus_val = func(merge(vars, Dict(:x => x_val - Δx_scaled))) # Evaluate function at x - Δx + vars[:x] = x_val - Δx_scaled + x_minus_val = func(vars) # 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 + vars[:x] = x_val + Δx_scaled + x_plus_val = func(vars) # Evaluate function at x + Δx return (x_plus_val - center_val) / Δx_scaled end end @@ -102,14 +105,17 @@ function y_grad(func, vars, width, height; Δy = 1) idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int # Evaluate function at y - center_val = func(merge(vars, Dict(:y => y_val))) + vars[:y] = y_val + center_val = func(vars) # Compute the finite difference if idx_y == height - y_minus = func(merge(vars, Dict(:y => y_val - Δy_scaled))) # Evaluate function at y - Δy + vars[:y] = y_val - Δy_scaled + y_minus = func(vars) # 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 + vars[:y] = y_val + Δy_scaled + y_plus_val = func(vars) # Evaluate function at y + Δy return (y_plus_val - center_val) / Δy_scaled end end @@ -136,20 +142,25 @@ function laplacian(func, vars, width, height; Δx = 1, Δy = 1) idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int - center_val = func(merge(vars, Dict(:x => x_val, :y => y_val))) + center_val = func(vars) if Δx == 0 ∇x = 0 else + vars[:y] = y_val 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))) + vars[:x] = x_val + Δx_scaled + x_plus_val = func(vars) + vars[:x] = x_val - Δx_scaled + x_minus_val = func(vars) ∇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, :y => y_val))) + vars[:x] = x_val + Δx_scaled + x_plus = func(vars) ∇x = (x_plus - center_val) / Δx_scaled^2 else # idx_x == width - x_minus = func(merge(vars, Dict(:x => x_val - Δx_scaled, :y => y_val))) + vars[:x] = x_val - Δx_scaled + x_minus = func(vars) ∇x = (center_val - x_minus) / Δx_scaled^2 end end @@ -157,15 +168,20 @@ function laplacian(func, vars, width, height; Δx = 1, Δy = 1) if Δy == 0 ∇y = 0 else + vars[:x] = x_val 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))) + vars[:y] = y_val + Δy_scaled + y_plus_val = func(vars) + vars[:y] = y_val - Δy_scaled + y_minus_val = func(vars) ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 elseif idx_y == 1 - y_plus = func(merge(vars, Dict(:x => x_val, :y => y_val + Δy_scaled))) + vars[:y] = y_val + Δy_scaled + y_plus = func(vars) ∇y = (y_plus - center_val) / Δy_scaled^2 else # idx_y == height - y_minus = func(merge(vars, Dict(:x => x_val, :y => y_val - Δy_scaled))) + vars[:y] = y_val - Δy_scaled + y_minus = func(vars) ∇y = (center_val - y_minus) / Δy_scaled^2 end end @@ -175,39 +191,39 @@ 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 + # Extract x and y values directly 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) + # Pre-calculate positions for x and y in the array/matrix + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) - # if there are any Matrix in vars values, then filter the iterations - range_x = -Δx:Δx - range_y = -Δy:Δy + # Initialize min_val + min_val = func(vars) + # 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) - if any([isa(v, Matrix) for v in values(vars)]) - idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int + # Calculate adjusted ranges to avoid division by zero in the loop + range_x = (min_x:max_x) .- idx_x + range_y = (min_y:max_y) .- idx_y - # 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 + # Loop through the neighborhood + @inbounds 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) + # Adjust the temp_vars for each iteration + vars[:x] = x_val + dx / (width - 1) + vars[:y] = y_val + dy / (height - 1) - val = func(temp_vars) + # Evaluate the function and update min_val + val = func(vars) if val < min_val min_val = val end @@ -216,40 +232,76 @@ function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) 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) +# 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) # Initialize the center values x_val = vars[:x] y_val = vars[:y] - max_val = func(vars) + 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 |> round |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int + # Calculate pixel indices for the center point + idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int + idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> 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) + # Evaluate within a circular neighborhood + 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 + temp_vars[:x] = (new_x - 1) / (width - 1) - 0.5 + temp_vars[:y] = (new_y - 1) / (height - 1) - 0.5 + + val = func(temp_vars) + if val < min_val + min_val = val + end + end + end end - # Evaluate neighborhood - for dx in range_x, dy in range_y + return min_val +end + +function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) + # Extract x and y values directly + x_val = vars[:x] + y_val = vars[:y] + + # Pre-calculate positions for x and y in the array/matrix + 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 + max_val = func(vars) + + # 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) + + # Calculate adjusted ranges to avoid division by zero in the loop + range_x = (min_x:max_x) .- idx_x + range_y = (min_y:max_y) .- idx_y + + # Loop through the neighborhood + @inbounds 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) + # Adjust the temp_vars for each iteration + vars[:x] = x_val + dx / (width - 1) + vars[:y] = y_val + dy / (height - 1) - val = func(temp_vars) + # Evaluate the function and update max_val + val = func(vars) if val > max_val max_val = val end @@ -260,39 +312,40 @@ 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 + # Extract x and y values directly x_val = vars[:x] y_val = vars[:y] + # Pre-calculate positions for x and y in the array/matrix + idx_x = round(Int, (x_val + 0.5) * (width - 1) + 1) + idx_y = round(Int, (y_val + 0.5) * (height - 1) + 1) + + # Initialize sum and count 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 + # 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) - if any([isa(v, Matrix) for v in values(vars)]) - idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> 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 + # Calculate adjusted ranges to avoid division by zero in the loop + range_x = (min_x:max_x) .- idx_x + range_y = (min_y:max_y) .- idx_y - # Evaluate neighborhood - for dx in range_x, dy in range_y + # Loop through the neighborhood + @inbounds 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) + # Adjust the temp_vars for each iteration + vars[:x] = x_val + dx / (width - 1) + vars[:y] = y_val + dy / (height - 1) - sum_val += func(temp_vars) + # Add the function evaluation to sum_val + sum_val += func(vars) count += 1 end @@ -307,6 +360,7 @@ gradient_functions = Dict( :y_grad => y_grad, :laplacian => laplacian, :neighbor_min => neighbor_min, + :neighbor_min_radius => neighbor_min_radius, :neighbor_max => neighbor_max, :neighbor_ave => neighbor_ave ) \ No newline at end of file From 4687ce2f01e0942da7c9e942762a3ebbdc6a6eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:40:23 +0200 Subject: [PATCH 24/31] Update color functions --- src/Color.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Color.jl b/src/Color.jl index 25fb618..b0de650 100644 --- a/src/Color.jl +++ b/src/Color.jl @@ -15,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 @@ -98,6 +100,8 @@ end 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] From 35131ab91ca260f14ea3e02b50c8c417d64799e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:40:38 +0200 Subject: [PATCH 25/31] Update DynamicalSystems.jl --- src/DynamicalSystems.jl | 240 +++++++++++++++++++++++++++++++++++----- 1 file changed, 210 insertions(+), 30 deletions(-) diff --git a/src/DynamicalSystems.jl b/src/DynamicalSystems.jl index 39cf19b..b5e40fd 100644 --- a/src/DynamicalSystems.jl +++ b/src/DynamicalSystems.jl @@ -5,11 +5,16 @@ using ProgressMeter struct VariableDynamics name::Symbol - F_0::Union{GeneticExpr, 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, GeneticExpr(F_0), GeneticExpr(δ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,13 +29,13 @@ 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!(vals, dynamics::DynamicalSystem, genetic_funcs, w, h, t, dt, complex_func::Function; renderer = :threaded, kwargs...) +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, width, height, t, dt, complex_func) + 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 @@ -42,14 +47,131 @@ function evolve_system!(vals, dynamics::DynamicalSystem, genetic_funcs, w, h, t, end end +contains_color_code(ge::GeneticExpr) = contains_color_code(ge.expr) + +# Check method's source code for color-specific logic (this is just a placeholder) +contains_color_code(e::Expr) = occursin("Color(", string(e)) + +contains_complex_code(ge::GeneticExpr) = contains_complex_code(ge.expr) + +# 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)) + + +using ExprTools + +# 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 + + # 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 + + 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 + + # 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 + + # 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) + + # Combine results from both initial and dynamic function checks + return (is_color_F0 || is_color_δF, is_complex_F0 || is_complex_δF) +end + 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) init_funcs = [compile_expr(F_0(ds), custom_operations, primitives_with_arity, gradient_functions, width, height) 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 + # 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)) + + 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 + end + t = 0. + + # 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] for x_pixel in 1:width for y_pixel in 1:height @@ -57,14 +179,22 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normali y = (y_pixel - 1) / (height - 1) - 0.5 for i in 1:length(dynamics) - vars = Dict(:x => x, :y => y, :t => t) - val = invokelatest(init_funcs[i], vars) + 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) - 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 + + # 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 @@ -110,10 +240,32 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normali 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)) - vals = evolve_system!(vals, dynamics, genetic_funcs, width, height, t, dt, complex_func; renderer = renderer) # Evolve the system + 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) @@ -121,6 +273,9 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normali 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 @@ -152,9 +307,25 @@ function animate_system(dynamics::DynamicalSystem, width, height, T, dt; normali println("Expressions saved to: $expr_file") end -function evolve_system_basic!(vals, dynamics::DynamicalSystem, genetic_funcs, 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))) +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 for x_pixel in 1:width for y_pixel in 1:height @@ -162,23 +333,29 @@ function evolve_system_basic!(vals, dynamics::DynamicalSystem, genetic_funcs, wi 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))) + vars[:x] = x + vars[:y] = y + val = dt .* invokelatest(genetic_funcs[i], 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 + 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 + # # Update vals + # for i in 1:length(dynamics) + # vals[i] += δvals[i] + # end return vals end @@ -186,7 +363,8 @@ 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 = merge(Dict(:t => t), Dict(name(ds) => vals[i] for (i, ds) in enumerate(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 @@ -243,7 +421,8 @@ function evolve_system_vectorized!(vals, dynamics::DynamicalSystem, genetic_func # 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))) + 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) @@ -270,7 +449,8 @@ function evolve_system_vectorized_threaded!(vals, dynamics::DynamicalSystem, gen 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))) + 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 From 5c03c864d6c51cd3e3073de306e43813c66c0b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:40:59 +0200 Subject: [PATCH 26/31] update Expr functions --- src/ExprEvaluation.jl | 13 ++++++++++--- src/ExprGenerators.jl | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/ExprEvaluation.jl b/src/ExprEvaluation.jl index e9e6a6f..9e36166 100644 --- a/src/ExprEvaluation.jl +++ b/src/ExprEvaluation.jl @@ -54,7 +54,7 @@ function convert_expr(expr, custom_operations, primitives_with_arity, gradient_f 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 |> round |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> round |> Int]) + 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 @@ -127,7 +127,7 @@ function compile_expr(expr::Symbol, custom_operations::Dict, primitives_with_ari 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 |> round |> Int, (vars[:y] + 0.5) * (height-1) + 1 |> round |> Int]) + 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 @@ -153,4 +153,11 @@ compile_expr(expr::GeneticExpr, custom_operations::Dict, primitives_with_arity:: 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) \ No newline at end of file +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 79985f8..03890a4 100644 --- a/src/ExprGenerators.jl +++ b/src/ExprGenerators.jl @@ -46,7 +46,7 @@ const primitives_with_arity = Dict( :B => -1, :C => -1, :D => -1, - :t => 0 + :t => -1 ) # special_funcs take not only numbers as arguments @@ -64,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, @@ -94,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] From 526b43a5bd5461c2485004f37ba7f9ef1453f57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:41:16 +0200 Subject: [PATCH 27/31] Update boolean functions --- src/MathOperations.jl | 341 ++++++++++++++++++++++++------------------ 1 file changed, 193 insertions(+), 148 deletions(-) diff --git a/src/MathOperations.jl b/src/MathOperations.jl index 77b383f..3143650 100644 --- a/src/MathOperations.jl +++ b/src/MathOperations.jl @@ -5,6 +5,15 @@ ternary(cond::Float64, x, y) = Bool(cond) ? x : y # If cond is a float, convert threshold(x, t = 0.5) = x >= t ? 1 : 0 +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) + function rand_scalar(args...) if length(args) == 0 return rand(1) |> first @@ -77,45 +86,50 @@ function gaussian_kernel(size, sigma) return kernel end +# TODO: Remove kwargs? Right now only works for Δx = 1 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 + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> 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) + Δx_scaled = Δx / (width - 1) - # Evaluate function at x - vars[:x] = x_val - center_val = func(vars) + center_val = computed_values[idx_y, idx_x] if idx_x == width - vars[:x] = x_val - Δx_scaled - x_minus_val = func(vars) # Evaluate function at x - Δx + x_minus_val = computed_values[idx_y, idx_x - Δx] return (center_val - x_minus_val) / Δx_scaled else - vars[:x] = x_val + Δx_scaled - x_plus_val = func(vars) # Evaluate function at x + Δx + x_plus_val = computed_values[idx_y, idx_x + Δx] return (x_plus_val - center_val) / Δx_scaled end end +# TODO: Remove kwargs? Right now only works for Δy = 1 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 + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> 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) - # Evaluate function at y - vars[:y] = y_val - center_val = func(vars) + center_val = computed_values[idx_y, idx_x] - # Compute the finite difference if idx_y == height - vars[:y] = y_val - Δy_scaled - y_minus = func(vars) # Evaluate function at y - Δy - return (center_val - y_minus) / Δy_scaled + y_minus_val = computed_values[idx_y - Δy, idx_x] + return (center_val - y_minus_val) / Δy_scaled else - vars[:y] = y_val + Δy_scaled - y_plus_val = func(vars) # Evaluate function at y + Δy + y_plus_val = computed_values[idx_y + Δy, idx_x] return (y_plus_val - center_val) / Δy_scaled end end @@ -132,75 +146,55 @@ function grad_direction(expr, vars, width, height; Δx = 1, Δy = 1) return atan.(∂f_∂y, ∂f_∂x) end +# TODO: Remove kwargs? Right now only works for Δx = Δy = 1 function laplacian(func, vars, width, height; Δx = 1, Δy = 1) - x_val = vars[:x] - y_val = vars[:y] + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - Δ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 + 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 = (x_val + 0.5) * (width - 1) + 1 |> round |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int + Δ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(vars) + # 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 - ∇x = 0 - else - vars[:y] = y_val - if idx_x > 1 && idx_x < width - vars[:x] = x_val + Δx_scaled - x_plus_val = func(vars) - vars[:x] = x_val - Δx_scaled - x_minus_val = func(vars) - ∇x = (x_plus_val + x_minus_val - 2 * center_val) / Δx_scaled^2 - elseif idx_x == 1 - vars[:x] = x_val + Δx_scaled - x_plus = func(vars) - ∇x = (x_plus - center_val) / Δx_scaled^2 - else # idx_x == width - vars[:x] = x_val - Δx_scaled - x_minus = func(vars) - ∇x = (center_val - x_minus) / Δx_scaled^2 - end + 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 - ∇y = 0 - else - vars[:x] = x_val - if idx_y > 1 && idx_y < height - vars[:y] = y_val + Δy_scaled - y_plus_val = func(vars) - vars[:y] = y_val - Δy_scaled - y_minus_val = func(vars) - ∇y = (y_plus_val + y_minus_val - 2 * center_val) / Δy_scaled^2 - elseif idx_y == 1 - vars[:y] = y_val + Δy_scaled - y_plus = func(vars) - ∇y = (y_plus - center_val) / Δy_scaled^2 - else # idx_y == height - vars[:y] = y_val - Δy_scaled - y_minus = func(vars) - ∇y = (center_val - y_minus) / Δy_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 smalles value from a neighborhood of size (2Δx + 1) x (2Δy + 1) around the point (x, y) +# 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) - # Extract x and y values directly - x_val = vars[:x] - y_val = vars[:y] + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - # Pre-calculate positions for x and y in the array/matrix + 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 - min_val = func(vars) + # 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) @@ -208,22 +202,13 @@ function neighbor_min(func, vars, width, height; Δx = 1, Δy = 1) min_y = max(1, idx_y - Δy) max_y = min(height, idx_y + Δy) - # Calculate adjusted ranges to avoid division by zero in the loop - range_x = (min_x:max_x) .- idx_x - range_y = (min_y:max_y) .- idx_y - # Loop through the neighborhood - @inbounds for dx in range_x, dy in range_y - if dx == 0 && dy == 0 + @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 - - # Adjust the temp_vars for each iteration - vars[:x] = x_val + dx / (width - 1) - vars[:y] = y_val + dy / (height - 1) - - # Evaluate the function and update min_val - val = func(vars) + val = computed_values[dy, dx] if val < min_val min_val = val end @@ -234,30 +219,27 @@ 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) - # 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 + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - # Temporary variables to avoid repeated dictionary updates - temp_vars = copy(vars) + 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) - # Calculate pixel indices for the center point - idx_x = (x_val + 0.5) * (width - 1) + 1 |> round |> Int - idx_y = (y_val + 0.5) * (height - 1) + 1 |> round |> Int + # Initialize min_val using the center point value from the cached data + min_val = computed_values[idx_y, idx_x] - # Evaluate within a circular neighborhood + # 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 - temp_vars[:x] = (new_x - 1) / (width - 1) - 0.5 - temp_vars[:y] = (new_y - 1) / (height - 1) - 0.5 - - val = func(temp_vars) + val = computed_values[new_y, new_x] if val < min_val min_val = val end @@ -268,17 +250,20 @@ function neighbor_min_radius(func, vars, width, height; Δr = 1) 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) - # Extract x and y values directly - x_val = vars[:x] - y_val = vars[:y] + func_key = Symbol(string(func)) + if !haskey(vars, func_key) + cache_computed_values!(func, width, height, vars) # Ensure values are cached + end - # Pre-calculate positions for x and y in the array/matrix + 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 - max_val = func(vars) + # 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) @@ -286,22 +271,13 @@ function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) min_y = max(1, idx_y - Δy) max_y = min(height, idx_y + Δy) - # Calculate adjusted ranges to avoid division by zero in the loop - range_x = (min_x:max_x) .- idx_x - range_y = (min_y:max_y) .- idx_y - # Loop through the neighborhood - @inbounds for dx in range_x, dy in range_y - if dx == 0 && dy == 0 + @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 - - # Adjust the temp_vars for each iteration - vars[:x] = x_val + dx / (width - 1) - vars[:y] = y_val + dy / (height - 1) - - # Evaluate the function and update max_val - val = func(vars) + val = computed_values[dy, dx] if val > max_val max_val = val end @@ -310,48 +286,115 @@ function neighbor_max(func, vars, width, height; Δx = 1, Δy = 1) 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) - # Extract x and y values directly - x_val = vars[:x] - y_val = vars[:y] +# 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 - # Pre-calculate positions for x and y in the array/matrix + 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 sum and count - sum_val = func(vars) - count = 1 + # Initialize max_val using the center point value from the cached data + 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) + # 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 - # Calculate adjusted ranges to avoid division by zero in the loop - range_x = (min_x:max_x) .- idx_x - range_y = (min_y:max_y) .- idx_y + 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 + end + end + end - # Loop through the neighborhood - @inbounds for dx in range_x, dy in range_y - if dx == 0 && dy == 0 - continue + return max_val +end + +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) - # Adjust the temp_vars for each iteration - vars[:x] = x_val + dx / (width - 1) - vars[:y] = y_val + dy / (height - 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 - # Add the function evaluation to sum_val - sum_val += func(vars) + @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) + + sum_val = 0.0 + count = 0 + + 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 + sum_val += computed_values[new_y, new_x] + count += 1 + end + end + 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, @@ -362,5 +405,7 @@ gradient_functions = Dict( :neighbor_min => neighbor_min, :neighbor_min_radius => neighbor_min_radius, :neighbor_max => neighbor_max, - :neighbor_ave => neighbor_ave + :neighbor_max_radius => neighbor_max_radius, + :neighbor_ave => neighbor_ave, + :neighbor_ave_radius => neighbor_ave_radius ) \ No newline at end of file From f57abf454fa80c29b2174a9f2187aae97d0a123d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:42:17 +0200 Subject: [PATCH 28/31] Update UI.jl --- src/Renderer.jl | 81 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/src/Renderer.jl b/src/Renderer.jl index a3a9ce2..fb3b55a 100644 --- a/src/Renderer.jl +++ b/src/Renderer.jl @@ -75,12 +75,40 @@ function save_image_and_expr(img::Matrix{T}, genetic_expr::GeneticExpr; folder = println("Expression saved to: $expr_file") end -function generate_image_basic(func, width::Int, height::Int; clean = true) +# 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(:x => (x - 1) / (width - 1) - 0.5, :y => (y - 1) / (height - 1) - 0.5) # Add more variables if needed, e.g., :t => time + # 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 @@ -98,12 +126,15 @@ function generate_image_basic(func, width::Int, height::Int; clean = true) return img end -function generate_image_threaded(func, width::Int, height::Int; clean = true) +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 - 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 + # 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 @@ -121,12 +152,12 @@ function generate_image_threaded(func, width::Int, height::Int; clean = true) return img end -function generate_image_vectorized(func, width::Int, height::Int; clean = true) +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(:x => x, :y => y)), X, Y) + 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) @@ -139,7 +170,7 @@ function generate_image_vectorized(func, width::Int, height::Int; clean = true) return output end -function generate_image_vectorized_threaded(func, width::Int, height::Int; clean = true, n_blocks = Threads.nthreads() * 4) +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) @@ -154,7 +185,7 @@ function generate_image_vectorized_threaded(func, width::Int, height::Int; clean Y_block = Y[start_y:end_y, :] # Vectorize within the block - img_block = broadcast((x, y) -> invokelatest(func, Dict(:x => x, :y => y)), X_block, Y_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]) @@ -176,21 +207,45 @@ global height = 128 # Default height 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 = :threaded, kwargs...) +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, width, height; clean = clean) + return generate_image_basic(func, possible_types, width, height; clean = clean) elseif renderer == :vectorized - return generate_image_vectorized(func, width, height; clean = clean) + return generate_image_vectorized(func, possible_types, width, height; clean = clean) elseif renderer == :threaded - return generate_image_threaded(func, width, height; clean = clean) + return generate_image_threaded(func, possible_types, width, height; clean = clean) elseif renderer == :vectorized_threaded - return generate_image_vectorized_threaded(func, width, height; clean = clean, kwargs...) + return generate_image_vectorized_threaded(func, possible_types, width, height; clean = clean, kwargs...) else error("Invalid renderer: $renderer") end From 28b83d3615e6c2869cfb2018334c449d77173050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:42:51 +0200 Subject: [PATCH 29/31] Update Project.toml --- Project.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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" From 408fcb3eb6445faf572f207d1dff6350aaf7e01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:45:06 +0200 Subject: [PATCH 30/31] Add some garbage examples -- to be cleaned --- examples/animation10.jl | 15 + examples/animation11.jl | 15 + examples/animation12.jl | 16 + examples/animation13.jl | 21 + examples/animation14.jl | 19 + examples/animation15.jl | 40 + examples/animation16.jl | 34 + examples/animation17.jl | 51 ++ examples/animation18.jl | 37 + examples/animation19.jl | 39 + examples/animation20.jl | 72 ++ examples/animation21.jl | 72 ++ examples/animation22.jl | 21 + examples/animation23.jl | 21 + examples/animation3.jl | 16 + examples/animation4.jl | 22 + examples/animation5.jl | 22 + examples/animation6.jl | 22 + examples/animation7.jl | 18 + examples/animation8.jl | 21 + examples/animation9.jl | 14 + examples/animation_test.jl | 18 + examples/benchmark_append.jl | 265 ++++++ examples/benchmark_evolve.jl | 254 ++++++ examples/benchmark_evolve_threadvector.jl | 232 +++++ examples/debug20.jl | 76 ++ examples/example.jl | 57 +- examples/plot_benchmark.jl | 76 ++ .../plot_evolve_threadandvec_benchmark.jl | 60 ++ examples/refactor_code_5.jl | 596 +++++++++++++ examples/refactor_test.jl | 797 ++++++++++++++++++ examples/refactor_test_1.jl | 104 +++ examples/refactor_test_2.jl | 190 +++++ examples/refactor_test_3.jl | 325 +++++++ examples/refactor_test_4.jl | 575 +++++++++++++ examples/refactor_test_5.jl | 668 +++++++++++++++ examples/refactor_test_6.jl | 680 +++++++++++++++ examples/refactor_test_7.jl | 779 +++++++++++++++++ examples/testing22.jl | 76 ++ examples/testluxor20.jl | 76 ++ examples/v2_animation_1.jl | 193 +++++ examples/v2_animation_2.jl | 52 ++ examples/v2_animation_3.jl | 20 + examples/v2_animation_4.jl | 23 + examples/v2_animation_5.jl | 18 + examples/v2_animation_6.jl | 15 + examples/v2_animation_7.jl | 23 + examples/v2_animation_8.jl | 15 + examples/v2_animation_simple.jl | 20 + 49 files changed, 6888 insertions(+), 3 deletions(-) create mode 100644 examples/animation10.jl create mode 100644 examples/animation11.jl create mode 100644 examples/animation12.jl create mode 100644 examples/animation13.jl create mode 100644 examples/animation14.jl create mode 100644 examples/animation15.jl create mode 100644 examples/animation16.jl create mode 100644 examples/animation17.jl create mode 100644 examples/animation18.jl create mode 100644 examples/animation19.jl create mode 100644 examples/animation20.jl create mode 100644 examples/animation21.jl create mode 100644 examples/animation22.jl create mode 100644 examples/animation23.jl create mode 100644 examples/animation3.jl create mode 100644 examples/animation4.jl create mode 100644 examples/animation5.jl create mode 100644 examples/animation6.jl create mode 100644 examples/animation7.jl create mode 100644 examples/animation8.jl create mode 100644 examples/animation9.jl create mode 100644 examples/animation_test.jl create mode 100644 examples/benchmark_append.jl create mode 100644 examples/benchmark_evolve.jl create mode 100644 examples/benchmark_evolve_threadvector.jl create mode 100644 examples/debug20.jl create mode 100644 examples/plot_benchmark.jl create mode 100644 examples/plot_evolve_threadandvec_benchmark.jl create mode 100644 examples/refactor_code_5.jl create mode 100644 examples/refactor_test.jl create mode 100644 examples/refactor_test_1.jl create mode 100644 examples/refactor_test_2.jl create mode 100644 examples/refactor_test_3.jl create mode 100644 examples/refactor_test_4.jl create mode 100644 examples/refactor_test_5.jl create mode 100644 examples/refactor_test_6.jl create mode 100644 examples/refactor_test_7.jl create mode 100644 examples/testing22.jl create mode 100644 examples/testluxor20.jl create mode 100644 examples/v2_animation_1.jl create mode 100644 examples/v2_animation_2.jl create mode 100644 examples/v2_animation_3.jl create mode 100644 examples/v2_animation_4.jl create mode 100644 examples/v2_animation_5.jl create mode 100644 examples/v2_animation_6.jl create mode 100644 examples/v2_animation_7.jl create mode 100644 examples/v2_animation_8.jl create mode 100644 examples/v2_animation_simple.jl 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 From 6019660b31909c5838aedcac2ac6b0c7997dffd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jofre=20Vall=C3=A8s?= Date: Sun, 25 Aug 2024 13:48:35 +0200 Subject: [PATCH 31/31] Update README.md --- README.md | 6 ++++++ ...ed_performance_analysis_compared_minimum.png | Bin 0 -> 92648 bytes 2 files changed, 6 insertions(+) create mode 100644 combined_performance_analysis_compared_minimum.png 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 0000000000000000000000000000000000000000..cf095424038b95b80ea7ba66aa512717fe6a9929 GIT binary patch literal 92648 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 mV*|86