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
45 changes: 38 additions & 7 deletions geneticengine/representations/stackgggp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ class Genotype:
class ListWrapper(RandomSource):
dna: list[int]
index: int = 0
# Total codon reads (does not wrap); used to detect one full genome pass.
consumed: int = 0

def randint(self, min: int, max: int) -> int:
if not self.dna:
raise IndexError("empty stack genome")
self.consumed += 1
self.index = (self.index + 1) % len(self.dna)
v = self.dna[self.index]
return v % (max - min + 1) + min
Expand All @@ -52,6 +57,10 @@ def random_float(self, min: float, max: float) -> float:
v = 1 * (max - min) / k + min
return v

def completed_one_pass(self) -> bool:
"""Whether at least one full pass over ``dna`` has been consumed."""
return bool(self.dna) and self.consumed >= len(self.dna)


def add_to_stacks(stacks: dict[type, list[Any]], t: type, v: Any):
if t not in stacks:
Expand All @@ -67,13 +76,29 @@ def find_element_that_meets_mh(stack, metahandler):


def create_tree_using_stacks(g: Grammar, r: ListWrapper, failures_limit=100):
"""Map a linear genome to a tree via typed stacks.

Translation continues until **both**:

1. the start-symbol stack holds at least one value, and
2. the genome has been consumed at least once (``len(dna)`` codon reads),

``failures_limit`` applies only until the first start-symbol value appears;
after that, mapping continues until one full genome pass. The phenotype is
the last start-symbol value observed when both conditions first hold.
"""
all_stack_types = g.get_all_mentioned_symbols()

stacks: dict[type, list[Any]] = {k: [] for k in all_stack_types}

failures = 0
last_good: Any | None = None

while not stacks[g.starting_symbol] and failures < failures_limit:
while True:
if last_good is not None and r.completed_one_pass():
return last_good
if last_good is None and (failures >= failures_limit or r.completed_one_pass()):
break
try:
weights = g.get_weights()
target_type: type[Any] = r.choice_weighted(
Expand Down Expand Up @@ -135,11 +160,13 @@ def create_tree_using_stacks(g: Grammar, r: ListWrapper, failures_limit=100):
v = apply_constructor(target_type, args)
add_to_stacks(stacks, target_type, v)
except IndexError:
failures += 1
if stacks[g.starting_symbol]:
return stacks[g.starting_symbol][0]
else:
raise GeneticEngineError("Stack genome not enough.")
# Only count mapping failures before the first start-symbol value;
# afterwards keep consuming the genome until one full pass is done.
if last_good is None:
failures += 1
if stacks[g.starting_symbol]:
last_good = stacks[g.starting_symbol][-1]
raise GeneticEngineError("Stack genome not enough.")


class StackBasedGGGPRepresentation(
Expand All @@ -148,7 +175,11 @@ class StackBasedGGGPRepresentation(
RepresentationWithCrossover[Genotype],
):
"""This representation uses a list of integers to guide the generation of
trees in the phenotype."""
trees in the phenotype.

Mapping stops once the start-symbol stack is non-empty **and** the genome
has been read at least once; the last start-symbol value is returned.
"""

def __init__(
self,
Expand Down
60 changes: 60 additions & 0 deletions tests/representations/stack/stack_stop_condition_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Stack mapping stops after one full genome pass with a start-symbol value."""

from __future__ import annotations

from abc import ABC
from dataclasses import dataclass

from geneticengine.grammar.grammar import extract_grammar
from geneticengine.representations.stackgggp import ListWrapper, StackBasedGGGPRepresentation, create_tree_using_stacks
from geneticengine.random.sources import NativeRandomSource


class Root(ABC):
pass


@dataclass
class Leaf(Root):
pass


@dataclass
class Node(Root):
a: Root
b: Root


def test_list_wrapper_tracks_consumed_across_wraps():
w = ListWrapper(dna=[10, 20, 30])
assert w.consumed == 0
assert not w.completed_one_pass()
w.randint(0, 9)
w.randint(0, 9)
w.randint(0, 9)
assert w.consumed == 3
assert w.completed_one_pass()
w.randint(0, 9) # wraps
assert w.consumed == 4
assert w.completed_one_pass()


def test_stack_mapping_requires_full_genome_pass_before_return():
g = extract_grammar([Leaf, Node], Root)
rep = StackBasedGGGPRepresentation(g, gene_length=64, failures_limit=10_000)
geno = rep.create_genotype(NativeRandomSource(0))
wrapper = ListWrapper(dna=list(geno.dna))
tree = create_tree_using_stacks(g, wrapper, failures_limit=10_000)
assert isinstance(tree, Root)
assert wrapper.completed_one_pass()
assert wrapper.consumed >= len(geno.dna)


def test_stack_mapping_continues_until_genome_consumed():
"""Even if a start-symbol value appears early, keep reading until one full pass."""
g = extract_grammar([Leaf, Node], Root)
rep = StackBasedGGGPRepresentation(g, gene_length=128, failures_limit=10_000)
geno = rep.create_genotype(NativeRandomSource(1))
wrapper = ListWrapper(dna=list(geno.dna))
create_tree_using_stacks(g, wrapper, failures_limit=10_000)
assert wrapper.consumed >= len(geno.dna)
Loading