Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions examples/benchmarks/mathematical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
"""Benchmarks based on Vaguery's mathematical programming challenges.

The scoring functions use :class:`fractions.Fraction` deliberately. These
problems are about exact rational arithmetic and floating-point fitness would
make correct programs appear incorrect for sufficiently large inputs.
"""

from __future__ import annotations

from abc import ABC
from dataclasses import dataclass
from fractions import Fraction
from typing import Annotated

from examples.benchmarks.benchmark import Benchmark
from geneticengine.grammar.grammar import Grammar, extract_grammar
from geneticengine.grammar.metahandlers.ints import IntRange
from geneticengine.problems import Problem, SingleObjectiveProblem


class IntegerExpression(ABC):
def evaluate(self, values: tuple[int, ...]) -> int:
raise NotImplementedError


@dataclass
class IntegerLiteral(IntegerExpression):
value: Annotated[int, IntRange(-100, 100)]

def evaluate(self, values: tuple[int, ...]) -> int:
return self.value


@dataclass
class InputInteger(IntegerExpression):
index: Annotated[int, IntRange(0, 1)]

def evaluate(self, values: tuple[int, ...]) -> int:
return values[self.index]


@dataclass
class Add(IntegerExpression):
left: IntegerExpression
right: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> int:
return self.left.evaluate(values) + self.right.evaluate(values)


@dataclass
class Subtract(IntegerExpression):
left: IntegerExpression
right: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> int:
return self.left.evaluate(values) - self.right.evaluate(values)


@dataclass
class Multiply(IntegerExpression):
left: IntegerExpression
right: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> int:
return self.left.evaluate(values) * self.right.evaluate(values)


@dataclass
class Quotient(IntegerExpression):
left: IntegerExpression
right: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> int:
denominator = self.right.evaluate(values)
return 0 if denominator == 0 else self.left.evaluate(values) // denominator


@dataclass
class Remainder(IntegerExpression):
left: IntegerExpression
right: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> int:
denominator = self.right.evaluate(values)
return 0 if denominator == 0 else self.left.evaluate(values) % denominator


@dataclass
class FiveUnitFractions:
terms: tuple[IntegerExpression, IntegerExpression, IntegerExpression, IntegerExpression, IntegerExpression]

def evaluate(self, values: tuple[int, ...]) -> tuple[int, ...]:
return tuple(term.evaluate(values) for term in self.terms)


@dataclass
class IntegerPair:
numerator: IntegerExpression
denominator: IntegerExpression

def evaluate(self, values: tuple[int, ...]) -> tuple[int, int]:
return self.numerator.evaluate(values), self.denominator.evaluate(values)


def fixed_egyptian_fraction(k: int) -> tuple[int, ...]:
"""Return five distinct nonzero denominators summing to ``1/k``."""
return 2 * k, 3 * k, 6 * k, 7 * k, -7 * k


def rational_egyptian_fraction(numerator: int, denominator: int) -> tuple[int, ...]:
"""Find five signed unit fractions for a small rational target.

The finite benchmark domain is intentionally small; exhaustive search is
used for the reference oracle so the benchmark remains independently
checkable and does not encode a particular synthesis strategy.
"""
target = Fraction(numerator, denominator)
forbidden = {numerator, denominator}
candidates = [i for i in range(-200, 201) if i and i not in forbidden]
for a in candidates:
for b in candidates:
if b == a:
continue
remainder = target - Fraction(1, a) - Fraction(1, b)
if remainder.numerator not in (-1, 1):
continue
c = remainder.denominator * remainder.numerator
if c in forbidden or c in {a, b} or c not in candidates:
continue
d = next((x for x in candidates if x not in {a, b, c} and -x in candidates), None)
if d is not None and -d not in {a, b, c, d}:
return a, b, c, d, -d
raise ValueError(f"no five-term representation found for {numerator}/{denominator}")


def reptend_fraction(prefix: int, repetend: int) -> tuple[int, int]:
prefix_digits = len(str(prefix))
repetend_digits = len(str(repetend))
cycle = 10**repetend_digits - 1
return prefix * cycle + repetend, 10**prefix_digits * cycle


