Packages

Elixir implementation of the GEPA (Genetic-Pareto) optimizer that combines LLM-powered reflection with Pareto search to evolve text-based system components.

Current section

Files

Jump to
gepa_ex examples 16_circle_packing.exs
Raw

examples/16_circle_packing.exs

#!/usr/bin/env elixir
Code.require_file("support/live_cli.exs", __DIR__)
defmodule CirclePackingExample do
@moduledoc false
@tolerance 1.0e-6
@seed_code """
pack = fn config, current_best_solution ->
n = config.num_circles
circle_radius = fn circle ->
Map.get(circle, :r, Map.get(circle, "r", 0.0))
end
circles =
if is_list(current_best_solution) and length(current_best_solution) == n do
current_best_solution
else
centers =
if n == 4 do
[{0.25, 0.25}, {0.75, 0.25}, {0.25, 0.75}, {0.75, 0.75}]
else
center = [{0.5, 0.5}]
inner =
for i <- 0..7 do
angle = 2.0 * :math.pi() * i / 8.0
{0.5 + 0.25 * :math.cos(angle), 0.5 + 0.25 * :math.sin(angle)}
end
outer_count = max(n - 9, 0)
outer =
if outer_count > 0 do
for i <- 0..(outer_count - 1) do
angle = 2.0 * :math.pi() * i / outer_count
{0.5 + 0.43 * :math.cos(angle), 0.5 + 0.43 * :math.sin(angle)}
end
else
[]
end
(center ++ inner ++ outer)
|> Enum.take(n)
end
Enum.map(centers, fn {x, y} ->
margin = min(min(x, y), min(1.0 - x, 1.0 - y))
%{x: x, y: y, r: margin * 0.65}
end)
end
%{
circles: circles,
all_scores: [Enum.reduce(circles, 0.0, fn circle, acc -> acc + circle_radius.(circle) end)]
}
end
"""
def seed_code, do: @seed_code
def problem(simple?) do
num_circles = if simple?, do: 4, else: 26
%{
name: "unit-square circle packing",
num_circles: num_circles,
timeout_ms: 5_000,
target_sum_radii: if(simple?, do: 1.0, else: 2.3)
}
end
def evaluate(candidate, problem, opt_state) do
result =
GEPA.CodeExecution.execute_code(candidate,
mode: :in_process,
timeout: problem.timeout_ms,
entry_point: :pack,
entry_point_args: [
Map.take(problem, [:num_circles, :timeout_ms]),
best_circles(opt_state)
],
seed: 0
)
returned = Map.get(result.variables, "__return__", result.result)
{score, circles, all_scores, details, feedback} =
evaluation_result(result, returned, problem)
{score,
%{
Input:
"Pack #{problem.num_circles} non-overlapping circles inside the [0,1] x [0,1] square.",
Output: inspect(circles),
Feedback: feedback,
code: candidate,
circles: circles,
all_scores: all_scores,
metrics: metrics(all_scores),
stdout: result.stdout,
error: result.error,
traceback: result.traceback,
validation_details: details,
scores: %{"sum_radii" => score}
}}
end
defp evaluation_result(%{success: false} = result, _returned, _problem) do
details = %{sum_radii: 0.0, execution_error: result.error}
{0.0, [], [0.0], details, "The candidate code did not execute successfully: #{result.error}"}
end
defp evaluation_result(_result, returned, problem) do
case validate_return(returned, problem.num_circles) do
{:ok, circles, all_scores, details} ->
score = details.sum_radii
{score, circles, all_scores, details, feedback_for(score, details, problem)}
{:error, reason, circles, all_scores, details} ->
{0.0, circles, all_scores, details, reason}
end
end
defp validate_return(%{} = returned, num_circles) do
with {:ok, raw_circles} <- fetch_key(returned, :circles),
{:ok, raw_scores} <- fetch_key(returned, :all_scores),
{:ok, circles} <- normalize_circles(raw_circles),
{:ok, all_scores} <- normalize_scores(raw_scores) do
validate_circles(circles, all_scores, num_circles)
else
{:error, reason} -> {:error, reason, [], [0.0], %{sum_radii: 0.0}}
end
end
defp validate_return(other, _num_circles) do
{:error, "pack must return a map, got #{inspect(other)}", [], [0.0], %{sum_radii: 0.0}}
end
defp validate_circles(circles, all_scores, num_circles) do
details = packing_details(circles, num_circles)
if valid_packing?(details) do
{:ok, circles, all_scores, details}
else
{:error, validation_feedback(details), circles, all_scores, details}
end
end
defp packing_details(circles, num_circles) do
shape_errors =
if length(circles) == num_circles do
[]
else
["expected #{num_circles} circles, got #{length(circles)}"]
end
boundary_violations =
circles
|> Enum.with_index()
|> Enum.flat_map(fn {circle, index} ->
if circle.x - circle.r < -@tolerance or circle.x + circle.r > 1.0 + @tolerance or
circle.y - circle.r < -@tolerance or circle.y + circle.r > 1.0 + @tolerance do
["circle #{index} is outside the unit square"]
else
[]
end
end)
negative_radii =
circles
|> Enum.with_index()
|> Enum.flat_map(fn {circle, index} ->
if circle.r < 0.0, do: ["circle #{index} has negative radius #{circle.r}"], else: []
end)
overlaps =
for {left, i} <- Enum.with_index(circles),
{right, j} <- Enum.with_index(circles),
i < j,
overlap?(left, right) do
"circles #{i} and #{j} overlap"
end
radii = Enum.map(circles, & &1.r)
sum_radii = Enum.sum(radii)
%{
expected_circles: num_circles,
actual_circles: length(circles),
boundary_violations: boundary_violations,
overlaps: overlaps,
negative_radii: negative_radii,
shape_errors: shape_errors,
min_radius: Enum.min(radii, fn -> 0.0 end),
max_radius: Enum.max(radii, fn -> 0.0 end),
avg_radius: if(radii == [], do: 0.0, else: sum_radii / length(radii)),
sum_radii: sum_radii
}
end
defp valid_packing?(details) do
details.shape_errors == [] and details.boundary_violations == [] and
details.overlaps == [] and details.negative_radii == []
end
defp overlap?(left, right) do
dx = left.x - right.x
dy = left.y - right.y
distance = :math.sqrt(dx * dx + dy * dy)
distance < left.r + right.r - @tolerance
end
defp validation_feedback(details) do
messages =
details.shape_errors ++
details.negative_radii ++ details.boundary_violations ++ details.overlaps
"Validation failed: " <> Enum.join(Enum.take(messages, 6), "; ")
end
defp feedback_for(score, details, problem) do
cond do
score >= problem.target_sum_radii - @tolerance ->
"Valid high-scoring packing. Preserve the constraints and concise code shape."
details.overlaps != [] ->
"Circles overlap. Reduce radii or move centers farther apart before increasing total radius."
details.boundary_violations != [] ->
"Some circles leave the unit square. Keep x-r, x+r, y-r, and y+r inside [0,1]."
true ->
"Valid packing with sum_radii #{Float.round(score, 4)}. Increase radii while preserving all unit-square and non-overlap constraints."
end
end
defp best_circles(nil), do: nil
defp best_circles(%{best_example_evals: evals}) when is_list(evals) do
Enum.find_value(evals, fn eval ->
side_info = Map.get(eval, :side_info, Map.get(eval, "side_info", %{}))
raw_circles = Map.get(side_info, :circles, Map.get(side_info, "circles"))
case normalize_circles(raw_circles) do
{:ok, circles} -> circles
{:error, _reason} -> nil
end
end)
end
defp best_circles(_opt_state), do: nil
defp fetch_key(map, key) do
string_key = to_string(key)
case {Map.fetch(map, key), Map.fetch(map, string_key)} do
{{:ok, value}, _} -> {:ok, value}
{_, {:ok, value}} -> {:ok, value}
_ -> {:error, "pack return map must contain #{inspect(string_key)}"}
end
end
defp normalize_circles(nil), do: {:error, "pack must return circles"}
defp normalize_circles(circles) when is_list(circles) do
circles
|> Enum.map(&normalize_circle/1)
|> collect_results()
end
defp normalize_circles(_circles), do: {:error, "circles must be a list"}
defp normalize_circle(%{} = circle) do
with {:ok, x} <- number_field(circle, :x),
{:ok, y} <- number_field(circle, :y),
{:ok, r} <- number_field(circle, :r) do
{:ok, %{x: x, y: y, r: r}}
end
end
defp normalize_circle({x, y, r}), do: normalize_circle([x, y, r])
defp normalize_circle([x, y, r]) do
with {:ok, x} <- finite_number(x, "x"),
{:ok, y} <- finite_number(y, "y"),
{:ok, r} <- finite_number(r, "r") do
{:ok, %{x: x, y: y, r: r}}
end
end
defp normalize_circle(other), do: {:error, "invalid circle #{inspect(other)}"}
defp number_field(map, key) do
string_key = to_string(key)
map
|> Map.get(key, Map.get(map, string_key))
|> finite_number(string_key)
end
defp finite_number(value, _field) when is_integer(value), do: {:ok, value * 1.0}
defp finite_number(value, _field)
when is_float(value) and value == value and value > -1.0e300 and value < 1.0e300 do
{:ok, value}
end
defp finite_number(_value, field), do: {:error, "#{field} must be a finite number"}
defp normalize_scores(scores) when is_list(scores) do
scores
|> Enum.map(&finite_number(&1, "score"))
|> collect_results()
|> case do
{:ok, []} -> {:ok, [0.0]}
other -> other
end
end
defp normalize_scores(_scores), do: {:ok, [0.0]}
defp collect_results(results) do
Enum.reduce_while(results, {:ok, []}, fn
{:ok, value}, {:ok, values} -> {:cont, {:ok, [value | values]}}
{:error, reason}, _acc -> {:halt, {:error, reason}}
end)
|> case do
{:ok, values} -> {:ok, Enum.reverse(values)}
{:error, reason} -> {:error, reason}
end
end
defp metrics([]), do: metrics([0.0])
defp metrics(all_scores) do
alpha_fixed = 0.1
ema_fixed =
Enum.reduce(tl(all_scores), hd(all_scores), fn score, acc ->
alpha_fixed * score + (1.0 - alpha_fixed) * acc
end)
alpha_adaptive = 2.0 / (length(all_scores) + 1)
ema_adaptive =
Enum.reduce(tl(all_scores), hd(all_scores), fn score, acc ->
alpha_adaptive * score + (1.0 - alpha_adaptive) * acc
end)
%{
max_score: Enum.max(all_scores),
mean_score: Enum.sum(all_scores) / length(all_scores),
ema_score_fixed: ema_fixed,
ema_score_adaptive: ema_adaptive
}
end
end
example = [
name: "Circle Packing Live Example",
script: "examples/16_circle_packing.exs",
summary: "Optimizes executable Elixir geometry code for unit-square circle packing.",
required: []
]
config = LiveCLI.parse_or_halt(System.argv(), example)
estimated_calls = max(config.max_metric_calls * 2, 1)
IO.puts(LiveCLI.cost_warning(example[:name], config.adapter, config.provider, estimated_calls))
problem = CirclePackingExample.problem(config.simple?)
{:ok, result} =
GEPA.OptimizeAnything.optimize_anything(
seed_candidate: CirclePackingExample.seed_code(),
dataset: [problem],
valset: [problem],
evaluator: &CirclePackingExample.evaluate/3,
objective:
"Improve executable Elixir code that packs non-overlapping circles inside a unit square while maximizing the sum of radii.",
background: """
Candidate code must bind a pack function:
pack = fn config, current_best_solution -> ... end
The function receives %{num_circles: n, timeout_ms: ms} and the best valid
prior circle list for the same problem, or nil. It must return
%{circles: circles, all_scores: scores}. Each circle may be %{x: x, y: y, r: r}
or [x, y, r]. All circles must stay inside [0,1] x [0,1], must not overlap,
and should finish quickly. Higher sum_radii is better.
Simple mode uses four circles for a short live smoke. Full mode uses
twenty-six circles, matching the upstream problem size.
""",
engine: %{
max_metric_calls: config.max_metric_calls,
reflection_minibatch_size: config.minibatch_size,
cache_evaluation: :memory,
frontier_type: :objective
},
reflection: %{
reflection_lm: config.client,
structured_output: config.structured_output?,
skip_perfect_score: false
}
)
IO.puts("""
Circle Packing Optimization Complete
====================================
Problem size: #{problem.num_circles} circles
Best sum_radii: #{Float.round(GEPA.Result.best_score(result), 4)}
Best code:
#{GEPA.Result.best_candidate(result)}
""")