From 59e24a8c72608f79ab92700265acee14eac57e3d Mon Sep 17 00:00:00 2001 From: Alcides Fonseca Date: Thu, 17 Sep 2026 17:10:22 +0000 Subject: [PATCH 1/3] Stop stack mapping after one genome pass with a start value. Keep translating until the start-symbol stack has produced a candidate and the genome has been fully read once; remember the last good value so later pops cannot erase a completed mapping. Co-authored-by: Cursor --- .../representations/stackgggp/__init__.py | 45 +++++++++++--- .../stack/stack_stop_condition_test.py | 60 +++++++++++++++++++ 2 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 tests/representations/stack/stack_stop_condition_test.py diff --git a/geneticengine/representations/stackgggp/__init__.py b/geneticengine/representations/stackgggp/__init__.py index c0591637..b2d333d1 100644 --- a/geneticengine/representations/stackgggp/__init__.py +++ b/geneticengine/representations/stackgggp/__init__.py @@ -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 @@ -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: @@ -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: + break try: weights = g.get_weights() target_type: type[Any] = r.choice_weighted( @@ -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( @@ -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, diff --git a/tests/representations/stack/stack_stop_condition_test.py b/tests/representations/stack/stack_stop_condition_test.py new file mode 100644 index 00000000..66d0e777 --- /dev/null +++ b/tests/representations/stack/stack_stop_condition_test.py @@ -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.nxt.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) From 577856fd3ae64799173015bce7cdbf982b70c8a5 Mon Sep 17 00:00:00 2001 From: Alcides Fonseca Date: Thu, 17 Sep 2026 17:20:13 +0000 Subject: [PATCH 2/3] Fail stack mapping after one empty genome pass. Without a start-symbol value, completing a full genome read must abort instead of looping forever on successful non-start constructions. Co-authored-by: Cursor --- geneticengine/representations/stackgggp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geneticengine/representations/stackgggp/__init__.py b/geneticengine/representations/stackgggp/__init__.py index b2d333d1..4b75ebe9 100644 --- a/geneticengine/representations/stackgggp/__init__.py +++ b/geneticengine/representations/stackgggp/__init__.py @@ -97,7 +97,7 @@ def create_tree_using_stacks(g: Grammar, r: ListWrapper, failures_limit=100): 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: + if last_good is None and (failures >= failures_limit or r.completed_one_pass()): break try: weights = g.get_weights() From b35b89ae594a7a8e3cfab83eb4d5ba33eeb7dfbc Mon Sep 17 00:00:00 2001 From: Alcides Fonseca Date: Wed, 7 Oct 2026 23:25:50 +0000 Subject: [PATCH 3/3] fix: correct stack representation test import --- tests/representations/stack/stack_stop_condition_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/representations/stack/stack_stop_condition_test.py b/tests/representations/stack/stack_stop_condition_test.py index 66d0e777..171fcc65 100644 --- a/tests/representations/stack/stack_stop_condition_test.py +++ b/tests/representations/stack/stack_stop_condition_test.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from geneticengine.grammar.grammar import extract_grammar -from geneticengine.nxt.stackgggp import ListWrapper, StackBasedGGGPRepresentation, create_tree_using_stacks +from geneticengine.representations.stackgggp import ListWrapper, StackBasedGGGPRepresentation, create_tree_using_stacks from geneticengine.random.sources import NativeRandomSource