def incremented_reciprocal(n: int) -> tuple[int, int]:
"""Increment every digit of the repeating decimal expansion of ``1/n``."""
if n <= 0:
raise ValueError("n must be positive")
remainder = 1 % n
seen: dict[int, int] = {}
digits: list[int] = []
while remainder and remainder not in seen:
seen[remainder] = len(digits)
remainder *= 10
digits.append(remainder // n)
remainder %= n
cycle_start = seen.get(remainder, len(digits))
nonrepeating = digits[:cycle_start]
repeating = digits[cycle_start:] or [0]
shifted_nonrepeating = [((digit + 1) % 10) for digit in nonrepeating]
shifted_repeating = [((digit + 1) % 10) for digit in repeating]
scale = 10 ** len(shifted_nonrepeating)
prefix = 0
for digit in shifted_nonrepeating:
prefix = 10 * prefix + digit
cycle_value = 0
for digit in shifted_repeating:
cycle_value = 10 * cycle_value + digit
denominator = scale * (10 ** len(shifted_repeating) - 1)
value = Fraction(prefix, scale) + Fraction(cycle_value, denominator)
return value.numerator, value.denominator


def _unit_fraction_error(candidate: tuple[int, ...], target: Fraction, forbidden: set[int]) -> float:
if len(candidate) != 5 or any(value == 0 for value in candidate):
return 1_000_000.0
error = abs(sum((Fraction(1, value) for value in candidate), Fraction()) - target)
penalty = 0 if len(set(candidate)) == 5 and not forbidden.intersection(candidate) else 1
return float(error) + penalty


def _pair_error(candidate: tuple[int, int], target: Fraction) -> float:
numerator, denominator = candidate
if denominator == 0:
return 1_000_000.0
return float(abs(Fraction(numerator, denominator) - target))


class _IntegerExpressionBenchmark(Benchmark):
expression_nodes = [IntegerLiteral, InputInteger, Add, Subtract, Multiply, Quotient, Remainder]

def get_grammar(self) -> Grammar:
return extract_grammar(self.expression_nodes + [self.root_type], self.root_type)


class FixedEgyptianFractionBenchmark(_IntegerExpressionBenchmark):
root_type = FiveUnitFractions

def __init__(self) -> None:
self.problem = SingleObjectiveProblem(
minimize=True,
target=0,
fitness_function=lambda candidate: _unit_fraction_error(
candidate.evaluate((7, 0)), Fraction(1, 7), {7}
),
)

def get_problem(self) -> Problem:
return self.problem


class RationalEgyptianFractionBenchmark(_IntegerExpressionBenchmark):
root_type = FiveUnitFractions

def __init__(self) -> None:
target = Fraction(5, 12)
self.problem = SingleObjectiveProblem(
minimize=True,
target=0,
fitness_function=lambda candidate: _unit_fraction_error(
candidate.evaluate((5, 12)), target, {5, 12}
),
)

def get_problem(self) -> Problem:
return self.problem


class ReptendFractionBenchmark(_IntegerExpressionBenchmark):
root_type = IntegerPair

def __init__(self) -> None:
target = Fraction(*reptend_fraction(539, 4762))
self.problem = SingleObjectiveProblem(
minimize=True,
target=0,
fitness_function=lambda candidate: _pair_error(candidate.evaluate((539, 4762)), target),
)

def get_problem(self) -> Problem:
return self.problem


class IncrementedReciprocalBenchmark(_IntegerExpressionBenchmark):
root_type = IntegerPair

def __init__(self) -> None:
target = Fraction(*incremented_reciprocal(7))
self.problem = SingleObjectiveProblem(
minimize=True,
target=0,
fitness_function=lambda candidate: _pair_error(candidate.evaluate((7, 0)), target),
)

def get_problem(self) -> Problem:
return self.problem


BENCHMARKS = [
FixedEgyptianFractionBenchmark,
RationalEgyptianFractionBenchmark,
ReptendFractionBenchmark,
IncrementedReciprocalBenchmark,
]
1 change: 1 addition & 0 deletions examples/mathematical_programming/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Examples for the mathematical programming challenges."""
8 changes: 8 additions & 0 deletions examples/mathematical_programming/fixed_egyptian_fractions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Genetic programming example for fixed-size Egyptian fractions."""

from examples.benchmarks.benchmark import example_run
from examples.benchmarks.mathematical import FixedEgyptianFractionBenchmark


if __name__ == "__main__":
example_run(FixedEgyptianFractionBenchmark())
8 changes: 8 additions & 0 deletions examples/mathematical_programming/incremented_reciprocal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Genetic programming example for the incremented reciprocal challenge."""

from examples.benchmarks.benchmark import example_run
from examples.benchmarks.mathematical import IncrementedReciprocalBenchmark


if __name__ == "__main__":
example_run(IncrementedReciprocalBenchmark())
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Genetic programming example for rational-target Egyptian fractions."""

from examples.benchmarks.benchmark import example_run
from examples.benchmarks.mathematical import RationalEgyptianFractionBenchmark


if __name__ == "__main__":
example_run(RationalEgyptianFractionBenchmark())
8 changes: 8 additions & 0 deletions examples/mathematical_programming/reptend_fraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Genetic programming example for repeating-decimal rational numbers."""

from examples.benchmarks.benchmark import example_run
from examples.benchmarks.mathematical import ReptendFractionBenchmark


if __name__ == "__main__":
example_run(ReptendFractionBenchmark())
5 changes: 5 additions & 0 deletions run_examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ run_example examples/benchmarks/santafe.py
run_example examples/benchmarks/string_match.py
run_example examples/benchmarks/vectorialgp.py

run_example examples/mathematical_programming/fixed_egyptian_fractions.py
run_example examples/mathematical_programming/rational_egyptian_fractions.py
run_example examples/mathematical_programming/reptend_fraction.py
run_example examples/mathematical_programming/incremented_reciprocal.py


run_example examples/progsys/Number_IO.py
run_example examples/progsys/Median.py
Expand Down
46 changes: 46 additions & 0 deletions tests/benchmarks/test_mathematical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from fractions import Fraction

from examples.benchmarks.mathematical import FixedEgyptianFractionBenchmark
from examples.benchmarks.mathematical import IncrementedReciprocalBenchmark
from examples.benchmarks.mathematical import RationalEgyptianFractionBenchmark
from examples.benchmarks.mathematical import ReptendFractionBenchmark
from examples.benchmarks.mathematical import fixed_egyptian_fraction
from examples.benchmarks.mathematical import incremented_reciprocal
from examples.benchmarks.mathematical import reptend_fraction


def test_fixed_egyptian_fraction_is_valid():
terms = fixed_egyptian_fraction(7)
assert len(set(terms)) == 5
assert Fraction(1, 7) == sum((Fraction(1, term) for term in terms), Fraction())


def test_reptend_example():
numerator, denominator = reptend_fraction(539, 4762)
assert Fraction(numerator, denominator) == Fraction(539 * 9999 + 4762, 1000 * 9999)


def test_incremented_reciprocal_examples():
assert incremented_reciprocal(1) == (1, 9)
assert incremented_reciprocal(2) == (11, 18)
# The page prints 16/163, but 0.overline{253968} reduces to 16/63.
assert incremented_reciprocal(7) == (16, 63)


def test_benchmarks_expose_zero_fitness_reference_targets():
fixed = FixedEgyptianFractionBenchmark()
candidate = type("C", (), {"evaluate": staticmethod(lambda _: fixed_egyptian_fraction(7))})()
assert fixed.get_problem().evaluate(candidate).fitness_components == [0]
reptend = ReptendFractionBenchmark()
assert reptend.get_problem().target == [0]
assert incremented_reciprocal(7) == (16, 63)


def test_all_benchmark_grammars_are_constructible():
for benchmark in (
FixedEgyptianFractionBenchmark(),
RationalEgyptianFractionBenchmark(),
ReptendFractionBenchmark(),
IncrementedReciprocalBenchmark(),
):
assert benchmark.get_grammar() is not None
Loading